Intel Pentium 4は、2000年に発売されたCPUで、その設計思想は当時の他のプロセッサと一線を画す部分がいくつかあり、設計のマーケティング呼称としてNetBurstという名前がつけられていた。
その中でも、ALUをCPUの他の部分の倍速で動かす設計 Double-Pumped ALU
は珍しく、これもマーケティング用にRapid Execution Engine
という名前で宣伝されていた。
このページでは、Pentium 4の倍速ALUが実際倍速で動いているのか確認する方法と、確認に至るまでにわかったPentium 4プログラミングのコツを紹介する。
用意したもの
- Intel Pentium 4 1.5GHz (Socket 423, 180 nm) 通称Willamette
- ASUS P4T (BIOS 1007)
- RDRAM 1 GBytes
- Debian 12 Bookworm
やったこと
大方針として、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で動いていることが検証できた。
何が気に入らなかったのか
素人考えとしてはINC
とADD
に即値で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として公開されている。
参考文献
- Intel Corporation. IA-32 Intel® Architecture Optimization Reference Manual. Order Number: 248966-012 June 2005.
- 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.