賢いスタックフレーム
abdev 10月 6th, 200564コンパイラは、ネイティブコードを構成する各オペコードを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の規格を例にお話します
- レジスタの値をrspが示すメモリにコピー
- 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コンパイラでも生かしたいものです。
10月 7th, 2005 at 15時17分11秒
やっほ!読んではみたもののまーったくわかんないよ。この間はありがとね!ブログお引越ししました♪http://d.hatena.ne.jp/miyumama72です。画像も入れたいからヤマダ電機いかな。店員さん私の説明で欲しいモノわかってくれるかなぁ。はなっから自分で探す気ないもんっ。頼むよ、店員さ〜ん。なんか不安やなぁ。
10月 7th, 2005 at 20時35分51秒
ebpは関数内でのローカル変数を参照するために使用するレジスタですので基本的な使い方は、以下のようになります。基本的に戻り値はサイズによりますが、al,ax,eax,edx:eaxのいずれかになります。ebpとesp以外のレジスタは保障されません。多くの関数では引数の数がほとんど無いので、64bit環境では、メモリーアクセスを伴う前後処理を減らせるように、レジスタ渡しを行うのでしょう!山本様の提案は、パスカル風のアクセスになります。’—-’ 関数を呼び出す側(C言語風)’ sprintfのように、可変長引数対応の為、’ スタックは呼び出し元が保障する’—- push param1 ’4byte push param2 ’4byte Call kansu add esp,8 ’引数のサイズ分戻す ’ 関数の定義kansu: push ebp mov ebp,esp sub esp,0Ch ’ローカル変数用領域(12byte)mov eax,[ebp+xxxx] ’+は引数にアクセスmov ebx,[ebp-yyyy] ’-はローカル変数にアクセス 内部処理 mov esp,ebp pop ebp ret’—-’ 関数を呼び出す側(Pascal言語風)’ 呼び出し引数は固定’—- push param1 ’4byte push param2 ’4byte Call kansu’ 関数の定義kansu: push ebp mov ebp,esp sub esp,0Ch ’ローカル変数用領域(12byte)mov eax,[ebp+xxxx] ’+は引数にアクセスmov ebx,[ebp-yyyy] ’-はローカル変数にアクセス 内部処理 mov esp,ebp pop ebp ret 8 ’パラメータ分もespを調整する
10月 7th, 2005 at 22時30分51秒
マティさんの話は山本さんもご存知だと思いますが。
10月 8th, 2005 at 9時19分15秒
素人考えですみませんが、構文解析時にレジスタ数に合わせて数式を分解することは出来ないでしょうか。今やっているのは、一組のレジスタとスタックのあるコンピューターでの計算の仕方のように見えます。構文解析時に使えるレジスタ数で処理できるまで数式を分解できればスタックを使う量はかなり減ると思うのですが?あくまで素人の考えることです。笑ってください。
10月 8th, 2005 at 23時11分11秒
やっぱり、そうですよね!
10月 9th, 2005 at 17時12分23秒
> 構文解析時にレジスタ数に合わせて数式を分解することは出来ないでしょうか。大抵のコンパイラは標準でそこまでやってくれるのですが、今のABの状態は9/28の記事に書いてあるとおりです。それが「ABは最適化が弱い」と言われる所以の一です。ま、汎用レジスタの数がたったの8個しかないのもコンパイラ技術者の悩みのタネなのですが。この貴重な資源を如何にやり繰りするかが腕の見せ所なわけで。# Itaniumの汎用レジスタの数は128個。用途は主に整数マルチメディア演算。ftp://download.intel.co.jp/jp/developer/jpdoc/24531704_j.pdf# 余談に余談を重ねますが、次世代プレステのCellアーキテクチャも、たしか128個。用途は主にループの展開。> スタックを使う量はかなり減るスタック使用量を意識する必要があるのは、主として再帰処理を行う場合です。再帰の深さに比例してスタックを食うからです。計算の過程で一時的にスタックを確保する場合、演算が終わればスタックを即時解放することができるのでスタック使用量を意識する必要はあまりありません。>>マティさんpush ebpmove ebp, espこの2行は定番中の定番ですね。大抵の処理系ではサブルーチンの先頭でこの処理を行って自動変数の領域を確保するわけですが、ABでは?ABでは変則的なことをやってのけてたりします。是非一度御覧あれ >マティさん
10月 11th, 2005 at 19時43分30秒
見てみました。EBPが活用されていませんね!クラスが絡むと、より複雑な処理に展開しています・・・だから、ESPの話しが出てきたのですね!理解しました。