スーパースカラースタックプロセッサ:詳細





OoOとスタックドマシンのフロントエンドを備えたスーパースカラープロセッサの概念を探る一連の記事の続き。

この記事のトピックは、ビュー内での関数の呼び出しです。



前の記事:

1-直線作品の説明

2-関数呼び出し、レジスターの保存



前の記事で関数呼び出しの外側に注目すると、これらすべてが内部で実行される方法を見失いました。 著者は自分自身をハードウェアの専門家とは考えていませんが、それでも問題を予測しています。



関数から戻った後、プロセッサを以前と同じ状態にしたいと述べました。 そのため、レジスタの状態を維持することに専念していました。 しかし、プロセッサの状態はレジスタだけではありません。 モップの列、コンベアの状態も......



関数呼び出しの時点で次のような状況を想像してください。



その結果、新しい関数の実行を開始することはできません





すべての引数を計算する呼び出しの前に待機する一般化された命令によって関数呼び出しを宣言したときに、説明された問題を回避するために特定の手段を講じたように見えるかもしれません。



ただし、データに依存しない場合、デコーダーは関数を呼び出した後にコードを処理する時間があるという事実に問題があります。 例:
int i, j, k; … i = foo (i + 1); j += k; bar (i, j);
      
      





fooから戻った後、現在のタスクのコンテキストでjが計算および保存(および復元)されたという事実や、j + = kのモップが保存されたという事実に依存することはできません。 彼女に計算する時間がなかったら



関数呼び出しを見ると、デコーダはこの関数から戻るまで動作を停止するとします。 次に、この状況はどうですか:

  int i, j, k; … bar (foo (i + 1), j += k);
      
      





1つのデコーダーでは十分ではありません。 関数から戻った後、デコードを開始し、さらにコードを再度実行するだけでよいようです。



しかし、結局のところ、基本的に、スーパースカラリティによって引き起こされるこれらの問題は、私たちのアーキテクチャに固有のものではありません。車輪を再発明する必要はないかもしれません。



さて 、IntelのSandy Bridgeを見てみましょう( Tasit Murka、別名Felidに感謝)

ここだけでなく



サンディブリッジ。





おそらくSandy Bridgeフロントエンド、





関数呼び出しはどのように発生しますか?

次の例を見てみましょう。

 int i, j, k, l, m, n; ... i = 1 + foo (1 + bar (1 + biz (1 + baz (1 + j) + k) + l) + m) + n;
      
      





このコードはすべて1つの32バイトチャンクに収まり、コード生成の順序(オプティマイザーをオフにした場合)はテキストの順序に対応すると想定します。 L0mキャッシュに分類されるセクションのインスタンスに応じてコードをマークしましょう。

 int i, j, k, l, m, n; ... 1> i = 1 + 4> foo ( 1> 1 + 3> bar ( 1> 1 + 2> biz ( 1> 1 + 1> baz (1 + j) 2> + k) 3> + l) 4> + m) 5> + n;
      
      



このスニペットは、おそらく使用可能な256行のうち5行を占有します。

当然のことながら、コンパイラーは小さな関数のインライン化に苦労しています。



結論



そこで、非常に洗練されたマイクロアーキテクチャの1つで関数がどのように呼び出されるのかを調べましたが、これからどのような利点が得られるのでしょうか。



客観的なポイントを強調します。



これはすべて、スタックフロントエンドを備えたアーキテクチャで機能すると思われるかもしれません。 しかし、ニュアンスがあります。

例で説明しましょう。 アッカーマン関数は次のとおりです。

 int a(int m, int n) { if (m == 0) return n + 1; if (n == 0) return a(m - 1, 1); return a(m - 1, a(m, n - 1)); }
      
      



単純に見えますが、再帰の驚異を示しています。 次のグラフは、呼び出しa(3、5)のネストの深さのダイナミクスを示しています。xはステップ番号、yは呼び出しの深さです。



関数呼び出しを任意の数のパラメーターを持つ一般化された命令と見なすことにしたため、m * n!= 0の場合、最初の引数(m-1)はモップのスタックに残り、2番目の引数は計算されます:a(m、n- 1)。 さて、最初の引数に計算する時間があり、その値がレジスタスタックで呼び出されたときに保存される場合。 ただし、最初の引数の式が、2番目の引数の子呼び出しの引数よりも長く評価される場合があります。 そして、私たちにはかなりの量の故障したモップがハングアップします。

親呼び出しモップは、子呼び出しが完了するまで待機します。 SandyBrigeでは、通話の深さは数万台に簡単に達することができます。たとえば、モップはそれほど多くありません。



問題の本質は、呼び出しを一般化された命令として認識し、それによってプログラムを一般化された表現として認識したことです。 また、再帰によるこの式のツリーは、任意の高さにすることができます。 一方、式スタックは制限されています。 また、スタック要素はモップのインデックスであり、その数も限られています。



しかし、私たちはそれほど簡単にgivingめることに慣れていないので、プランBが出てきます。



プランb



スーパースカラーレジスタプロセッサにはこのような問題はありません。 モップ間の接続の識別は、後で行われます-レジスタの名前を変更する段階で。

私たちの国では、これらの接続はモップのインデックスのスタックに保存されます。



たぶん、スタックの状態を保存しますか? モップのインデックスを維持するだけでは不十分であることがわかりました。モップ自体にも注意が必要です。 割り当てられているかどうかにかかわらず、スタック全体でモップの保存を整理するのは自然なことのように思えますが、質問は完全に別です。



そしてここで、レジスタを保存しようとしたときに遭遇したのと同じ問題に直面しています。 つまり、単一の(すべての機能の)モップの識別で:



これらの問題を解決する方法は同じです:



一見したところ、メモリを介してデコードされたモップを駆動する必要性は途方もないように見えます。 しかし、 カテコールアミンが燃え尽きると、災害の規模がそれほど大きくないことが明らかになります。

  1. 最新のプロセッサでは、パラメータ(少なくともそれらの一部)はレジスタを介して送信されますが、再帰呼び出しでは、スタックを除いて、パラメータを保存する場所がありません。

    • リターンアドレスとフレームポインタもスタックに保存されます
    • 揮発性レジスタは、呼び出し元によってスタックに格納されます
    • 不揮発性レジスタもスタックに格納されますが、呼び出し側によって
  2. 'a(m-1、a(m、n-1))'のような式は、a(m、n-1)に等しい一時変数を(明示的または間接的に)導入することにより、コンパイラーが分割できます。 これにより、保存する必要があるモップの数が減ります。
  3. 関数呼び出しである無条件遷移の時点までにすべての失敗した条件分岐は、すでに投げました
  4. 次のチャレンジのすべてのモップは、SandyBridgeと同様の手法を使用して、破棄するか、単にロードしないこともできます。
  5. しかし、そのままにしておくことができます。関数から戻ると、ボーナスを受け取り、実行の準備ができているか、すでに部分的に実行されています(データに依存しない)
  6. 最も「経済的な」バージョンでは、1つのシリアル化されたコールモップのみがスタックに追加されます。これは、たとえば、フレームバッファアドレスを送信する場合と大差ありません。
  7. (de)mopsのシリアル化は、コストのかかる操作のようには見えません。さらに、関数からの現在の戻りをブロックすることなく、事前にバックグラウンドで実行できます。
  8. ロードとともにモップをスタックに保存することは、l0mキャッシュの代替となるはずです。
  9. 直列化されたモップのサイズは大きすぎません。SandyBridgeの場合、初期モップサイズ最大147ビット、圧縮形式で見積もられます-85ビット(およびすべてのストライプのx87、SSE、AVXがあります)
  10. プロセッサの技術的機能が外部からアクセス可能になり、技術的な秘密が侵害される可能性があるという事実は、著者を怖がらせません。 最後に、プロセッサにこのデータをワンタイムパッドでxorさせます。




次は何ですか



これまでのところ、スタックされたすべてのマシンの弱点、つまり過剰なメモリアクセスに注意を払っていません。

したがって、 次の記事でそれらに対処します。



All Articles