do
len <- hF...">

パート2/3。 HaskellのICFPC 2009に理想的なVMコンパイラーであり、コメントを広めています

ここから始めましょう。



これで、1つの命令だけでなく、ファイル全体をデコードする方法がわかりました。



readMyFile = withBinaryFile "bin4.obf" ReadMode $ \ h -> do <br>

len <- hFileSize h<br>

buf <- mallocBytes $ fromInteger len<br>

hGetBuf h buf $ fromInteger len<br>

return (len, buf)<br>





これは基本的にファイルで機能するため、必須の要素です。 withBinaryFileはファイルを開き、指定された「ユーザー」関数を実行してハンドルを渡し、ファイルを閉じて、ユーザー関数が返したものを返します。 ここでは、$記号の後に、1つのパラメーターh(ハンドルから)を持つ「ユーザー定義」関数を記述しました。 この関数は、ファイルサイズを受け取り、バッファを割り当て、バッファに読み取り、バッファ自体とその長さ(バイト単位)を返します。 ここでの「ユーザー関数」には名前がなく、次のように始まることに注意してください。







\h -> function_body --







次は、バッファから命令とデータワードを取得する機能です。 バッファへの現在のポインタ(必要なバイト数だけシフト済み)がここに渡され、命令インデックスが渡されます。 仕様から、奇数-奇数インデックスを覚えており、オペコードとデータワードもそうです。 つまり、命令はゼロバイトから始まり、次に8番目から始まり、データワード(浮動)、それぞれ4番目から始まり、ゼロから始まります。



instruction :: Ptr a -> Int -> IO (Word32,Double)<br>

instruction ptr i = do <br>

let (iaddr,daddr) = if i `mod` 2 /= O then ( O , 4 ) else ( 8 , O ) -- instruction/data <br>

instr <- peek (plusPtr ptr iaddr) :: IO Word32 -- <br>

dta <- peek (plusPtr ptr daddr) :: IO Double -- <br>

return (instr, dta)<br>





操作が「不均等に」数学的に聞こえることに注意してください: "/ ="。

次に、バッファー全体で「サイクル」を実行し、ペアのリスト(指示が与えられます)を返します。

ast = mapM ( \ i -> instruction (plusPtr buf $ i * 12 ) i) [ O .. nframes - 1 ]<br>





ここでは多くのことが起こります。 まず、マップ関数など(特に、mapM)は次のように機能します。リストの1つの要素を変換する「ユーザー関数」が提供され、リスト自体がそれに渡されます。次に、mapはこのユーザー関数をそれぞれに適用しますリストの要素であり、この関数の値から新しいリストを形成します。 マップループは内部のどこかにあります(詳細は説明しません)。



let q = map (\h -> h * 2) [1,2,3,4,5]







[2,4,6,8,10]を返します。これは典型的な例です。 入力の問題にはインデックスがあり(ゼロから最後まで)、出力には各インデックスで「命令」関数を呼び出した結果のリストがあります。 すぐに理解するのは難しいこともありますが、その時が来ました-pythonとrubyはこの方向に進んでおり、それらと共に別の言語群(もちろん、そこにない言語から)が動いています。



ast = mapM ( \ i -> instruction (plusPtr buf $ i * 12 ) i) [ O .. nframes - 1 ]<br>







mapMに戻ると、渡された関数には、2番目の引数として0、1などが順に渡される命令呼び出しが含まれます。最初の引数として、反復ごとに12バイトのオフセットを持つバッファーが渡されます(1オペコード+ 1データワード= 12バイト)。 plusPtrは、指定されたバイト数から離れた新しいポインターを形成します。



それがサイクル全体です。 その結果、「フラット」オペコードのリストがあります(フラットは、仕様に従って1対1に変換されるため)。



[(Add 0 77 66, 0.0), (Sub 1 0 102, 0.0), ...]







つまり、セル77と66を追加し、結果を0にします。次に、セル0の内容からセル102の内容を減算し、結果を1 ...に置きま​​す。 これが私たちのコードです。 そして、ここにデータがあります:セル0で0.0、最初の0.0など。



この時点で、アセンブラーであっても、どの言語でも不完全なコードを停止して生成できます(結局、タスク仕様からの操作はそれに近い)。 これを行うには、各フラット操作の文字列(目的の言語)に翻訳を追加し、ループでコードを提供する必要があります。 しかし、それは逐語的に実行されるため、ゆっくりと退屈になります。



なぜ遅いのですか? 1回だけ使用されるいくつかの結果のメモリへの書き込み操作があり、実際には、レジスタまたはスタックのどこかに保存されることがあります(C / Haskell / Javaコンパイラなどがこれらの詳細を実行します) ASTを代表し、「Haskell」は「何でも」として機能します)。 また、くしゃみごとに構造フィールドにレコードを生成する場合、そこからインベントリを作成するコンパイラはありませんが、メモリには1つのレコードしかありません。



したがって、「メモリ」セルがどのカテゴリに分類されるかを分析します。



1)定数:読み取り専用、書き込み不可

2)実メモリー:書き込みの前に読み取り、読み取りよりも遅く書き込みます

3)一時変数:書き込み、次に複数の読み取り

4)ワンタイム変数:書き込み、次に読み取り。 カッコ付きの式に入ります。



誰がメモリセルを読み書きするかを理解するには、次のセクションで各命令の動作を説明する必要があります:一部の命令はメモリのみを読み取り(メモリからポートへの出力)、他の命令は書き込みのみ(ポートからメモリへ)、ほとんどの読み取りと書き込み(算術、条件付き)。



各命令の動作-読み取るもの(消費するもの)について説明します。



consumesData (Add _ r1 r2) = [r1,r2]<br>

consumesData (Sub _ r1 r2) = [r1,r2]<br>

consumesData (Mul _ r1 r2) = [r1,r2]<br>

consumesData (Div _ r1 r2) = [r1,r2]<br>

consumesData (Output r1 r2) = [r2]<br>

consumesData (If _ condr1 _ r1 r2) = [condr1,r1,r2]<br>

consumesData (Sqrt _ r2) = [r2]<br>

consumesData (Copy _ r2) = [r2]<br>

consumesData _ = []<br>





後者の場合、下線は「残りすべて」を意味し、最初の場合は、引数のコンストラクタのこの場所にあるものに興味がないことを意味します。 ご覧のとおり、ここでもパターンマッチングが使用されています。



同様に、各操作の記述(生成)に関する動作を説明します。

producesData (Add addr _ _) = [addr]<br>

producesData (Sub addr _ _) = [addr]<br>

producesData (Mul addr _ _) = [addr]<br>

producesData (Div addr _ _ ) = [addr]<br>

producesData (If _ _ addr _ _) = [addr]<br>

producesData (Input addr _) = [addr]<br>

producesData (Copy addr _) = [addr]<br>

producesData (Sqrt addr _ ) = [addr]<br>

producesData _ = []<br>





ポートに関して各命令が何を行うか-読み込むポートを説明します。

readsPort (Input _ port) = [port]<br>

readsPort _ = []<br>





そして、記録は彼女が書いたポートです:

writesPort (Output r1 _) = [r1]<br>

writesPort _ = []<br>





さらに、記録を短くするために(著者の癖)

cmap = concatMap<br>





concatMapは何をしますか? mapと同じですが、concatを実行します。 Concatは「リストを1レベル接着します」。 小さな例:



concat [ "hello" , "africa" ] = "helloafrica" <br>

concat [[ 1 , 2 , 3 , 4 ],[ 5 , 6 , 7 , 8 ]] = [ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ]<br>

concat [[]] = []<br>

<br>

map ( \ i -> [ 1 .. i]) [ 1 .. 5 ] = [[ 1 ],[ 1 , 2 ],[ 1 , 2 , 3 ],[ 1 , 2 , 3 , 4 ],[ 1 , 2 , 3 , 4 , 5 ]]<br>

concatMap ( \ i -> [ 1 .. i]) [ 1 .. 5 ] = [ 1 , 1 , 2 , 1 , 2 , 3 , 1 , 2 , 3 , 4 , 1 , 2 , 3 , 4 , 5 ]<br>









今、最も恐ろしい最大の作品:

produceCode ast dta = <br>

let <br>

inports = cmap readsPort ast :: [Int]<br>

outports = cmap writesPort ast :: [Int]<br>





readPort / writesportを各オペコードに適用し、すべてのリストを1つのリストに連結します。 したがって、一般的に入力ポートと出力ポートのリストを取得しました。

-- consumes,produces,outputs to port [(memory/port ref,op address)] lookup tables <br>

consumes = cmap ( \ (d,a) -> consumesData d `zip` [a,a .. ] ) (ast `zip` [ O.. ]) :: [(Int,Int)]<br>

produces = cmap ( \ (d,a) -> producesData d `zip` [a,a .. ] ) (ast `zip` [ O.. ])<br>

outputsPort = cmap ( \ (d,a) -> writesPort d `zip` [a,a .. ] ) (ast `zip` [ O.. ])<br>





ここでは、次の質問をすることができるいくつかのルックアップテーブル(リファレンスブック?)を作成しました。



指定したアドレスに書き込む命令はどのアドレスにありますか? (一般的に、質問は愚かです。したがって、タスクの定義により、この命令はアドレスNにありますが、私は今それを実現しました)

指定されたセルを読み取る命令のアドレスは何ですか?

指定されたポートに書き込む命令はどのアドレスですか?



Haskellでは、プリミティブルックアップテーブルはペア(キー、値)のリストです。 ここには、指定されたキーのすべての値を検索してリストを返す関数があります。 ここにあります:



-- op address that reads/writes given mem/port <br>

reads m = map snd $ filter ((m == ) . fst) consumes :: [Int]<br>

writes m = map snd $ filter ((m == ) . fst) (produces) :: [Int]<br>

outputs m = map snd $ filter ((m == ) . fst) outputsPort :: [Int]<br>





それらは、指定されたメモリ位置を読み取るか、指定されたポートに書き込む、または書き込むアドレスのリストを単に返します。 仕組み:

reads m = map snd $ filter ((m == ) . fst) consumes^M<br>





文字通り、2番目の要素を取得し、最初に指定されたmに等しい最初の要素を消費者ディレクトリ(以前に計算)から除外します。 どのように機能しますか?



フィルター関数は、「ユーザー関数」((m ==)。fst)とリストを入力として受け取ります。 リストから各項目の入力にカスタム関数が渡され、TrueまたはFalseが返されます。 このリストから、結果に応じて、要素が結果のリストに含まれるかどうかが決まります。 したがって、小さなリストが残っています。 このリストは、map関数の2番目の引数です(「$」演算子については前に説明したとおり)。 次に、マップはsnd関数をリストの各要素に適用し、結果のリストを形成します。



検討されていないもの。 snd / fst関数は、ペアの2番目または1番目の要素をそれぞれ返します。つまり、ペアの分解に関与し、簡単に言えば、2番目または1番目の要素を噛みます。



fst (1,2) = 1

snd (1,2) = 2

fst ("Hello", 2+2) = "Hello"

snd ("Hello", 2+2) = 4







そして今、Haskellの興味深い点の1つは、関数の構成です。 例を考えてみましょう:



fx = sin (cos (sqrt (x))), - :

fx = sin $ cos $ sqrt x







ここで、脳を動かして、「x」が常に機能の肉挽き器を通過することを確認します。最初にsqrt、次にcos、次に罪です。 確かに、関数fxのグラフを作成することは可能であり、グラフを作成することが可能であるため、これらの3つの関数のアプリケーションのシーケンスは同じ結果を与える1つの関数に対応するため、数学者だけが別の名前を付けませんでした!!! 正式な思考を持つ人々を許してください。



ここで、渡された関数をプロットするHaskell関数があるとします。例えば



plot :: (Double -> Double) -> Double -> Double -> IO() ( IO () void, : plot Double Double, Double ( ), void)

plot sin 0 6.28 -- sin 0 6.28

plot cos 0 3.14 -- , , .







ここで、sin関数またはcos関数をそのまま渡すことがわかります。引数は、グラフの作成中に作成された人に渡されます。 名前が発明されていない機能の完全な肉挽き器をどのように渡すことができますか?



そのような肉挽き器を説明する緊急の必要性があります。 したがって:



fun1 = sin -- 1

fun2 = sin . cos . sqrt -- , -







次に、不完全なアプリケーションについて説明します。 ここに、たとえば累乗法などの2つの引数を取る関数があります。

pow 2 3 = 8 --

pow2:

pow2 = pow 2







必要な2つの引数のうち1つの引数だけを渡しましたが、だれも私たちを止めてscりませんでした! pow2関数のタイプは何ですか? 非常に簡単です。彼女は別のDouble引数を必要とし、それを追加し、この引数でpow関数を呼び出します。2つは既に存在します!



pow2 4 = 16





Haskellのタイプを記述する表記も天井から取られたものではないことがわかりました。手を見てください:



pow :: Double -> Double -> Double -- double, double, double

pow2 :: Double -> Double -- double, double







また、pow2はpow関数に1つの引数を指定することで取得されたため、1つの引数をpowに追加すると新しい関数が作成されると結論付けることができます!



pow :: Double -> (Double -> Double) -- .







したがって、必要に応じて括弧を右側に追加できますが、意味は同じです。 また、これからも続きます。



pow 2 3 =(pow 2)3、つまり、最初に関数を生成してから、欠落している引数を追加します(関数をトリプルに適用します)。



羊に戻りましょう。

reads m = map snd $ filter ((m == ) . fst) consumes<br>





フィルタには関数があります:(m ==)。 fst。 ここでは、ポイント(組成)と部分的な適用を観察します:m ==。 これはm ==です。これは、入力で1つの引数を受け取り、等号の右側に代入する関数です。 m ==関数は、引数をmと比較した結果を返します。 mが5の場合、((m ==)5)はTrueを返します。



今、私たちはすべてを右から左に読みます:肉挽き器があり、それが含むものが最初にfst関数に送られ(ご存じのように、ペアから最初の要素を抽出します)、次にこの値がm ==に送られます。ブール値を返します。 したがって、カップル(a、b)が肉挽き器に入り、ブール値が出てきます。これはこのmに等しいでしょうか?



ここで、書き込みまたは読み取りされるセルへのすべての参照を選択する必要があります。そのため、次のことから始めます。



-- all memory references (including temporaries etc - from plain program) <br>

alldata = nub $ sort $ cmap consumesData ast ++ (map recast $ cmap producesData ast) :: [Int]<br>





ここで(右から左に読みます)「cmapproducesDataAstast」は、すべての書き込み可能なセルのリストをアドレスのフラットリストの形式で返します。同様に、consumsDataで発生します。 。 基本的に、nubにはソートされたリストは必要ありませんが、これは以前は知りませんでした。



nub [1,2,1] = [1,2]

nub [2,1,2] = [2,1]







-- constants <br>

constants = filter ( \ m -> O == (length $ writes m)) alldata :: [Int]<br>





そして、次のことが起こりました。誰も書き込めないすべてのアドレスをすべてのデータから選択しました。 そして、それらはそれらからのみ読み取られるため、おそらく定数です。 ここにリストがあります。



-- all persistent (optimized memory) <br>

persistents = filter ( \ m -> (head $ reads m) <= (head $ writes m)) (alldata \\ constants) :: [Int]<br>





そして、ここでは、繰り返しの間に時々保存されるすべての変数を選択しました。 これを行うには、すべてのデータを取得し、そこから定数を減算し(操作\\-リストの違い)、誰が最初に読み取り、誰が最初に書き込むかを見つけました。 あなたが彼らが書く場所の前で読むなら、彼らはそこに何かがあると信じています! 前回から残りました! したがって、この方法で、ブラックボックスのメモリである場所のリストを作成し、読み取りおよび書き込みに使用できるようにしました。



-- temporaries which are reused parts of expressions <br>

lets = filter ( \ m -> 1 < (length $ reads m)) (alldata \\ constants) \\ persistents :: [Int]<br>





これで、残りのデータから(すべてのデータからこれまでに知っているすべてを差し引いたため)、複数回読み取られたセルが見つかりました。 これは永続的なデータではないため(除外)、最初に計算されてから数回使用されるのはおそらく一時変数です。 一時変数は同じ反復内で使用されますが、それ以外では必要ありません。



-- expressions to inline (variables that are used 1 time) <br>

onerefs = filter ( \ m -> (length $ reads m) == 1 && (length $ writes m) == 1 ) <br>

((alldata \\ constants) \\ persistents) :: [Int]<br>





そして、ここでは、一度書き込まれてから1回読み取られる変数を計算しました。 それらについては、何も開始せず、角括弧で囲まれた式がそれらから形成されます。 ブラックボックスの作成者は、このようなクラスの変数を必要としていました。なぜなら、これらのクラスには低レベルの言語があり、高レベルの結果ではプロセッサのレジスタに住んでいるからです。しかし、コンパイラは私たちの知らないところでこれを処理します。



次に、ツリー操作(操作中に読み取られるアドレスのみが書き込まれる)から、ツリーのようなオペコード、つまり接尾辞Expを持つものに移動する必要があります-それらは独自の種類へのリンクを含み、ツリーを形成します。 これを行うために、アドレス全体でツリー全体を生成する関数を記述します。 再帰的になります。 定数が存在するアドレスを彼女に与えれば、やるべきことはあまりないからです。 そして、他の2つのセルを追加した結果が書き込まれるアドレスを彼女に与えると、他の2つのOpを追加するOpを生成する必要があり、それらはそこに定数であるか、突然乗算の結果があります...



-- geherates reference to the expression identified by given address, <br>

-- this excludes address where to store it, only value is obtained <br>

ref :: DestAddr -> Op<br>

ref a <br>

| elem a constants = Const (dta !! a)<br>

| elem a onerefs = geneval a<br>

| elem a lets = ReadVarExp a<br>

| elem a persistents = ReadExp a<br>

| otherwise = trace "oops1" $ undefined<br>





ここに、入力パラメーターにいくつかの前提条件が重ねられたref関数があります。そのような条件の構文は、縦棒、条件、等号、関数の本体で、条件が真の場合に満たされます。 if ... elseif ... elseif ... else ... endifのような大きなものです。 トレース "oops" $ undefinedで説明した最後のエラー-エラーを出力し(トレース)、プログラムを停止します(未定義)。



入力アドレスが定数に属している場合、定数へのアクセスを生成します(定数を表すOpのみ)。 Constコンストラクターパラメーターによって決定される定数は、単に指定されたアドレスのメモリ値です。 dtaはすべてのメモリセルを順番に含むリストで、dfa !! aはリストのa番目の要素へのアクセスです。



入力アドレスがonerefsに属している場合(メモリが1回書き込まれ、次に1回読み取られる)、他の2つのopでツリーノードを形成する必要があります(括弧を使用した操作)。 Genevalが呼び出され(以下で説明します)、単純なOp(2つのメモリセルを追加し、結果を3番目に配置)から生成されます(他の2つのOpを追加し、_in general_を返します)



同様に、一時変数(一度書き込まれ、多くが読み取られます)の読み取り、および入力ポートの読み取り用。



geneval関数自体については後で説明しますが、パターンマッチングを使用します。 受信したアドレスから、(リストastから)フラットOpを選択し、ツリーに変換します。 奇妙なことに、この関数は上記のようにrefを呼び出すため、関数は相互に再帰的です。



-- turns plain code in tree code, converting memory refs to ops via "ref" <br>

geneval a = e $ ast !! a<br>

e (Add addr r1 r2) = AddExp (ref r1) (ref r2)<br>

e (Sub addr r1 r2) = SubExp (ref r1) (ref r2)<br>

e (Mul addr r1 r2) = MulExp (ref r1) (ref r2)<br>

e (Div addr r1 r2) = DivExp (ref r1) (ref r2)<br>

e (If cmdop cmdr addr r1 r2) = IfExp cmdop (ref cmdr) (ref r1) (ref r2)<br>

e (Input addr r1) = ReadPortExp r1<br>

e (Sqrt addr r1) = SqrtExp (ref r1)<br>

e (Copy addr r1) = ref r1<br>

e x = trace (show x) $ undefined<br>





Op Copyは、結果を元のアドレスに渡すことを意味します。 Add / Sub / Mul / Divコマンド-フラットからツリーへの逐語的な変換。 入力はポートから読み取るように変換されます。 一時変数または定数変数からの読み取りは、上記でrefに変換されます。



次に、世代を書きます

retval = "module Vm where \n data VMState = VMState { " ++ <br>

(intercalate "," $ <br>

map (( "m" ++ ) . show) persistents ++ <br>

map (( "o" ++ ) . show) outports<br>

) ++ "::Double } \n " ++ <br>







これにより、構造自体の説明を含むパーツが生成されました。 ここで、明らかに、(++)はリスト(文字列)を接着する演算子であり、インターカレートは複数のリストを接着し、指定されたリストと交互に並べます(読みます:区切りコンマを挿入します)。構造には、プレフィックスm(メモリ)とoの接頭辞が付いた出力ポート-出力ポート。 最後に、Double型を指定し、改行を忘れないでください。



Double型ではなく、!! Double型を指定すると、レイジーフィールドではなくレイジーフィールドを持つ構造になります。 この研究では、すべての出力ポートを計算すると、速度の差が20倍以上であることが示されています(前の投稿を参照)。 さて、厳密性注釈(感嘆符)を置き換えるかどうかは読者次第です。 「すべての出力ポート」と言ったのはなぜですか? 遅延計算では、N回目の反復後にこの構造の値を取得し、たとえば、必要なポートを1つだけ要求できるためです。 仕事の大きな前線が開き始め、一時停止後、計算された値が来ます(たとえば、



putStrLn $ "o10=" ++ (show $ o10 vmlast)









その後、次のようになります。「o10 =」がコンソールに書き込まれ、その後一時停止し、値が表示されます。 同じputStrLnを再度書き込むと、遅延はもうなくなります。 計算された値。 後でo11を書くように依頼すると、遅延は再び大きくなりますが、o10とo11にはo10についてすでに完全に計算されており、o11については考慮されない可能性が高いため、おそらくそれほど大きくないでしょう。



"initState = VMState {" ++ <br>

(intercalate "," $ <br>

map ( \ a -> "m" ++ (show a) ++ "=" ++ (show $ dta !! (recast a))) persistents ++ <br>

map (( ++ "=O" ) . ( "o" ++ ) . show) outports<br>

) ++ "} \n " ++ <br>





ここで、ブラックボックスの状態の初期値を作成する関数が生成されました。 すべてのメモリは、適切な場所(dta)にあったバイナリの値で初期化されます。 すべての出力ポートはゼロに初期化されます。 Haskellでは、構造の必須の初期化が必要です。



"nextState x (" ++ (intercalate "," $ map (( "i" ++ ) . show) inports) ++ ")= \n let \n " ++ <br>

cmap ( \ a -> " t" ++ show a ++ "=(" ++ (ast2haskell "x" $ geneval a) ++ ") :: Double \n " ) lets ++ <br>

" in x { \n " ++ <br>

intercalate ", \n " (<br>

map ( \ a -> " m" ++ show a ++ "=" ++ (ast2haskell "x" $ geneval a)) persistents ++ <br>

map ( \ a -> " o" ++ show a ++ "=" ++ (ast2haskell "x" $ geneval $ head $ outputs a)) outports )<br>

++ "} \n " ;<br>





ここでは、最もボリュームのある部分が生成されます。 まず、入力パラメーターのタプルが次のような形式で形成されます:(i10、i11、i60000)、その後、「let」が入り、すべての一時(非永続)変数がプレフィックス「t」で入力されます。 次に、各メンバーの式が記述される新しい構造が形成されます:x {m20 = expr1、m25 = expr2 ...、o10 = exprN}。 初期化tおよびmの式は、「tNN」、「iNN」、および「mNN x」を指します。後者は、前の反復のメモリ値を意味します。



in retval -- .









エンディングが続きます



All Articles