Pentium 4の倍速ALUを確認する

Intel Pentium 4は、2000年に発売されたCPUで、その設計思想は当時の他のプロセッサと一線を画す部分がいくつかあり、設計のマーケティング呼称としてNetBurstという名前がつけられていた。 その中でも、ALUをCPUの他の部分の倍速で動かす設計 Double-Pumped ALUは珍しく、これもマーケティング用にRapid Execution Engineという名前で宣伝されていた。 このページでは、Pentium 4の倍速ALUが実際倍速で動いているのか確認する方法と、確認に至るまでにわかったPentium 4プログラミングのコツを紹介する。

用意したもの

やったこと

大方針として、ALUに実際に命令を流して、それにかかる時間を測定することで外から動作周波数を推定する方向で行くことにした。

ALUは、しばしば直前のサイクルの結果を次のサイクルで使うためのフォワーディングの機能が実装されている。 この機能のおかげで、レジスタへの書き込みを待たずに後続の演算で直前の演算結果を利用することができる。 Pentium 4にもおそらくこの機能はあるだろうということで進めた(結果から言うと実際あった)。

近代のCPUは、命令のデータ依存関係から、お互い依存関係にない命令を並列に実行するため、単純な命令実行数を数えるにはすべての命令が直列化されるよう、直前の命令の結果に依存するように工夫する必要がある。 最もシンプルに思いつくのは、以下のようにINC %eaxを並べることだ:

.loop:
    .rept 64
    inc %eax
    .endr
    loop .loop

これだと、直前の結果がわかるまで次の命令は実行できないので、ループにかかった時間を測定して、最終的な%eaxの値を割れば、フォワーディングが毎サイクル動いているという前提があるならALUの動作周波数になるはずだった。

ところがならない。

上記1,500 MHz のPentium 4実機で動かすと、約2,100 MHzくらいの結果になってしまう。 プログラム自体は正しそうで、例えばRyzen 5800X3Dだと定格の動作周波数とほぼ一致するため、上記プログラムがPentium 4の実装と噛み合っていない部分がありそうだった。 分岐先のアラインメントや、分岐予測ミスの影響緩和のためにループの長さを長くするなどを実施してみたが効果なし。 そこで、ちょっと古い最適化マニュアル [1]を見たところこんな記述が:

Avoid instructions that unnecessarily introduce dependence-related stalls: inc and dec instructions, partial register operations (8/16-bit operands).
The inc and dec instructions should always be avoided. Using add and sub instructions instead avoids data dependence and improves performance.

というわけで、とりあえず単純にINCをやめてaddで即値を指定することにしてみたうえで、もう命令が長くなってloopで指定できるオフセットには収まらないので、分岐予測ミスのペナルティ軽減のためループ自体の長さもちょっと長くした:

.loop:
    .rept 256
    add $1,%eax
    .endr
    sub $1,%ecx
    jnz .loop

ここまでやると、2,994 MHzの結果となり、1,500 MHzのPentium 4のALUが約3.0 GHzで動いていることが検証できた。

何が気に入らなかったのか

素人考えとしてはINCADDに即値で1を渡すのは等価に思える。 マニュアル [2]を見ると、実際動作として違うのはFLAGSの扱いのようだ。 ADDは、

The OF, SF, ZF, AF, CF, and PF flags are set according to the result.

で、INCは、

The CF flag is not affected. The OF, SF, ZF, AF, and PF flags are set according to the result.

となっている。 挙動や最適化マニュアルの口ぶりから見るに、INCではCFを更新しないので、実行結果は前の命令のFLAGSの結果に依存していて、その結果ストールが起きうるということのようだ。 とはいえ少なくとも1命令/サイクル以上では発行できているので、今回のような一般には起きないような命令列を流したことで、依存性解析の実装上の細かいところが見えてしまっているのだと思う。 新しいプロセッサでは、このあたりの依存性の解析が細かくなっていて目に見える量のストールは観測できない模様。

なお、[1]の命令レイテンシの表を見ると、今回試した第1世代のPentium 4 (Willamette)と同じく第2世代のPentium 4 (Northwood)も倍速ALUの中でフォワーディングしてくれる機能があるようで、ADD/SUBレイテンシは0.5と書いてあるが、同じ表の中で第3世代のPentium 4 (Prescott; CPUID Model 3)はレイテンシ1になっていて、今回使ったフォワーディングの機能が削除されている模様。 Prescottでも、スループットはNorthwoodまでと同じ0.5 (cycle)と書いてあるので、ALUが倍速で動くこと自体は同じようだ。

結論

初期のPentium 4は実際ALUがCPUクロックの2倍の周波数で動いていて、フォワーディングが実装されていて、プログラムから活用可能なことが確認できた。 1.5 GHzのWillametteなら3 GHzでadd命令を流すことができる。

INC/DEC命令はキャリーフラグを更新せず他のフラグを更新するので、不要なデータ依存性を作ってストールを起こすため、ターゲットとしているプロセッサの実装によっては避けたほうが無難。

付録:この測定で使ったプログラムはRust crateとしてライブラリにして GitlabにてCalcmhzとして公開されている。

参考文献

  1. Intel Corporation. IA-32 Intel® Architecture Optimization Reference Manual. Order Number: 248966-012 June 2005.
  2. Intel Corporation. Intel® 64 and IA-32 Architectures Software Developer’s Manual. Volume 2 (2A, 2B, 2C, & 2D): Instruction Set Reference, A-Z. Order Number: 325383-080US. June 2023.