64コンパイラは、ネイティブコードを構成する各オペコードをAMD64の規格へと変更しながら作られていくのですが、x86の機械語をAMD64の機械語にただ単に挿げ替えればいいということではありません。

先日、話題に出したように、レジスタは16個に増えるし、実数演算はSSE2を使うようにしなきゃならないですし、、、そんでもって本日の議題となるのですが、スタックフレームの扱い方についても、大きな変更点が生じるのです。

現状のABコンパイラ(Ver4.0 – 32ビット版)は、「最適化」という側面においては頭の悪い子だということはご承知のとおりですね。pushとpopしかしらないような、おこちゃまです。x86が持っている汎用レジスタはeax,ecx,edx,ebx,esp,ebp,esi,ebiの8本。esp,ebpはスタックフレームの制御に使ってるんで、実質上、項として扱えるレジスタはそれ以外の6本ということになります。うーん、これじゃ、ちょっと複雑な計算をコンパイルさせると、すぐに頭打ちしちゃいますな。

レジスタ数が足りないと、その足りない分をメモリで補うのがコンパイラのお約束です(っつか、そうしないと物理的に演算ができないんです)。

具体的に言うと、足りない分の値をpushし、スタックへと退避させることで演算中の値を保持します。そして、うまい具合に退避した値をレジスタに取り出しつつ、すべての演算をこなすというわけなんですね。

AMD64だって、レジスタ数は増えましたが、やはり複雑な演算式をコンパイラに通すと、たちまちレジスタは足りなくなります。となると、AMD64にアップグレードさせたところで、退避率は減っても、完全解消することはできないんです(完全解消させるとなると、.NETのCLRの話とかになっちゃうんでやめておきます)。

まぁ、CPUがどんなにたくさんのレジスタを積もうとも、理論的に限界が生じることは確かなようです。少なくとも、汎用レジスタが16本とかいっているウチは限界みえみえです。どっちにしても、スタックフレームにはお世話になるわけです。pushとpopには甘えなくちゃならんのです。

しかぁーし(`△´)、この工程、ちょっとだけ疑って考えてみましょう。

pushをするっつーことは、それだけで下記の通り、二つの動作をCPUにさせるということになります。※ここでは、AMD64の規格を例にお話します

  1. レジスタの値をrspが示すメモリにコピー
  2. rspに8を加算

「メモリに退避」を行うことが目的なんで、1番目の「レジスタの値をスタックポインタが示すメモリにコピー」はいいんですよ。しかしですよ、2番目のスタックポインタへの加算行為が許しがたい行為なんですよ。「退避」が目的なんで、アクセスの対象となるのはレジスタとメモリだけというのが理想的なんですが、それと同時にrspも書き換えられてしまうんですよね。

※誤解を招くといけないんで、断っておきます。pushに必要なクロックサイクルは1です。キャッシュミスなどの状況によって増えることがあるのかわからないっすけど、おおよそケースにおいて、pushは最小タイムで完了します。

rspが変動的であると、rspをベース値にしたオフセットを自由に扱えなくなる恐れがでてきます。AB4コンパイラでは、区間内でespが変動的なので、ebpをスタックフレームのベース値(ローカル変数フレームのベース値)として採用することで、この問題を回避しています。

ここから、x64の規格のお話に突入していきます。

もしも、もしもです。区間内で必要なスタックフレームのサイズがわかったとしたら、どのような利点が生まれるのでしょうか。ちなみに、ここで言う区間とは、ひとつの関数だと考えてもらって結構です。

スタックフレームのサイズがあらかじめわかっていれば、その区間の始めと終わりに下記のような命令を入れてしまえば、rspを変動させずに済みます。

sub rsp,(スタックフレームサイズ)

...処理

add rsp,(スタックフレームサイズ)
ret

区間内ではあらかじめフリーなスタックフレーム領域が確保されるので、いちいち、rspを変化させてしまうようなpush/popを使わなくてもよいことになります。

push/popの代わりに何を使うかというと、そいつはズバリ、mov命令なんです。rspをベース値にしてスタックフレーム内のオフセット値を指定してやればいいんですよ。

push rax   ;退避
...処理
pop rax   ;復元

raxを保持したい場合には上記のような書き方をしていましたが、今後は下記のようなrspが変化しない書き方に変わります。

mov qword ptr[rsp+オフセット], rax   ;退避
...処理
mov rax, qword ptr[rsp+オフセット]   ;復元

この話は、演算中に不足したレジスタ値を退避するだけにとどまりません。実は、関数の呼び出し規約に深くかかわってくるのです。

x86の環境では、標準の呼び出し規約(_cdeclや_stdcall)はパラメータの引渡しをすべてpushしていましたが、AMD64の環境では、第4パラメータまでは汎用レジスタ、以降のパラメータはスタックフレームにmovすることで実現しているんです。新しい呼び出し規約にスムーズに対応するためには、スタックフレームの先立った確保は必須になります。

なんだかんだで、こういう部分のケアが64コンパイラの製作では必要だってことです。アーキテクチャが変わると、根底から覆されてしまいますな。今回は、x86→AMD64という、互換性がかなり重視された環境化での移行作業ですが、もし仮にまったく別物のCPUに対応させようともなると、考えるだけで恐ろしいものがあります。

Itaniumあたりにはお願いしたいところです。おこちゃまにもわかりやすいアーキテクチャであってくれと・・・・・

さてさて、自分で読み返すのが大変になってきたんで、今日はここらでやめときます。

ともかく、AMD64の新規格に対抗しながらのAB5×64コンパイラは、最適化の問題を最低限クリアしたものになりそうなことは確かなようです。この技術、追々は32コンパイラでも生かしたいものです。