最適化コンパイラが作られている方法論を学ぶために、 に特化したもの を作りました。 6502 アーキテクチャ . 奇妙なことに、私のコンパイラは、GCC、LLVM、および対照的なすべてのさまざまなコンパイラよりも早くコードを生成します。
私のコンパイラは、過剰な段階の最適化のフレーズで余計なことをしなくなったと思いますので、肯定的な側面は、コード生成面からのものであることが望まれます。 ほとんどのコンパイラはマルチターゲットであり、バックエンドは最新の RISC に似たシステム用に設計されており、もはやぼろぼろの 6502 ではないため、これは理想的です。スピードのクロージングレグ。
私のコンパイラは、VBCC、SDCC、KickC などのレトロ システムや組み込みシステム用に設計されたものよりも優れています。 そういうわけで、私の方法論を書き留めておくのは適切な提案のように思えました.
不正行為の免責事項
理解を深める前に、コンパイラが利益を上げている 3 つの領域について説明したいと思います。 私はこれらを私の「チート」と呼んでいます。さまざまなコンパイラ がおそらくうまく 実行する可能性があるためです。
最初に、私のコンパイラは として知られているものを生成します。 「違法」な指示. 違法な命令とは、製造元によって正式に文書化されていない命令ですが、ハードウェア内に軽度の命令が存在する場合です。 ほとんどの 6502 チップには、2 つの「重罪」命令の実行を組み合わせた 1 組の違法な命令が存在します。 彼らの支出は一対のサイクルを設定することができますが、すべてのバックエンドがそれらを生成するわけではありません.
2d、私のコンパイラのプラス面のペアは、その計算努力によっても定義されます。 私のコンパイラは、CPU バジェット (ミリ秒のペア) の大部分を別の命令に費やしていますが、すべてのコンパイラが実行しているわけではありません。 通常、解決策を探すために残業をすればするほど、結果は大きくなります。
最後に、さまざまなコンパイラのベンチマークを実行するときに、むしろ評価が面倒な、パドルの有効性を代替するループ展開とさまざまな最適化があります。 私はこれに悩まされていない綿密な評価を擁護しようとしましたが、明らかに私はもはや理想的ではない立場にあります.
正誤判断してアルゴリズムへ!
私は私の塗りつぶしをしようとしています」アウトサイダーアート」と書いて、コード生成の重要性を知らなかったので書いてみました。 実際のところ、この方法論は私のコンパイラではうまく機能しませんでした。 私は主に、何年も前に書いた不規則なアニメーションの配置に主に基づいています。 私のアルゴリズムは、異なる命令とレジスタ割り当てを組み合わせているため、注目を集めていますが、 継続の支出が書かれているためです。
。 私の長年のプログラミングの中で、これは私がそのような暗い魔法のために学んだ最も生産的な合理的な支出です.
フリートが最終的に機能する仕組みを示すために、現在のすべてのブロックが私の IR は DAG で SSA 発信 。 コード生成の最初のステップは、これらの両方のプロパティを中断し、現在のブロックを full-ordered
順序付けられた現在のブロックは、開始から合計まで処理され、操作ごとに一致するコードの組み合わせのペアが生成されます。 組み合わせのリストは指数関数的に増加しますが、ほとんどの場合、動的プログラミング および branch-and-sure。
生成されたコードを強化するために、現在のすべてのブロックの出力状態が入力として後続ブロックに供給されます。 このジョブは、プット ポイントに到達しない限り繰り返されます。 冷却時間デスク a-la
模擬焼鈍 を使用して検索します。結果が早くなります。 この後、現在のすべてのブロックには、調達するコード シーケンスのペアが含まれている可能性があります。 最もコストのかかるものもヒューリスティックに剪定され、
パーティション化されたブール値を解くことによって、適切な違いも作成されます。二次状況 (PBQP)。 レーズ内では、主要なブロック間の遷移として追加の命令が挿入され、いくつかの最適化パスが適切な測定のために次の会議コードで高速化されます。
詳細説明
Classic Block IR
前述のとおり、IR は現在のブロックごとに DAG インイン SSA 発信、画像を一瞥しないと複雑に聞こえます。
以下のコードを考える:
foo=fn() ^ 5 return foo – (foo & 3)
IR DAG は次のように表示されます:
グラフ内のすべてのノードは価格を表し、エッジは情報のドリフトを示します。 価格を計算するために、それを指しているノードが最初に計算されることを望みます — 例えば、
(&)
、ノード (^)
と (3)
したい最初に計算されます。
ディペンデンシーグラフです)。
ご注文 コード生成の最初のステップは、IR を閉じて防御し、そのノードから完全な発見を行い、すべてのノードをその依存関係の後に配置します。 ここでは、すべてのエッジ システムが同じ方向 (プロット内で下向き) になるように、すべてのノードを束線に配置することに相当します。
生成された会議コードは、この発見を明らかにします.
どうすればこれを発見できますか?
遠いです トポロジカルの支出を制定するためにまっすぐ進むただし、欠点があります。DAG には正当な順序付けのペアがあり、いくつかは他のものよりも優れたコードを生成します
適切な順序付け (効率的なコードを生成するもの) を得るために、トポロジカル ソート アルゴリズムはやや時代遅れですが、ヒューリスティックのペアを使用して修正する必要があるかもしれません。 まず、さまざまなノードの後に発生するノードを制限するために、「偽の」エッジもグラフに追加されます。 2d では、把握アルゴリズムも時代遅れになり、トラバーサルでトポロジー形式が明らかになります。 私のコンパイラでは、 グラフを通る最長ルート であり、エビ を含む順序が生成されます。 は居住範囲 であり、レジスターの剛性とショップがはるかに少なくなります。
まさにここに私のコンパイラからの真のDAG.
クラシック ブロック命令の置換 - ピース 1: アニメーション
IR の意向により、会議コードを生成するまでには時間がかかりました。 先ほど、私のアルゴリズムはアニメーションの配置と歩調を合わせていたと述べました。これは珍しく、無関係に聞こえるかもしれませんが、最初にアニメーションの配置を指定してから、コンパイラのように動作するように長くする方が簡単なようです。
私が話しているアニメーションは、以前はファミコンのゲームでした。 その配置では、フレームごとに最近のタイルセットをビデオ メモリにロードし、古いフレームのタイルセットを上書きすることによって、アニメーションも実行されます。
ほとんどのゲームは、マッパーと呼ばれるカートリッジに特定のハードウェアを使用することを発明しました。 それにもかかわらず、私の場合、そのハードウェアの断片を費やすつもりのないゲームを作っていました。 代わりに、センターマンとして CPU の消費するバイトを再現する必要がありました。 バイトを再現する明白な方法論は、ループ (C からの「memcpy」を仲介する) を使用することですが、これは緩すぎることが判明しました。 反復ごとに、インクリメント、比較、ループへの分岐に非常に重要な時間が費やされていました。
最も生産的な方法論一見すると、ループを展開した場合、以前は機能していましたが、一度ではありませんでしたが、完全に機能していました。 たとえば、バイト シーケンス (10、20、30) をビデオ メモリに再生したい場合は、次のようなミーティング コードを書くこともできます:
lda #20 ; 負荷定数: 20 sta PPUDATA ; ビデオ RAM lda #30 に追加します。 負荷定数: 30 sta PPUDATA ; ビデオ RAM lda #40 に追加します。 負荷定数: 40 sta PPUDATA ; ビデオ RAM に追加 
さまざまな言葉で、私は使用forループが何であったかを勉強するよりも早くプログラミングのクラスをやめた初心者のようにコードを書くこと。 とはいえ、以前は完全に素早かったので、それが重要な理由でした.おそらくそれを穏やかに自動化する必要があります。 単純な方法は、アップロードされたバイトごとに 1 つのロードと 1 つのストアを生成するスクリプトを書き留めることでしょう。 レジスタが値を保持しているため、(1, 8, 1, 8, 1, 8) のような系列には、最も単純な 2 つのロードが必要です:
ldx #1 ; レジスタ X lda #8 をロードします。 stx PPUDATA ; レジスタ A をロードします。 レジスタ X をビデオ RAM sta PPUDATA に追加します。 ビデオ RAM にレジスタ A を追加 stx PPUDATA sta PPUDATA stx PPUDATA sta PPUDATA
これらの負荷を最適に決定するにはどうすればよいですか?
問題を簡単に解決するために、最初に CPU でこれを解決する方法を示します。
レジスタが 1 つの CPU の場合、アルゴリズムは冗長な負荷を見つけてそれらを取り除くことになります。 冗長負荷ということで、以下の2つ目の負荷のように、停止のない負荷をお勧めします:
lda #10 sta PPUDATA lda #10 // 冗長 sta PPUDATA 
冗長な負荷を発見して排除することは、控えめに言っても成立しません。 以前にロードされた価格の重罪助長発見。 以下の疑似コードは、最適には
という重罪を犯します。 アップロードするバイトごとに prev=-1: if prev !=byte: emit_load(byte) prev=byte emit_store()
このアイデアは ほぼ 3 つの「前の」変数を 1 つの変数として使用することで、3 つのレジスターを使用するように拡張できます。 問題を整然と解決するために、構造体またはファイル形式を使用することをお勧めします:
struct cpu_state { int a int x int y }
残念ながらマイナス面があります。 すべてのレジスターを見つけてショップを削除できると仮定しても、負荷を軽減する必要があるかどうかはわかりません。 価格がどのレジスターにも表示されなくなった場合、どのレジスターがそれを穏やかにロードする必要があるでしょうか?これらの決定は、負荷の集合体 (ブルート ドライブ) を持っている可能性があるすべての会議コードを生成し、最も単純な方法で健全な状態を維持することによって、最適に発明することができます。 1つですが、この艦隊は実行不可能になります。 ロードが必要なバイトごとに、おそらく十分に所有できる 3 つの選択肢があります。 組み合わせの選択肢は指数関数的に増加します.
総当り探索木の描画幸いなことに、このブルート ドライブ デバイスは、時代遅れのトリックであれば、軽度に使用できます。 それ検索場所の広さにビックリして、ほとんどの組み合わせを削除する必要があるかもしれません.
会議コードが2つ生成された場合、両方とも同一の観測可能なファセット ストップを持ち、よりコストの高い方も剪定されます。 ファセット効果により、同一のバイト シーケンスをアップロードし、同一のレジスタ状態で停止することをお勧めします。 説明のために、次の 2 つのコードは同等です:
lda #1 sta PPUDATA ldx #2 stx PPUDATA ldy #3 sty PPUDATA sta PPUDATA
ldy #1 sty PPUDATA ldx #2 stx PPUDATA ldy #3 sty PPUDATA lda #1 sta PPUDATA 
毎回アップロードシーケンス (1、2、3、1) であり、両方ともレジスタを (A=1、X=2、Y=3) のままにします。 ただし、2 つ目はさらに負荷がかかるため、プルーニングも必要になる可能性があります。コードも。 完全なコード シーケンスを生成し、それらを比較する代わりに、命令ごとにコードを生成し、すべてのステップで評価する必要があります
これを実現するために、アルゴリズムは組み合わせを幅優先で処理します。 最初に最初のバイトの完全な組み合わせを生成し、一部を削除します。次に、2 番目のバイトの完全な組み合わせを生成し、一部を削除し、他の多くの要素を削除します。 プルーニングは、すべての集計の「cpu_state」(以前に起動された構造体) によってインデックス付けされたハッシュ デスクに結果を格納することによって実行されます。
効率を高めるために、剪定のさらなる工夫は時代遅れに見えるでしょう。 すべての反復ステップで最も便利な日付と同じくらい多くの集計を見つけることに成功した場合、3 つまたは余分な負荷で遅いさまざまな組み合わせを取り除くことができます。 これにより、任意の「cpu_state」から別の負荷に変換するには 3 回の負荷で十分であるため、最終的な結果の最適性が維持されます。
これらすべてをまとめると、3 レジスタ CPU で負荷を排除するアルゴリズムは次のようになります。
アップロードするバイトごとに prev_hash_map.determined(): next_lowest_cost=INFINITY next_hash_map.determined() ごとにAggregate in prev_hash_map: if aggregate.trace>=prev_lowest_cost + 3: // プルーニングの 2 次元の工夫が各レジスタ (A、X、Y) ごとに継続: new_combination=aggregate if new_combination.cpu_state.register !=byte: new_combination.code.append_load () new_combination.trace +=1 new_combination.cpu_state.register=byte if aggregate.trace (真のコードをのぞき見することに勝った場合に備えて、C++ 実装をセットアップしました ここ と ここ。)
私はこのアニメーションの仕事のためにカウボーイビバップのイントロをファミコンのROMに変換しました。 GIF は低いフレームレートで実行されます。 クラシック ブロック命令の置換 - ピース 2: コンパイラ
わかった、わかった。 . . アニメーション用のコードを生成できます。 ここにコンパイラに関する社説があります、モラル?
さて、これを提案してください:
このアルゴリズムで一連のバイトを処理する代わりに、先ほど作成した一連のIR操作
これは先ほどのIR操作の順番です。 各ノードのプレースホルダーとして変数名 (Q、R、S、T) を追加しました:
Q=名前 fn R=Q ^ 5 S=R & 3 T=R - S return T
バイトのシーケンスに対して行った評価、操作ごとに組み合わせを生成し、最悪のものを取り除くことができますが、2 つあるにもかかわらず広範な違い:
さまざまな IR 操作により、さまざまな会議の指示が生成されます (もはや重罪ロードではありません)。 「cpu_state」構造体も変数名を検出します。
これをディスカバーに打ち込んで、アルゴリズムが注文されたIR。 アルゴリズムは、最初の操作の組み合わせを作成することから始まります:
"name fn"
、最も単純なものがあります:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?)
その後、XOR 演算 (^)。 これを行う単一の会議命令があります: 「EOR」、それにもかかわらず、XOR は交換可能であるため、2 つのコードの組み合わせが存在します:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor:lda#5; レジスタ A eor Q に 5 をロードします。 レジスタ A と変数 Q sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R, X=?, Y=?)
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor: eor #5 ; レジスタ A と変数 5 sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R, X=?, Y=?)
2 番目の集約は、Q 変数のロード命令を必要としなくなります。これは、既にレジスター内にロードされているためです。 これらのコード シーケンスはどちらも同一のファセット ストップを持っているため、最初の集計は削除されたように見えます。
さて、AND演算(&)。 AND 命令がありますが、SAX と呼ばれる不正な命令もあります。これは、レジスタ A とレジスタ X のビットごとの AND を開始し、停止結果を格納します。 これは 4 つの組み合わせで終わるので、枝刈りされたものを派生させない 2 つを最も簡単に示します:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor: eor #5 ; レジスタ A と変数 5 sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R、X=?、Y=?) です。 および: および #3 ; AND レジスタ A と 5 sta S ; 停止結果を変数 S に格納します。 cpu_state は (A=S, X=?, Y=?)
レーズ内では、主要なブロック間の遷移として追加の命令が挿入され、いくつかの最適化パスが適切な測定のために次の会議コードで高速化されます。
詳細説明
Classic Block IR
前述のとおり、IR は現在のブロックごとに DAG インイン SSA 発信、画像を一瞥しないと複雑に聞こえます。
以下のコードを考える:
foo=fn() ^ 5 return foo – (foo & 3)
IR DAG は次のように表示されます:
グラフ内のすべてのノードは価格を表し、エッジは情報のドリフトを示します。 価格を計算するために、それを指しているノードが最初に計算されることを望みます — 例えば、
(&)、ノード
(^)
と (3)
したい最初に計算されます。
ディペンデンシーグラフです)。
ご注文 コード生成の最初のステップは、IR を閉じて防御し、そのノードから完全な発見を行い、すべてのノードをその依存関係の後に配置します。 ここでは、すべてのエッジ システムが同じ方向 (プロット内で下向き) になるように、すべてのノードを束線に配置することに相当します。
生成された会議コードは、この発見を明らかにします.
どうすればこれを発見できますか?
遠いです トポロジカルの支出を制定するためにまっすぐ進むただし、欠点があります。DAG には正当な順序付けのペアがあり、いくつかは他のものよりも優れたコードを生成します
適切な順序付け (効率的なコードを生成するもの) を得るために、トポロジカル ソート アルゴリズムはやや時代遅れですが、ヒューリスティックのペアを使用して修正する必要があるかもしれません。 まず、さまざまなノードの後に発生するノードを制限するために、「偽の」エッジもグラフに追加されます。 2d では、把握アルゴリズムも時代遅れになり、トラバーサルでトポロジー形式が明らかになります。 私のコンパイラでは、 グラフを通る最長ルート であり、エビ を含む順序が生成されます。 は居住範囲 であり、レジスターの剛性とショップがはるかに少なくなります。
まさにここに私のコンパイラからの真のDAG.
クラシック ブロック命令の置換 - ピース 1: アニメーション
IR の意向により、会議コードを生成するまでには時間がかかりました。 先ほど、私のアルゴリズムはアニメーションの配置と歩調を合わせていたと述べました。これは珍しく、無関係に聞こえるかもしれませんが、最初にアニメーションの配置を指定してから、コンパイラのように動作するように長くする方が簡単なようです。
私が話しているアニメーションは、以前はファミコンのゲームでした。 その配置では、フレームごとに最近のタイルセットをビデオ メモリにロードし、古いフレームのタイルセットを上書きすることによって、アニメーションも実行されます。
ほとんどのゲームは、マッパーと呼ばれるカートリッジに特定のハードウェアを使用することを発明しました。 それにもかかわらず、私の場合、そのハードウェアの断片を費やすつもりのないゲームを作っていました。 代わりに、センターマンとして CPU の消費するバイトを再現する必要がありました。 バイトを再現する明白な方法論は、ループ (C からの「memcpy」を仲介する) を使用することですが、これは緩すぎることが判明しました。 反復ごとに、インクリメント、比較、ループへの分岐に非常に重要な時間が費やされていました。
最も生産的な方法論一見すると、ループを展開した場合、以前は機能していましたが、一度ではありませんでしたが、完全に機能していました。 たとえば、バイト シーケンス (10、20、30) をビデオ メモリに再生したい場合は、次のようなミーティング コードを書くこともできます:
コード生成の最初のステップは、IR を閉じて防御し、そのノードから完全な発見を行い、すべてのノードをその依存関係の後に配置します。 ここでは、すべてのエッジ システムが同じ方向 (プロット内で下向き) になるように、すべてのノードを束線に配置することに相当します。
生成された会議コードは、この発見を明らかにします.
どうすればこれを発見できますか?
遠いです トポロジカルの支出を制定するためにまっすぐ進むただし、欠点があります。DAG には正当な順序付けのペアがあり、いくつかは他のものよりも優れたコードを生成します
適切な順序付け (効率的なコードを生成するもの) を得るために、トポロジカル ソート アルゴリズムはやや時代遅れですが、ヒューリスティックのペアを使用して修正する必要があるかもしれません。 まず、さまざまなノードの後に発生するノードを制限するために、「偽の」エッジもグラフに追加されます。 2d では、把握アルゴリズムも時代遅れになり、トラバーサルでトポロジー形式が明らかになります。 私のコンパイラでは、 グラフを通る最長ルート であり、エビ を含む順序が生成されます。 は居住範囲 であり、レジスターの剛性とショップがはるかに少なくなります。
クラシック ブロック命令の置換 - ピース 1: アニメーション
IR の意向により、会議コードを生成するまでには時間がかかりました。 先ほど、私のアルゴリズムはアニメーションの配置と歩調を合わせていたと述べました。これは珍しく、無関係に聞こえるかもしれませんが、最初にアニメーションの配置を指定してから、コンパイラのように動作するように長くする方が簡単なようです。
私が話しているアニメーションは、以前はファミコンのゲームでした。 その配置では、フレームごとに最近のタイルセットをビデオ メモリにロードし、古いフレームのタイルセットを上書きすることによって、アニメーションも実行されます。
ほとんどのゲームは、マッパーと呼ばれるカートリッジに特定のハードウェアを使用することを発明しました。 それにもかかわらず、私の場合、そのハードウェアの断片を費やすつもりのないゲームを作っていました。 代わりに、センターマンとして CPU の消費するバイトを再現する必要がありました。バイトを再現する明白な方法論は、ループ (C からの「memcpy」を仲介する) を使用することですが、これは緩すぎることが判明しました。 反復ごとに、インクリメント、比較、ループへの分岐に非常に重要な時間が費やされていました。
最も生産的な方法論一見すると、ループを展開した場合、以前は機能していましたが、一度ではありませんでしたが、完全に機能していました。 たとえば、バイト シーケンス (10、20、30) をビデオ メモリに再生したい場合は、次のようなミーティング コードを書くこともできます:
lda #20 ; 負荷定数: 20 sta PPUDATA ; ビデオ RAM lda #30 に追加します。 負荷定数: 30 sta PPUDATA ; ビデオ RAM lda #40 に追加します。 負荷定数: 40 sta PPUDATA ; ビデオ RAM に追加
さまざまな言葉で、私は使用forループが何であったかを勉強するよりも早くプログラミングのクラスをやめた初心者のようにコードを書くこと。 とはいえ、以前は完全に素早かったので、それが重要な理由でした.おそらくそれを穏やかに自動化する必要があります。 単純な方法は、アップロードされたバイトごとに 1 つのロードと 1 つのストアを生成するスクリプトを書き留めることでしょう。 レジスタが値を保持しているため、(1, 8, 1, 8, 1, 8) のような系列には、最も単純な 2 つのロードが必要です:
ldx #1 ; レジスタ X lda #8 をロードします。 stx PPUDATA ; レジスタ A をロードします。 レジスタ X をビデオ RAM sta PPUDATA に追加します。 ビデオ RAM にレジスタ A を追加 stx PPUDATA sta PPUDATA stx PPUDATA sta PPUDATA
これらの負荷を最適に決定するにはどうすればよいですか?
問題を簡単に解決するために、最初に CPU でこれを解決する方法を示します。
レジスタが 1 つの CPU の場合、アルゴリズムは冗長な負荷を見つけてそれらを取り除くことになります。 冗長負荷ということで、以下の2つ目の負荷のように、停止のない負荷をお勧めします:
lda #10 sta PPUDATA lda #10 // 冗長 sta PPUDATA
冗長な負荷を発見して排除することは、控えめに言っても成立しません。 以前にロードされた価格の重罪助長発見。 以下の疑似コードは、最適には
という重罪を犯します。 アップロードするバイトごとに prev=-1: if prev !=byte: emit_load(byte) prev=byte emit_store()
このアイデアは ほぼ 3 つの「前の」変数を 1 つの変数として使用することで、3 つのレジスターを使用するように拡張できます。 問題を整然と解決するために、構造体またはファイル形式を使用することをお勧めします:
struct cpu_state { int a int x int y }
残念ながらマイナス面があります。 すべてのレジスターを見つけてショップを削除できると仮定しても、負荷を軽減する必要があるかどうかはわかりません。 価格がどのレジスターにも表示されなくなった場合、どのレジスターがそれを穏やかにロードする必要があるでしょうか?これらの決定は、負荷の集合体 (ブルート ドライブ) を持っている可能性があるすべての会議コードを生成し、最も単純な方法で健全な状態を維持することによって、最適に発明することができます。 1つですが、この艦隊は実行不可能になります。 ロードが必要なバイトごとに、おそらく十分に所有できる 3 つの選択肢があります。 組み合わせの選択肢は指数関数的に増加します.
総当り探索木の描画幸いなことに、このブルート ドライブ デバイスは、時代遅れのトリックであれば、軽度に使用できます。 それ検索場所の広さにビックリして、ほとんどの組み合わせを削除する必要があるかもしれません.
会議コードが2つ生成された場合、両方とも同一の観測可能なファセット ストップを持ち、よりコストの高い方も剪定されます。 ファセット効果により、同一のバイト シーケンスをアップロードし、同一のレジスタ状態で停止することをお勧めします。 説明のために、次の 2 つのコードは同等です:
lda #1 sta PPUDATA ldx #2 stx PPUDATA ldy #3 sty PPUDATA sta PPUDATA
ldy #1 sty PPUDATA ldx #2 stx PPUDATA ldy #3 sty PPUDATA lda #1 sta PPUDATA 
毎回アップロードシーケンス (1、2、3、1) であり、両方ともレジスタを (A=1、X=2、Y=3) のままにします。 ただし、2 つ目はさらに負荷がかかるため、プルーニングも必要になる可能性があります。コードも。 完全なコード シーケンスを生成し、それらを比較する代わりに、命令ごとにコードを生成し、すべてのステップで評価する必要があります
これを実現するために、アルゴリズムは組み合わせを幅優先で処理します。 最初に最初のバイトの完全な組み合わせを生成し、一部を削除します。次に、2 番目のバイトの完全な組み合わせを生成し、一部を削除し、他の多くの要素を削除します。 プルーニングは、すべての集計の「cpu_state」(以前に起動された構造体) によってインデックス付けされたハッシュ デスクに結果を格納することによって実行されます。
効率を高めるために、剪定のさらなる工夫は時代遅れに見えるでしょう。 すべての反復ステップで最も便利な日付と同じくらい多くの集計を見つけることに成功した場合、3 つまたは余分な負荷で遅いさまざまな組み合わせを取り除くことができます。 これにより、任意の「cpu_state」から別の負荷に変換するには 3 回の負荷で十分であるため、最終的な結果の最適性が維持されます。
これらすべてをまとめると、3 レジスタ CPU で負荷を排除するアルゴリズムは次のようになります。
アップロードするバイトごとに prev_hash_map.determined(): next_lowest_cost=INFINITY next_hash_map.determined() ごとにAggregate in prev_hash_map: if aggregate.trace>=prev_lowest_cost + 3: // プルーニングの 2 次元の工夫が各レジスタ (A、X、Y) ごとに継続: new_combination=aggregate if new_combination.cpu_state.register !=byte: new_combination.code.append_load () new_combination.trace +=1 new_combination.cpu_state.register=byte if aggregate.trace (真のコードをのぞき見することに勝った場合に備えて、C++ 実装をセットアップしました ここ と ここ。)
私はこのアニメーションの仕事のためにカウボーイビバップのイントロをファミコンのROMに変換しました。 GIF は低いフレームレートで実行されます。 クラシック ブロック命令の置換 - ピース 2: コンパイラ
わかった、わかった。 . . アニメーション用のコードを生成できます。 ここにコンパイラに関する社説があります、モラル?
さて、これを提案してください:
このアルゴリズムで一連のバイトを処理する代わりに、先ほど作成した一連のIR操作
これは先ほどのIR操作の順番です。 各ノードのプレースホルダーとして変数名 (Q、R、S、T) を追加しました:
Q=名前 fn R=Q ^ 5 S=R & 3 T=R - S return T
バイトのシーケンスに対して行った評価、操作ごとに組み合わせを生成し、最悪のものを取り除くことができますが、2 つあるにもかかわらず広範な違い:
さまざまな IR 操作により、さまざまな会議の指示が生成されます (もはや重罪ロードではありません)。 「cpu_state」構造体も変数名を検出します。
これをディスカバーに打ち込んで、アルゴリズムが注文されたIR。 アルゴリズムは、最初の操作の組み合わせを作成することから始まります:
"name fn"
、最も単純なものがあります:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?)
その後、XOR 演算 (^)。 これを行う単一の会議命令があります: 「EOR」、それにもかかわらず、XOR は交換可能であるため、2 つのコードの組み合わせが存在します:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor:lda#5; レジスタ A eor Q に 5 をロードします。 レジスタ A と変数 Q sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R, X=?, Y=?)
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor: eor #5 ; レジスタ A と変数 5 sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R, X=?, Y=?)
2 番目の集約は、Q 変数のロード命令を必要としなくなります。これは、既にレジスター内にロードされているためです。 これらのコード シーケンスはどちらも同一のファセット ストップを持っているため、最初の集計は削除されたように見えます。
さて、AND演算(&)。 AND 命令がありますが、SAX と呼ばれる不正な命令もあります。これは、レジスタ A とレジスタ X のビットごとの AND を開始し、停止結果を格納します。 これは 4 つの組み合わせで終わるので、枝刈りされたものを派生させない 2 つを最も簡単に示します:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor: eor #5 ; レジスタ A と変数 5 sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R、X=?、Y=?) です。 および: および #3 ; AND レジスタ A と 5 sta S ; 停止結果を変数 S に格納します。 cpu_state は (A=S, X=?, Y=?)
幸いなことに、このブルート ドライブ デバイスは、時代遅れのトリックであれば、軽度に使用できます。 それ検索場所の広さにビックリして、ほとんどの組み合わせを削除する必要があるかもしれません.
会議コードが2つ生成された場合、両方とも同一の観測可能なファセット ストップを持ち、よりコストの高い方も剪定されます。 ファセット効果により、同一のバイト シーケンスをアップロードし、同一のレジスタ状態で停止することをお勧めします。 説明のために、次の 2 つのコードは同等です:
lda #1 sta PPUDATA ldx #2 stx PPUDATA ldy #3 sty PPUDATA sta PPUDATA ldy #1 sty PPUDATA ldx #2 stx PPUDATA ldy #3 sty PPUDATA lda #1 sta PPUDATA
毎回アップロードシーケンス (1、2、3、1) であり、両方ともレジスタを (A=1、X=2、Y=3) のままにします。 ただし、2 つ目はさらに負荷がかかるため、プルーニングも必要になる可能性があります。コードも。 完全なコード シーケンスを生成し、それらを比較する代わりに、命令ごとにコードを生成し、すべてのステップで評価する必要があります
これを実現するために、アルゴリズムは組み合わせを幅優先で処理します。 最初に最初のバイトの完全な組み合わせを生成し、一部を削除します。次に、2 番目のバイトの完全な組み合わせを生成し、一部を削除し、他の多くの要素を削除します。 プルーニングは、すべての集計の「cpu_state」(以前に起動された構造体) によってインデックス付けされたハッシュ デスクに結果を格納することによって実行されます。
効率を高めるために、剪定のさらなる工夫は時代遅れに見えるでしょう。 すべての反復ステップで最も便利な日付と同じくらい多くの集計を見つけることに成功した場合、3 つまたは余分な負荷で遅いさまざまな組み合わせを取り除くことができます。 これにより、任意の「cpu_state」から別の負荷に変換するには 3 回の負荷で十分であるため、最終的な結果の最適性が維持されます。
これらすべてをまとめると、3 レジスタ CPU で負荷を排除するアルゴリズムは次のようになります。
アップロードするバイトごとに prev_hash_map.determined(): next_lowest_cost=INFINITY next_hash_map.determined() ごとにAggregate in prev_hash_map: if aggregate.trace>=prev_lowest_cost + 3: // プルーニングの 2 次元の工夫が各レジスタ (A、X、Y) ごとに継続: new_combination=aggregate if new_combination.cpu_state.register !=byte: new_combination.code.append_load () new_combination.trace +=1 new_combination.cpu_state.register=byte if aggregate.trace (真のコードをのぞき見することに勝った場合に備えて、C++ 実装をセットアップしました ここ と ここ。)
私はこのアニメーションの仕事のためにカウボーイビバップのイントロをファミコンのROMに変換しました。 GIF は低いフレームレートで実行されます。 クラシック ブロック命令の置換 - ピース 2: コンパイラ
わかった、わかった。 . . アニメーション用のコードを生成できます。 ここにコンパイラに関する社説があります、モラル?
さて、これを提案してください:
このアルゴリズムで一連のバイトを処理する代わりに、先ほど作成した一連のIR操作
これは先ほどのIR操作の順番です。 各ノードのプレースホルダーとして変数名 (Q、R、S、T) を追加しました:
Q=名前 fn R=Q ^ 5 S=R & 3 T=R - S return T
バイトのシーケンスに対して行った評価、操作ごとに組み合わせを生成し、最悪のものを取り除くことができますが、2 つあるにもかかわらず広範な違い:
さまざまな IR 操作により、さまざまな会議の指示が生成されます (もはや重罪ロードではありません)。 「cpu_state」構造体も変数名を検出します。
これをディスカバーに打ち込んで、アルゴリズムが注文されたIR。 アルゴリズムは、最初の操作の組み合わせを作成することから始まります:
"name fn"
、最も単純なものがあります:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?)
その後、XOR 演算 (^)。 これを行う単一の会議命令があります: 「EOR」、それにもかかわらず、XOR は交換可能であるため、2 つのコードの組み合わせが存在します:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor:lda#5; レジスタ A eor Q に 5 をロードします。 レジスタ A と変数 Q sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R, X=?, Y=?)
(真のコードをのぞき見することに勝った場合に備えて、C++ 実装をセットアップしました ここ と ここ。)
私はこのアニメーションの仕事のためにカウボーイビバップのイントロをファミコンのROMに変換しました。 GIF は低いフレームレートで実行されます。クラシック ブロック命令の置換 - ピース 2: コンパイラ
わかった、わかった。 . . アニメーション用のコードを生成できます。 ここにコンパイラに関する社説があります、モラル?
さて、これを提案してください: このアルゴリズムで一連のバイトを処理する代わりに、先ほど作成した一連のIR操作
これは先ほどのIR操作の順番です。 各ノードのプレースホルダーとして変数名 (Q、R、S、T) を追加しました:
Q=名前 fn R=Q ^ 5 S=R & 3 T=R - S return T
バイトのシーケンスに対して行った評価、操作ごとに組み合わせを生成し、最悪のものを取り除くことができますが、2 つあるにもかかわらず広範な違い:
さまざまな IR 操作により、さまざまな会議の指示が生成されます (もはや重罪ロードではありません)。 「cpu_state」構造体も変数名を検出します。
これをディスカバーに打ち込んで、アルゴリズムが注文されたIR。 アルゴリズムは、最初の操作の組み合わせを作成することから始まります:
"name fn"
、最も単純なものがあります:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?)
その後、XOR 演算 (^)。 これを行う単一の会議命令があります: 「EOR」、それにもかかわらず、XOR は交換可能であるため、2 つのコードの組み合わせが存在します:
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor:lda#5; レジスタ A eor Q に 5 をロードします。 レジスタ A と変数 Q sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R, X=?, Y=?)
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor: eor #5 ; レジスタ A と変数 5 sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R、X=?、Y=?) です。 および: ldx #3 ; レジスタ X sax S に 3 をロードします。 レジスタ A と X の AND を変数 S に格納します。 cpu_state は (A=R, X=3, Y=?)
; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor: eor #5 ; レジスタ A と変数 5 sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R、X=?、Y=?) です。 および: および #3 ; AND レジスタ A と 5 sta S ; 停止結果を変数 S に格納します。 cpu_state は (A=S、X=?、Y=?) です。 減算: lda R ; R をレジスタ A にロードする sec sbc S ; レジスタ A sta T から S を引きます。 停止結果を変数 T に格納します。 cpu_state は (A=T, X=?, Y=?) ; 名前 fn: jsr fn ; 機能に sta Q という名前を付けます。 停止結果を変数 Q に格納します。 cpu_state は現在 (A=Q, X=?, Y=?) です。 xor: eor #5 ; レジスタ A と変数 5 sta R の XOR ; 停止結果を変数 R に格納します。 cpu_state は (A=R、X=?、Y=?) です。 および: ldx #3 ; レジスタ X sax S に 3 をロードします。 レジスタ A と X の AND を変数 S に格納します。 cpu_state は (A=R、X=3、Y=?) です。 減算: 秒 sbc S ; レジスタ A sta T から S を引きます。 ストップを保存する 変数 T への結果; cpu_state は (A=T, X=3, Y=?)
最後に、返品操作 、単一の RTS 命令で実装されます。 これは非常に簡単なので、コード例で例として説明するつもりはありません。 古い 2 つの組み合わせのストップに追加された「RTS」を想像してみてください。
終わる
として組み合わせが高速になるとすぐに、アルゴリズムは最もコストの低いものを選択します。 この場合、必要な命令が 1 つ少ないため、SAX 命令を AND の別の命令として使うのは、はるかに人です。
次の異なるこのように見えますが、もう実行されていませんが:
jsr fn sta Q eor #5 sta R ldx #3 sax S sec sbc S sta T rts
欠点は、ストア命令をロードするものがないため、膨大な数のストア命令を選択する必要がないことです。 これを修復するためのさらなるトレッキングは、ロードされていないショップを識別するスピードです。 これは最終コード内で終了します:
jsr fneor #5 ldx #3 sax S sec sbc S rts
特定のコンパイラー内では、コード ジェネレーターはショップについて少し賢く、ほとんどの場合、組み合わせを生成する際にそれらが必要かどうかを推定する可能性があります。 ストアの存在が集合体のトレースに影響するため、ここは深刻です。
クラシック ブロック命令の置換 - ピース 3: 継続
証明されているように、IR 操作をコンパイルすると、組み合わせの束。 反対に、IR 操作を会議の組み合わせに変換するコードを作成する場合はいつでも、繰り返しの単純なコーディング作業になる可能性があります。
欠点は、組み合わせの選択に関するものです。 すべての IR 操作には、おそらくそれを有効にできる正当な命令のペアが存在するように見えます。これらの命令のすべてに、追加の組み合わせが続くフィル バリアントが含まれている可能性があります。 これらを掛け合わせると (通信操作の場合は 2 倍になります)、IR 操作ごとに非常に多くの組み合わせを選択できるようになる可能性があります。
これらの詳細を抽象化し、操作を実施するためのビルディング ブロックを作成することは途方もないことです。 幸いなことに、継続を費やすことで解決策を学びました.
すべてのステップ (または「ビルディング ブロック」) は、 連続通過ファッション。 返す代わりに、これらの機能は、集計を生成するすべての命令で継続に名前を付けます。 説明のために、3 つの組み合わせを生成するために、継続は 3 つのさまざまな命令で 3 回参照されるように見えます。
レジスタ A に価格をロードするための機能が、疑似コードでどのようにのぞき見されるかを次に示します:
load_A(desired_value, cpu_state, continuation) if cpu_state.A==desired_value continuation( cpu_state, NOP) そうでなければ cpu_state.X==希望する値の継続(cpu_state, TXA) そうでなければ cpu_state.Y==希望する値の継続(cpu_state, TYA) でなければ継続(cpu_state, LDA) 継続(cpu_state, LAX)
関連付けられた価格がすでにすべての日付で最も高い場合レジスタ: 何も起こらない (NOP) か、レジスタ間の転送が発生する (TXA、TYA)。 それ以外の場合、2 つの組み合わせが発生します。1 つは LDA 命令の消費、もう 1 つは LAX 命令の消費です。
このように機能を書く目的は、それらもマイルドにするためです。 ほんの少しの配管で、合計すると ドメイン固有の言語:
invent(load_A(L), load_X(R), instruction(SAX))
このような 1 行で、おそらく正直なところ、50 のさまざまなミーティング シーケンスをカバーできる位置にあり、すべてがさまざまな組み合わせの命令とアドレッシング モードを処理します。 手で完全な確率を数え上げるよりも、簡単に書き留めることが重要です.
フロートで時計を奪う - ピース 1: 基本
私が説明したアルゴリズムが現在のブロックに関連付けられていると仮定しても、調整ドリフト (分岐、ループなど) も処理する必要があるかもしれません。 支店の運営は、もはや特定する必要はありません。あらゆる種類の会議の組み合わせが生成されます。
説明するために、このレギュレート ドリフト グラフ (四角形は現在のブロック) を見てください:
このグラフの現在のブロックにはそれぞれ、さまざまなラベル名 (L1、L2、L3 など) が割り当てられているようです。
現在のすべてのブロック のコードを一度に 1 つずつ生成します
、指示がこれらのラベルを活気づけたので、調整ドリフトを強制します。 現在のすべてのブロックにコードが生成されるとすぐに、結果を連結できます:
L1: ; (余分なコードはここでトレッキングします) beq L1 ; 分岐命令 bne L2 L2: ; (追加のコードはここでトレッキングします) jmp L4 ; リープ命令 L3: ; (追加のコードはここでトレックします) beq L1 bne L4 L4: ; (余分なコードはここにトレッキングします) rts
![]()
これで実装され、コンパイラが動作するようになりました。
時計を奪うon Float - ピース 2: 最適化
上記の手法は機能しますが、それにもかかわらず、現在のブロック境界を越えた適切なコードはもはや評価されません。 欠点は、ブロック間でレジスタ状態の情報を共有せずに、現在のブロックを 1 つずつコンパイルすることにあります。 現在のブロックの 1 つが古いブロックからの価格を必要とする場合、冗長な場合でもロード命令を発行するように見えます。のぞきます:
L0: ldx # 0 ; レジスタ X に 0 stx I をロードします。 ストア変数 I L1: ldx I ; 変数 I inx をロードします。 レジスタ X stx I をインクリメントします。 変数 I bne L1 を格納します。 レジスタ X がゼロでなくなった場合に分岐します
![]()
にもかかわらず、ループ がおそらくマイルド にならなければならない方法です:
ldx #0 ; L1: inx ; レジスタ X に 0 をロードします。 レジスタ X bne L1 をインクリメントします。 レジスタ X がゼロでなくなったら分岐
![]()
ループには冗長な負荷がありません。
これを解決するには、現在のブロックの最終的な「cpu_state」を共有する必要があります。 これは、現在のすべてのブロックの結果を後続のブロックに渡すことによって実行されます。
時代遅れの工夫: 現在のブロックごとに個別にコードを生成します.最近の工夫: 現在のブロックを生成することからの出力状態を、現在のブロックへの入力状態として費やす後続。
これは、すべてのノードが 2 回処理される反復ジョブです。 ノードが処理されると、これは後続の出力を入力として渡すために処理されるように、その後続ノードに依存しているように見えます。 プット ポイントに到達しない限り、これを繰り返すことができます。
ループの例に戻ると、最初の反復では非効率なコードが高く評価されますが、その後の反復では (L3) から (L1 )。 レジスタ X にプリロードされた変数 "I" が表示されるとすぐに、最適なコードが明らかになります。 -costing 結果に沿って、反復ごとにより厳密に導き出します。 これは、シミュレーテッド アニーリングに似ています。
フロートで時計を奪う - ピース 3: Some Loads Required
上記の方法で改善されたコードが生成されたとしても、最低コストの組み合わせを連結することによって、実際に機能するプログラムを導出する必要はもはやありません。 欠点は、すべてのコード集合体が特定のレジスタ値を入力として期待するようになったことですが、1 つの集合体の出力が他の集合体の入力に対応していない可能性があります。 ループ変数を Y レジスターの使用量で初期化するにもかかわらず、X レジスターの使用量をインクリメントするような無意味な結果を導き出す可能性があります:
; このコードは正しくコンパイルされていません! L0: ldy #0 ; Y を繰り返し回数として出力します。 注: sty の指示は、「使用されていない店舗を閉じることを守る」トレックによって削除されていました。 L1: ; X を反復回数として入力 inx bne L1
![]()
Toこれを修復するには、現在のすべてのブロックの入力状態で、前のブロックの出力状態を調べる必要があります。 不一致がある場合は、それを適切にするために負荷も挿入されます。
負荷を挿入すると、コードは次のようになります。 L0: ldy #0 sty I ; このストアは削除されなくなりました。 L1_entry: ldx I ; 挿入された荷重 L1: inx bne L1
![]()
どれが機能するか、とはいえもはや至高ではありません
Robフロートの時計 - ピース 4: PBQP
上記の問題入力/出力状態間の互換性を考慮せずに、現在のブロックごとに最も単純な最小コストの組み合わせを選択することによってプロンプトが表示されます。 理想的には、挿入された負荷の価格内で構成します.
そうすることは最適化状況であり、 分割されたブール二次領域 (PBQP)。 PBQP の状況を解決できれば、現在のブロックの最適な組み合わせを守ることができます.存在するのはほとんどの人にとって読むのが面倒です:
![]()
画像ソース
私のアドバイスは 方程式に苦労するのとは異なり、グラフのフレーズで視覚的に熟考することで PBQP を学ぶ方が簡単です。
すべてのノードに決定の有限リストがあり、それらの選択のすべてにトレースがあるグラフを想像してください。 PBQP の目標は、ノードごとに 1 つの異なる防御を行い、トレース全体を最小限に抑えることです。 解決策は、瀬戸際の頂点で行われた選択に合わせて、グラフ内のすべてのエッジに対してさらにトレースが発生することです。
下の画像はそのようなグラフです。 それぞれのノードには 2 つの異なる色があり、民俗色にはそれに続く痕跡があります。 2 色を選択する場合は、瀬戸際の価格も同様に支払う必要があります。 ここでは、選択した 2 つの色をキーとして使用して、すべての端に奇妙な机で価格を上げようとすることによって実行されます。
以下、いくつかの選択を行い、取り消し線を引いています私が持っていなかったすべての問題。 合計トレースは、考慮された数の合計であり、3 + 1 + 7 + 3 + 6 + 7=27 です。PBQP の目標は、この合計を最小化する選択を探すことです。
A swiftly アルゴリズム は、PBQP の問題をデバイス最適に解決するために存在します。 アルゴリズムはグラフで機能し、ノードが残っていない限りノードを削減および混合します。 停止すると、アルゴリズムは逆方向に動作し、どの選択を発明するかを熟考するために必要な手順を費やします。以前はこのビジュアライゼーションにスーツを完全に記述していました:
グラフはレギュレートドリフトのグラフです
ノードは主要なブロックです。 ノードの選択は、以前に生成されたコードの組み合わせです。 . しきい値コストは、2 つのコードの組み合わせが与えられた場合に、挿入する必要がある可能性のある余分な負荷の選択です。
PBQP ソルバーを強制し、組み合わせにそれを費やすことで、停止結果はデバイス最適化コードを高く評価します。 先程ご尽力頂いた打ち合わせループは
ldx #0 L1: inx bne L1![]()
どちらが最適か
複雑さとパフォーマンス )
アンカバーの設定はO(N
2
)、それにもかかわらず、無視できるランタイム トレースがあります。 Discover では、典型的なコードは非常に簡単に発見できます。
SSA を終了する複雑さが何であるかはわかりませんが、O(N2
))。 いずれにせよ、実行時のトレースは無視できます
コードを生成する現在のブロックは、過大な定数乗数を使用して、IR 操作の選択に関して O(N) です。 これにはインパクトのあるランタイム トレースがあり、ボトルネックはハッシュ デスクです。 これを軽減するため、塗りつぶし作成のハッシュ デスクをすばやく使用します。
PBQP の状況は、エッジの選択に対して O(N) で解決され、O(N3) 決定の選択に関連します。 これ は 拡張的なランタイム トレースになる可能性があります。決定の選択がエビに保たれるように延長されます。 PBQP ソルバーは、重要な場合は SIMD の費用を非常にうまく実装できると思いますが、その必要性はまだわかりません.
現在のブロックを繰り返し生成することは、不快な複雑さを伴います、それにもかかわらず、シミュレートされたアニーリングのおかげで、O(N) になります。 この段階での重大な痕跡は、会議の組み合わせを見つけることによって引き起こされるキャッシュ ミスです。 道徳的な方法論では、これらのキャッシュ ミスの膨大な選択も避けられます。
結論
このコード ジェネレーターは、私にとっては十分に機能しました。 それのシステムが毛むくじゃらだとしても、継続は最近の操作を簡単に追加できるように発明し、コード内の重要な技術的負債を一目瞭然に実行します。 生成された会議コードは適切でしたが、理想的ではなくなりました。 前述のように、私が実行したベンチマークではさまざまなコンパイラを打ち負かしましたが、他のコンパイラもおそらく私のものを打ち負かす可能性があることは明らかです。 私の最も単純な目標は、適切なコンパイラを高く評価することであり、もはや健全ではありません。
ところから始めて申し上げたように、私は各種コード生成方法について深い知識を持っていません。 私が検討したコード生成に関する論文のほとんどは、ILP / SAT ソルバーの評価を検討しており、長い機能をまとめるための時間ではないにしても、数分を擁護しています。 さらに、最近のすべての問題は RISC アーキテクチャ用に設計されており、これは 6502 とはまったく異なります。 O(N)であり、補償されていました。 あなたは重罪を犯し、「すべての問題をハッシュして100万回高速化する」などの一見したアドバイスを実行する必要はありません。