同時マルチスレッディングの仕組み
(blog.codingconfessions.com)- SMTは、1つのCPUコアが同じサイクルで複数スレッドの命令を発行し、命令レベル並列性だけでは埋まらない実行資源の空きをさらに活用するための手法
- IntelのHyper-Threadingはコアあたり2スレッドの実装で、アーキテクチャ状態を複製することで、OSからは1つの物理コアが2つの論理プロセッサのように見える
- 実際の実装では、すべての資源を単純に2倍にするのではなく、命令ポインタ・ITLB・RATは複製し、trace cache・TLBは共有し、uop queue・物理レジスタ・reorder bufferは均等分割する
- 性能向上はワークロードによって異なり、2つのスレッドがキャッシュを競合的に使うと遅くなることがある一方、データをやり取りする協調的なスレッドは共有キャッシュのおかげで高速化することがある
- 共有資源と投機実行はセキュリティ脆弱性につながる可能性があるため、セキュリティに敏感な環境や、最高性能・最小レイテンシが必要なワークロードではSMT無効化が現実的な選択肢となる
SMTが必要な理由
- 最新のプロセッサは数百個のレジスタ、複数のload/storeユニット、算術ユニットを備えており、これらを活用するためにパイプライン化、superscalar構造、アウトオブオーダー実行といった命令レベル並列性の技法を用いる
- パイプラインは命令実行を複数段階に分け、各サイクルごとに命令を次の段階へ渡していくもので、深さ5のパイプラインでは5サイクル目以降に最大5命令が同時進行できる
- superscalarプロセッサは1サイクルに複数命令を発行でき、最近のIntel Core i7プロセッサでは1サイクルに4命令を発行できる
- 実際のプログラムでは十分に独立した命令を見つけるのが難しく、実行資源が遊ぶ時間が生じる
- 水平的な無駄は、1つのスレッド内でissue widthを埋めるだけの独立命令を見つけられないときに発生する
- 垂直的な無駄は、後続命令が現在実行中の命令に依存しており、1サイクル中に1命令も発行できないときに生じる
- 従来のマルチスレッディングは、各サイクルで1つのスレッドの命令だけを発行し、次のサイクルで別のスレッドに切り替えることで垂直的な無駄を減らせるが、水平的な無駄とコンテキスト切り替えのオーバーヘッドは残る
- SMTはコンテキスト切り替えなしに同じサイクルで複数スレッドの命令を発行し、実行資源をより高い比率で埋める
- IntelのSMT実装であるHyper-Threadingはコアあたり2スレッドに限定される
Intel方式のSMTの基本構造
- 一般的な非SMTプロセッサは、一度に1つのスレッドの命令しか実行できない
- 各スレッドは、レジスタ値、プログラムカウンタ、制御レジスタなどを含むアーキテクチャ状態を持つ
- 2つのスレッドの命令を同時に実行するには、2つのスレッドの状態を同時に表現できなければならないため、SMT実装ではプロセッサのアーキテクチャ状態を複製する
- この複製により、1つの物理プロセッサがOSからは2つの論理プロセッサとして見え、OSはそれぞれにスレッドをスケジューリングできる
- マイクロアーキテクチャレベルのバッファや実行資源は、コスト、消費電力、チップ面積といった要因に応じて複製・共有・分割される
- ここでの議論対象は主にIntelのSMT実装であり、Intelの2002年のホワイトペーパーに基づいている
CPUマイクロアーキテクチャの3つの部分
- プロセッサはプログラマに対して公開インターフェースとしてISAを提供し、ISAには命令セットと、その命令が利用できるレジスタが含まれる
- マイクロアーキテクチャは、同じISAをサポートするプロセッサモデル間でも異なりうる内部実装である
- 最新プロセッサのマイクロアーキテクチャは大きく3つに分かれる
- フロントエンド: 次に実行するプログラム命令を取り出してデコードする命令制御ユニットを含む
- バックエンド: 物理レジスタ、算術ユニット、load/storeユニットなどの実行資源を含み、デコード済み命令に資源を割り当てて実行をスケジューリングする
- retirement unit: 実行済み命令の結果をプロセッサのアーキテクチャ状態へ最終的に反映する
フロントエンドでのSMT
- 命令ポインタは次に取り出す命令のアドレスを追跡する
- SMT対応プロセッサは、2つのプログラムの次命令を独立して追跡するために、2組の命令ポインタを持つ
- trace cacheは最近デコードされた命令traceを保存し、繰り返し実行される命令のデコードコストと実行遅延を減らす
- 2つの論理プロセッサが必要に応じて動的に共有する
- 一方のスレッドがより多くの命令を実行すれば、trace cache内のより多くのエントリを占有できる
- 各エントリは、2つのスレッドの命令を区別するためにスレッド情報でタグ付けされる
- trace cacheへのアクセスは毎サイクル、2つの論理プロセッサ間で仲裁される
- trace cache missが発生すると、フロントエンドはL1 instruction cacheからそのアドレスの命令を探し、L1 instruction cache missなら次の階層キャッシュやメインメモリから取得しなければならない
- L1 instruction cacheは仮想アドレスでデータをキャッシュするが、メインメモリアクセスには物理アドレスが必要となる
- ITLBは最近変換された仮想アドレスを保持し、仮想アドレスを物理アドレスへ変換する
- SMT対応プロセッサでは各論理プロセッサが独自のITLB cacheを持つ
- メインメモリから命令を取得するロジックはfirst come first servedで動作するが、両方の論理プロセッサが前進できるよう、各論理プロセッサに最低1つの要求スロットを予約する
- メインメモリから到着した命令は、デコード前に小さなstreaming bufferへ保存され、SMT対応プロセッサではこのバッファも論理プロセッサごとに複製される
- 命令は取得後、より小さく単純なuopへデコードされる
- uopはCPUフロントエンドとバックエンドの境界となるuop queueに入る
- uop queueは2つの論理プロセッサの間で均等に共有され、この静的分割によって両論理プロセッサが独立に進行できる
バックエンドでのSMT
- バックエンドはuop queueからマイクロ命令を取り出して実行するが、元のプログラム順序に縛られるだけでなく、アウトオブオーダー実行を行う
- 近接したプログラム命令同士は依存関係を持つことが多く、メインメモリ読み出しのような高レイテンシ処理があると、依存する命令まで待機しなければならない
- アウトオブオーダー実行エンジンは、後続命令を本来の順序より先に実行することで資源の無駄を減らす
- allocatorはマイクロ命令に必要な資源を識別し、利用可能性に応じて割り当てる
- 1サイクルでは一方の論理プロセッサのマイクロ命令に資源を割り当て、次のサイクルではもう一方の論理プロセッサへ切り替える
- uop queueに片方の論理プロセッサのマイクロ命令しかない場合や、一方の論理プロセッサが自分の取り分の資源を使い切った場合、allocatorはすべてのサイクルを他方の論理プロセッサに使う
- バックエンドの主要資源は、複製・共有・分割が混在している
- ISAレベルのX86-64は16個の汎用整数レジスタしか持たないが、マイクロアーキテクチャレベルでは数百個の物理整数レジスタと同程度の浮動小数点レジスタを持つ
- SMT対応プロセッサでは物理レジスタは2つの論理プロセッサに均等分割される
- メモリの読み書きに使われるload bufferとstore bufferも2つの論理プロセッサに均等分割される
レジスタリネーミング、スケジューリング、コミット
- アウトオブオーダー実行のために、バックエンドはレジスタリネーミングを行う
- ISAレベルではアーキテクチャレジスタが少ないため、プログラム命令は同じレジスタを複数の独立命令で再利用する
- アウトオブオーダー実行エンジンは元の論理レジスタを物理レジスタのいずれかに置き換え、より早い実行や並列実行を可能にする
- この対応関係はregister alias table、つまりRATに保持される
- 2つの論理プロセッサはそれぞれ独自のアーキテクチャレジスタ集合を持つため、RATもそれぞれ複製される
- register renamingとallocator段階を経た命令は準備キューに入る
- 1つはメモリ読み書き命令用のキューで、もう1つは一般命令用のキューである
- SMT対応コアではこれらのキューは2つの論理プロセッサに均等分割される
- プロセッサは複数のinstruction schedulerを並列に持つ
- 各CPUサイクルごとに、準備キューの一部の命令がschedulerへ送られる
- キューは1サイクルでは一方の論理プロセッサの命令を送り、次のサイクルではもう一方へ切り替える
- schedulerは論理プロセッサを意識せず、必要なオペランドと実行ユニットが準備できたマイクロ命令をただちに実行へ回す
- 公平性のため、scheduler queue内で一方の論理プロセッサが持てるactive entry数には上限がある
- 実行が終わった命令結果はreorder bufferに入る
- 命令はアウトオブオーダーで実行されても、プロセッサのアーキテクチャ状態には元のプログラム順序どおりにコミットされなければならない
- SMT対応コアではreorder bufferは2つの論理プロセッサに均等分割される
- retirement unitは、命令がアーキテクチャ状態へコミット可能かどうかを追跡し、正しいプログラム順序でretireする
- SMT対応コアでは各論理プロセッサのマイクロ命令を交互に処理する
- 一方の論理プロセッサにretireすべきマイクロ命令がなければ、全帯域を他方の論理プロセッサに使う
- 命令retire後にL1 cacheへ書き込む必要がある場合があり、この書き込み選択ロジックも毎サイクル2つの論理プロセッサ間で交互に処理する
メモリサブシステムとキャッシュへの影響
- データ要求の仮想アドレスを物理アドレスへ変換するTLBは、2つの論理プロセッサが必要に応じて動的に共有する
- TLBエントリは、2つの論理プロセッサの項目を区別するためにlogical processor idでタグ付けされる
- 各CPUコアは専用のprivate L1 cacheを持つ
- L2 cacheはマイクロアーキテクチャによってprivateの場合もあれば、コア間で共有される場合もある
- L3 cacheがあればコア間で共有される
- キャッシュは論理プロセッサの存在を認識しない
- L1 cacheと、場合によってはL2 cacheもコア専用であるため、2つの論理プロセッサのデータを必要に応じて一緒に保持する
- 2つのスレッドがキャッシュを激しく使うと、データ競合やevictionが発生して性能が低下することがある
- 2つのスレッドが同じデータ集合で作業する場合、共有キャッシュによって性能が向上することがある
性能とセキュリティにおける選択基準
- SMT対応コアで1スレッドしか実行していなくても、多くのバッファや実行資源は2つの論理プロセッサ間で共有または分割された状態のままとなり、単一スレッドの潜在性能を下げることがある
- 未使用の論理プロセッサではOSがidle loopを実行し、このループも他方の論理プロセッサが最大性能を出すために使える資源を消費しうる
- Intel Coreプロセッサでは、コアで1スレッドだけが実行されるときは資源共有や分割がないように見え、Intelはこれをその世代で導入された改善として扱っている
- 2つのスレッドがSMT対応コアの2つの論理プロセッサで動作する場合は、キャッシュアクセスパターンが性能を左右する
- 2つのスレッドがキャッシュを巡って競合すると、互いのデータをevictionして性能が低下する
- 一方のスレッドが生成したデータをもう一方が消費するような協調的なケースでは、キャッシュデータ共有によって性能が向上する
- 2つのスレッドがキャッシュを競合的に使わなければ、互いの性能を損なわずにCPUコア資源の利用率を高められる
- 絶対的な最大性能が必要なプログラムでは、単一スレッドが全資源を使えるようにSMTを無効化したほうがよいと考える専門家が多い
- SMTにはセキュリティ問題も伴う
- 共有資源と投機実行により、機密データが攻撃者に漏えいする可能性が生まれることがある
- Oracle LinuxとRed Hatの文書は、SMT関連のセキュリティ問題の事例に言及している
- 一般的な助言は、システムでSMTを無効化する方向である
- Intelが次世代プロセッサArrow LakeでHyper-Threadingを廃止する可能性があるという噂もある
1件のコメント
Hacker News のコメント
SMTを非常に単純化して理解すると、スレッドがキャッシュミスで止まっている間も、貴重なALUを忙しくし続けられる、という点が腑に落ちた
昔のノートPCのLPDDRはもっと遅く、コア数も不足していたので、より価値が大きかったのだろうが、最近はスケールしやすい作業よりコアのほうが多い場合も多く、価値をあまり実感しにくい
重要なスレッドと同じコアに作業を配置せず、キャッシュ競合を避けることもあるが、それは単一スレッド性能がボトルネックだと分かっているから
以前、DirectX 12のマルチスレッドレンダリングでEfficient/PerformanceコアとSMTコアをテストしたところ、i7-12700KではP-coreのみの場合とP+SMT、P+E+SMTで、複雑なシーンのレンダリング時間がほぼ同じだった。ただしXbox Series Xでは、SMTにも作業を配置したとき同じテストが少し速かった
SMTは入出力を待っているときや、単純な整数処理をしているときに真価を発揮する。2つのスレッドがどちらもFPUを飽和させられる場合、CPU内部データに所属を示すための追加タグ付けのせいで、たいていSMTのほうが遅くなる
重要なのはキャッシュミスというより、書き込みが完了する間にコアが別の仕事を実行できるようにすること
そのため、うまくスケールしないコードもあれば、ほぼ線形に近い速度向上を得るコードもある
こうしたプロセッサは、単に少し昼寝させておくほうがいいのかもしれない
バス幅、秒間転送数、キューイング、ビット転送・保存あたりの電力は改善されたが、プログラムがキャッシュにないデータを必要とし、予測も外れたなら、結局はRAMレイテンシが問題になる
Intelの次世代Arrow Lake CPUは、Hyper-Threading、つまりSMTを完全に削除する予定だという
性能向上は常にアプリケーションへの依存が大きかったので、単純化するほうがよいのかもしれない
いつ、どこで意味があるのかについての最近の議論はこちら: https://news.ycombinator.com/item?id=39097124
まだ必ずその地点に到達したとは確信していないが、IntelのP/Eコアは似た目標に向けた代替案であり、単一スレッド・低スレッド数の作業が多いデスクトップではかなり合理的だ。アプリケーション最適化でSMTとE-coreの区別を扱わなくてよい価値もありそう
一方AMDは当面、おおむね均質なコアを維持し、SMTを使い続ける計画だ。実際にどの戦略が優れているかはアプリケーションごとに大きく変わるため、単純には判断しにくそうだ
そのため、
std::thread::hardware_concurrency() / 2 - 1、つまりコア数 - 1にスレッド数を制限することにした。std::vectorを扱っているうまく動くときでも改善幅はせいぜい2桁パーセントで、連続した世代でさらに悪かった時期もあったのに、なぜ続けるのか分からない
こういう低レベルのCPU機能がどう動いているのか読むたびに驚かされる
大学で「コンピュータハードウェア入門」くらいの授業を受けたが、実際には「CPU設計入門」と呼ぶべきものだった。論理ゲートで加算器、ラッチ、フリップフロップなどを作り、学期末にはゲートレベルでごく基本的なプロセッサを設計できるようになっていた
しかし、レジスタリネーミングやアウトオブオーダー実行のようなものを思いついて作り出すのは、なかなか想像できない。こういうものもゲートレベルで設計するのだろうか? それとも、使う言語と、ゲートやトランジスタを配置してくれる「コンパイラ」があるのだろうか?
課題はすべてVerilogというハードウェア記述言語で行い、そのおかげでさまざまな要素を抽象化して書けた
SMTについてユーザーがよく抱く大きな誤解の1つは、「本物のコア」1つと、それより劣るコア1つがあると想像するメンタルモデルだ
観測可能なあらゆる面で、2つのスレッドは対等である
結局、性能だけを見るなら、概念的には単一スレッドコアが1.25個ほど、あるいはアプリケーションに応じてその比率だけある、というほうに近い
E-coreを導入した主な理由は、電力使用や性能というより、発熱とダイ面積だと見ている。だから私はいつもE-coreのないチップを買っており、そのほうがよいと思っている
この種の詳細な技術記事はどう検索すればよいのか気になる
まさにこのテーマで探してみたが、予想どおりエンドユーザー向けの技術なので、検索結果は何もきちんと説明していないユーザー向け記事ばかりだった
ただし、提示するURLはたいてい幻覚
別の端末の新しいFirefoxプライベートタブで確認したので、トラッキングやキャッシュを完全に防げるわけではないにしても、かなり妥当な近似だと思う
「SMTが有効なCPUコアでは、多くのバッファや実行リソースを2つの論理プロセッサが共有しなければならないため、SMTコアでスレッドが1つしか実行されていなくても、そのリソースはそのスレッドが使えず、潜在性能が低下する」という説明は、今では事実ではない
SMTモードではROBやフェッチ/デコード帯域幅などを分割するが、SMTでないときは全体を使えるようにする各種SMTコアを見てきた
SMTの主目的は、スーパースカラ実行エンジンの利用率を最大化すること
こうした流れは、人々がスーパースカラは以前ほど重要ではないと考えているという意味なのか気になる
全体として良い要約だが、ところどころ少し混ざっている感じがした
セキュリティに関係しない範囲だけでも、内部の人たちが使っている現場のコツをもっと知りたい
かわいそうなAMDのBulldozerアーキテクチャは、SMTがないとあれほど叩かれたのに、今では皆がSMTから離れつつある
もちろんBulldozerには、SMTがないこと以外にもはるかに多くの問題があったのは分かっている。実際には、複数のコアが同じALUのようなものを共有する、ほぼ正反対の構造だった。それでも性能をもう少し引き出せていれば、何かを先取りしていたと言えたのかもしれない
Power10は現在、コアあたり8スレッドのSMT8を効果的にサポートしており、何年にもわたりSMT中心の設計を発展させ続けてきた努力を考えると、彼らがこれを捨てるとは思えない
知っておくべき点として、GPUの演算ユニットも通常、CUあたり7〜10スレッド程度のSMTを使っている
この方式はレイテンシを隠すのに役立つ
各クロックサイクルごとに、利用可能な複数スレッドの中から、ビジーでないリソースを必要とするスレッドの命令を選んで開始する。ほとんどのGPUはクロックあたり複数の命令を開始せず、開始後に複数の命令が同時に進行することはあり得る。クロックあたり複数の命令を開始する場合でも、スカラ命令とベクトル命令のように、互いに異なる実行リソースを使う別々の命令クラスに属している必要があるかもしれない
SMT、すなわち同時マルチスレッディングは、各クロックサイクルごとにすべてのスレッドから多数の命令が同時に開始され、それらの命令がスーパースカラCPUの複数の実行ユニットをめぐって競合し、可能な限り多くの実行ユニットをビジーに保つ方式である。最新CPUの整数加算器6個それぞれのような並列実行ユニットごとに、すべての同時スレッドの命令を含むキューからどの命令を実行するかが個別に決定される