開発メモ : x64用にコンパイルしなおすデメリット

昨日の記事で、x64用にコンパイルしなおすメリットを書きましたが、通常は以下のようなデメリットのほうがそれらを上回ります。


ポインタ、関数アドレスなどがすべて64bit幅になるので、プログラムのサイズが増えます。その結果、L1/L2/L3 cacheに収まらなくなることがあります。プログラムのサイズだけではなく、ポインタなどを含む構造体のサイズも膨らみます。あと4byteではなく8byteでalignした場合などは、配列のデータサイズも著しく膨張します。これまたL1/L2/L3 cacheに収まらなくなる原因になります。


構造体サイズが増えた結果、アドレッシングがしきれなくてシフトや掛け算が必要になることがあります。

mov ebx , [ebp + eax * 8 + 123];

x86でよく見かけるアドレッシングですが、x86でも、倍率は、1,2,4,8のままです。何故AMDが命令拡張のときについでに16とか32の倍率を追加しなかったのかよくわかりませんが、x64ではx86のときより高い頻度でシフトや下手すると掛け算が必要になります。


あと、よくある例としては、配列の添え字です。int a[5] のような配列にアクセスするために、xxx = a[getInt()]; のようにアクセスしているとします。x86環境では、戻り値のintの値をそのまま加算する次のようなアドレッシングが使えます。

mov ebx , [ecx + eax * 4 + 123]

ところがx64環境だと、アドレス空間が64bitであるため、ecxではなくこれの64ビット版であるrcxを使う必要があります。この64ビットレジスタと32ビットレジスタの混在アドレッシングは不可能なので、次のように、もう片方のレジスタも64bitレジスタに変更する必要があります。

mov ebx , [rcx + rax * 4 + 123]

こうなると、getIntの戻り値を32bit(eax)で返されても、raxにビット幅を拡張しなければなりません。32bitの返し値でも64bitレジスタで返せばいいように思うのですが、その関数がraxの上位32bitをクリアしてくれることは保証されないという呼び出し規約のようで(何故そうなっているのかは私にはよくわかりませんが)、そのようなことは出来ません。


結局、関数からの戻り値を64bitに拡張する命令が必要になります。intやlongが32bit型である場合は、このように64bit幅に拡張するためのコストが都度ついてまわります。かと言って、intやlongが64bit型であるデータモデルだと、掛け算が64bit×64bitの掛け算になり、すこぶる遅くなったり、命令サイズが増えたりで、いいとは限りません。


また、x64だと64bit幅の演算が速いかのように思われていますが、64bit幅の即値に対してbitwise andをとる命令は無かったりします。すなわち、次のようにいったん代入してからandをする必要があります。

mov rax , 0x12345678abcdef
and ebx , rax

なかなか不自由です。


あとは、新しく追加されたr8〜r15レジスタですが、これらはアドレッシングが不自由なため、主に関数の引数の受け渡しや、一時待避用ぐらいにしか使えません。


ともかく、以上のような理由からx86用のコードをそのままx64でコンパイルしなおしても速くなるどころか遅くなるのが普通です。よっぽどx64に特化した書き方をすれば、20%〜30%ぐらい速くなることがありますが、それはかなり特殊なケースです。普通はICCで生成したx86用のコードより速くはならないです。


結論としては2GB以上の大きなメモリを扱うか、64bitに特化した書き方をする覚悟(=x86環境を完全に捨てる覚悟)がないなら、x64用にコンパイルしても何も嬉しくはないでしょう。市販のコンピュータ将棋ソフトには厳しいかも知れませんね。