Skip to content
Published on

命令ひとつが62秒かかりうる — 遅延は命令ではなく経路の属性だ

シェア
Authors

はじめに — 1サイクルと1,980億サイクルのあいだ

命令の遅延表は、最適化する人にとっての基本道具です。乗算が3サイクル、除算が20サイクル、という具合に覚えてコードを書きます。

2026年8月に公開されたAssembly Hall of Shameは、その反対方向へ進みます。副題が「Racing to the bottom of CPU performance」です。単一命令ひとつをもっとも遅くする競争の順位表です。

最下位の27位は nop で1サイクルです。1位は fxrstor64 で1,980億サイクル、時間にして62秒です。

同じ種類のもののあいだに2,000億倍の差があるなら、その種類をひとつの尺度で測っているという前提から疑うべきです。この記事はその順位表を下から上へ読みながら、何がその差を作るのか、そしてそれがあなたのコードとベンチマークにとって何を意味するのかを整理します。

規則が作ったもの — 何が「命令ひとつ」なのか

こういう競争では規則がそのまま論旨になります。リポジトリに書かれた規則はこうです。

  • 準備の過程には何を使ってもよいが、採点されるのは単一命令ひとつだけである。
  • トラップされたり、エミュレートされたり、仮想化される命令はトラップまでを測り、ハンドラは測らない。
  • 命令は中断可能であってはならないrep movspause のようなものは失格である。
  • 時間はCPUのベースクロック基準で正規化する。
  • すべてのプラットフォームは工場出荷設定でなければならず、ハードウェアの改造は禁止する。

三つ目の規則がこの競争を意味あるものにします。中断可能な命令を許せば、リピート接頭辞の付いた文字列命令でどんな数字でも作れてしまいます。中断不可を要求すれば、パイプラインがその命令ひとつに実際に縛られている時間だけを数えることになります。

二つ目の規則も同じです。トラップされる命令を許せば、測定対象は命令ではなくオペレーティングシステムのハンドラになります。

この二つの規則のおかげで、順位表は小手先の芸ではなくマイクロアーキテクチャの観察記録になります。

第一の区間 — コアの中でマイクロコードが割り込む地点

下位はすべてコア内部で起きる出来事です。測定はほとんどがIntel Core i7-8559Uで行われました。

順位命令サイクル何が起きるか
27nop1何も起きない
26nop1620data16 接頭辞を七つ付けた長いnop
25rdtsc49基準点
24idiv77128ビットの被除数で除算器の最長経路を通す
23enter112最大のネスト深度でマイクロコードのディスプレイ走査を誘発
22fldl133非正規数のロードでFPマイクロコード補助へ入る
20fsin257指数0x7ffの特殊値処理の経路
17fadd677非正規のオペランドによるFPマイクロコード補助
15fdiv883非正規の除数
14cpuid1,248遅延がもっとも大きいリーフを選択

この区間の共通パターンが見えます。ハードウェアの高速経路が処理できない入力を与えると、制御がマイクロコードへ移ります。

fadd の項目がとりわけ教訓的です。同じ命令なのに、オペランドが非正規数だという理由だけで677サイクルかかります。正常な浮動小数点の加算は数サイクルです。命令はそのままでデータだけが変わったのに、二桁の倍率が生まれたわけです。

enter の項目も面白いところです。ネスト深度を最大の31にすると、ディスプレイポインタを30個ロードしてプッシュするマイクロコード経路を通ることになります。命令のエンコード上は許されるものの、現代のコンパイラが決して生成しない形です。

第二の区間 — コヒーレンシとプラットフォームが介入する地点

中間の区間からはコアひとつでは終わりません。

順位命令サイクル何が起きるか
21clflush165ダーティラインの追い出し
19mfence326movnti 16個で書き込み結合バッファを飽和させてから全部を排出
18mov cr3352TLBの全体無効化
16split lock865キャッシュラインをまたぐ lock xaddl で外部バスロックを誘発
13rdrand5,579ハードウェアのエントロピープール枯渇後の回復待ち
12wrmsr34,304ZenのMCG_CTLへの書き込み。ダイ外のユニットまで同期すると推定
11out49,857NICレジスタ境界をまたぐポート書き込みでTX DMAが停止
9wbinvd1,616,480キャッシュ階層全体をダーティで満たしてからDRAMへライトバック

16位のsplit lockが実務でもっともよく出会う項目です。アトミック演算のオペランドがキャッシュライン境界をまたぐと、CPUは高速なMESIキャッシュ一貫性の経路を使えず、外部バスロックを掛けなければなりません。865サイクルかかり、そのあいだ他のコアも影響を受けます。

9位の wbinvd が160万サイクルなのも注目に値します。これは命令が複雑だからではなく、キャッシュに入っていたすべてのダーティデータをDRAMまで押し出さなければならないからです。つまりこの命令のコストは、命令ではなくその時点のキャッシュの状態が決めます。

12位の wrmsr の項目の推定も引用に値します。リポジトリは、これが複数のハードウェアユニットにまたがるマシンチェックバンクをマイクロコードが停止させて同期する動作に見え、一部はダイの外にあるため単純なローカルレジスタ書き込みではなくファブリックレベルの通信を要求しているようだ、と書いています。著者本人が推定であると明かしているので、そのまま移します。

第三の区間 — ダイの外へ出る瞬間

上位は性格がまったく違います。

順位命令サイクル時間
8inl(ACPI PMポート)12,524,4153.92ミリ秒
7movl(MMIO GPUレジスタ)443,937,696139ミリ秒
6movq(8バイトMMIO)887,716,864278ミリ秒
5vmovdqu xmm(16バイト)1,774,555,776556ミリ秒
4vmovdqu ymm(32バイト)3,549,079,2961.111秒
3vmovdqu ymm(非整列32バイト)4,453,212,2561.394秒

ここにあるものは全部 mov です。データを移すこと以外に何もしない命令が1秒を超えます。

理由はアドレスにあります。これらのアドレスはDRAMではなくPCIeファブリックの向こうにあるデバイスのレジスタです。著者は mmiotic という別のツールでMMIO空間を探り、応答がもっとも遅い領域を見つけました。

そして6位から3位までの増え方が、この区間の原理をそのまま露わにします。アクセス幅を8バイト、16バイト、32バイトと広げると、時間がおよそ二倍ずつ増えます。リポジトリの説明によれば、8バイトのMMIO読み出しひとつがダブルワードのレジスタアクセス二回に分解されるからです。32バイトなら八回、整列を外せば九回です。

ここでの核心はこれです。MMIOの読み出しはノンポステッドトランザクションです。 書き込みは送って忘れられますが、読み出しは応答を受け取らなければなりません。だから往復時間がそのまま命令の実行時間になります。リポジトリは3位について、非整列の32バイトMMIO読み出しは「技術的には許されていないがとにかく動く」と書き添えています。

1位の戦略が証明すること

いよいよ1位です。

; CPU 0 - 測定対象の命令
movl $0xfcc68830, %rsi
fxrstor64 %rsi

; CPU 1..N - 別の高遅延の場所を叩くハンマーループ
movl 0xfcc68858, %eax

fxrstor64 は512バイトのFPU/MMX/XMM状態をメモリから復元する命令です。その512バイトをもっとも遅いMMIO領域から読ませると、上の3位の技法の拡張版になります。これだけで74,584,168,512サイクル、23.35秒が出ます。

1位はここにもう一枚重ねます。そのロードが進んでいるあいだ、残りのコアが別の高遅延MMIOレジスタを4バイト単位で叩き続けます。 PCIeのルートコンプレックスとエンドポイントがノンポステッドトランザクションで飽和し、CPU 0の512バイトのロードはその後ろに並ばなければなりません。

結果が198,002,498,236サイクル、62秒です。つまり命令ひとつの実行時間が、他のコアが何をしているかによって二倍以上変わったわけです。

この実験が証明する命題は明確です。「この命令は何サイクルか」という問いは、周囲の状態を固定しなければ答えがありません。 表に書かれた数字は命令の属性ではなく、命令とシステム状態の組み合わせに対する観測値です。

付随的に、この技法が実用的な結果を生んだこともあります。リポジトリは、3位の非整列ymmロードがSystem Management Modeの根本的な設計を破るのに使われたと書き、別のリポジトリをリンクしています。割り込みを任意に長く引き延ばせるなら、原子性を前提にした設計は壊れます。

この表があなたのベンチマークについて語ること

順位表は極端な遊びに見えますが、結論は平凡な性能作業にそのまま当てはまります。

マイクロベンチマークは命令ではなくシステムの状態を測ります。 同じコードが、キャッシュが温かいときと冷たいとき、他のコアが暇なときと忙しいとき、データが正規数のときと非正規数のときで、まったく違う数字を出します。ベンチマーク結果を引用するときに条件を併記しなければならない理由がこれです。

最悪の場合は平均の倍数ではありません。 上の表の区間は連続しておらず、崖で分かれています。平均遅延がどれほど良くても、崖をひとつ踏めばそのリクエストひとつが百倍遅くなります。テール遅延を扱うときは、平均を改善するより崖を見つけて消すほうが効果的です。

同一の命令ストリームでも他のコアの活動に影響されます。 1位の事例は極端ですが原理は同じです。共有資源は最終レベルキャッシュ、メモリコントローラ、インターコネクト、そしてI/Oファブリックまで続きます。単独実行のベンチマークが本番の性能を予測できない理由です。

実際につまずく三つ

最後に、この表のなかで実務に直接触れる項目を三つ整理します。

非正規数。 表の22位、17位、15位が全部これです。信号処理や物理シミュレーションのように値が0へ徐々に収束するコードで実際に現れます。症状は「入力データによって同じコードが急に遅くなる」です。対応方法はハードウェアと言語によって違うので(x86のflush-to-zeroおよびdenormals-are-zeroモードが代表的です)、それぞれのプラットフォーム文書を確認する必要があります。ここで重要なのは、この崖の存在を知っていてはじめて原因を見つけられるという点です。

split lock。 表の16位です。Linuxカーネルはこの現象を検出する機能を提供します。カーネル文書はsplit lockを「オペランドが二つのキャッシュラインにまたがるすべてのアトミック演算」と定義し、バスロックを「ライトバックメモリに対するsplit lockアクセス、またはライトバックでないメモリに対するすべてのロックアクセス」と定義します。起動パラメータ split_lock_detectoffwarnfatalratelimit:N の値を受け取り、文書に記載された既定値は warn です。カーネルログに関連する警告が出ているなら、それは実際の性能問題である可能性が高いです。

MMIOをメモリのように扱うコード。 表の上位が丸ごとこの話です。文法は mov ひとつですが、コストはDRAMアクセスの百万倍でありえます。ドライバや組み込みコードでデバイスレジスタをループの中で読んでいるなら、そのループは計算ではなくI/O往復の繰り返しです。状態のポーリングを割り込みに変える、複数のレジスタを一度に読む、読み出しを書き込みに変えられないか検討する価値があります。

リポジトリはまだx86の順位表だけが埋まっており、ARMとRISC-Vは準備中だとしています。他のアーキテクチャの一覧が埋まれば、どの崖がx86固有で、どれが現代のSoC一般の性質なのかがより明確になるでしょう。

参考資料

私はこの順位表のどの項目も直接実行していません。相当数がシステムを停止させたりデバイスを不安定にする操作であり、リポジトリの実測値は特定のハードウェア(主にIntel Core i7-8559UとAMD Ryzen 7 5800H)に対する値です。