遅寝早起きで爆進中でございます

今朝は演算コードの生成部分についてのディープな話から…。しかし、この話題はコンパイラの中核を成す部分だといっても過言ではない、と〜っても重要なところなんでやんす。

コンパイラに搭載される演算コードの生成部分。例えば、

Dim a As Long
Dim b As Long
Dim c As Long
b=10
c=20
a=b+c

こういうソースコードをコンパイルすると、下記のようなアセンブルコードが出力されるんですね。

;b=10
push 10
pop eax
mov dword ptr[b],eax

;c=20
push 20
pop eax
mov dword ptr[c],eax

;a=b+c
push dword ptr[c]
push dword ptr[b]
pop eax
pop ebx
add eax,ebx
push eax
pop eax
mov dword ptr[a],eax

なんだか、わかるようなわかんないような・・・・・。とにかくpushとpopばっかやないかぁ〜!というのが正直なプロフェッショナル様方のご意見であって、あまりいいコード生成をしていないことは確かです…。

では、なぜpushとpopばかりになってしまうんでしょうか。そもそも、pushとpopって何か悪いことでもあるんでしょうか。

正直にお答えすると、pushとpopは必要でなければ無くなってほしいというのが本音なんですね。なぜかというと、いちいちメモリにアクセスしなければならないからです。pushという動作は、下記のような動作を行います。

  • espをsizeof(DWORD)分だけ加算
  • レジスタの内容または即値をespが示すメモリへコピー

popも同じく、この反対の動作を行い、メモリの内容をレジスタへコピーしてくれます。メモリにアクセスするのをどうして嫌がるかというと、遅いからなんです。演算を行うたびにいちいちメモリにアクセスしていたら日が暮れてしまいます(ちょい大袈裟!?)。最小限のメモリアクセスで、後はレジスタだけで演算してくれや〜というのがPCの方針であって、pushとpopはレジスタが煮詰まったときの最終手段だぞぃ、というのが本来あるべき姿なんです。

それでは先ほどのソースコード、どのようなアセンブラが出力されればめでたいんでしょう。

;b=10
mov dword ptr[b],10

;c=20
mov dword ptr[c],20

;a=b+c
mov eax,dword ptr[c]
add eax,dword ptr[b]
mov dword ptr[a],eax

う〜ん、マイルドでソフトなコードです。メモリアクセスやコード量を最小限に抑えています。それじゃ、最初からこういうコードを生成してくれるよう、コンパイラを改良してくれよ。と言いたくなるのですが、そうなると話は格段と厄介になってきます。

コンパイラはそもそも、どのようにして演算コードを生成しているんでしょう・・・・・?

まず言えることは、逆ポーランドに則り、コード生成を行っているっつーことです。逆ポーランドは、pushとpopを繰り返すことで演算を実現させているのですが、コンパイラは飽くまでもこの規則に則ってコードを生成しておるんです。

まぁ、おいらがネイティブコンパイラを作り始めたのは2003年の春のことですよ。もう、2年半前の出来事です。プログラミングさえ初心者レベルだというのに、こんなやつが作った演算コード生成部分は逆ポーランドに振り回されてpushとpopを繰り返してしまう、この程度のものです。

しかし、ABはVer5.0で64ビットに対応することになっとります。しかも、64ビットに対応させるためには、既存のコンパイラを作り変えるくらいの勢いが必要なんです。ということは、このような汚いコード生成部分を書き換えるチャンスなんです。レジスタだけでキレイに演算してくれるような、そういう機構を今このタイミングで作ってしまえばいいんです。

しかしですね、そいつを作るのは非常に大変なんですね。機械語というのは、ひとつの命令(例えばmov)であっても、状況に応じて、色々な形に変身するんです。

mov reg32,dword ptr[offset]
機械語 … 1000 1011 00rr r100 0010 0101 [32bit offset]

mov reg32,dword ptr[base_reg+offset]
機械語 … 1000 1011 10rr rbbb [32bit offset]

mov reg32,dword ptr[base_reg1+base_reg2+offset]
機械語 … 1000 1011 10rr r100 00bb bsss [32bit offset]

※機械語は2進数表記、"r" はreg32の識別フラグ、"b" 及び "s" はベースレジスタの識別フラグ

32ビットのmov命令だけをとっても、10種類以上の表現方法があるんではないかというくらいの勢いなんですね。別にアセンブル機構をソースレベルで持っているわけではないんで、こういう多種多様な命令を機械語化したいときは別途、アセンブラ命令→機械語への変換関数のようなものを作ってやる必要があります。

しかも、先ほどの話に戻って、演算の状況に応じて、多種多様なオペランドをそのつど指定していくのは、つらい作業です。それだったら、pushとpopを繰り返してしまったほうが10倍(もしかしたら100倍)くらい楽なんですよ。

まぁ…その…なんだ…

こういうこともありつつ、爆進中でございますので、「対応が遅い」だの、「メール送ったけど返事が無い、または遅い」とかがあっても、あまりお叱りにならないでください。

今がやまばでもあるんで、これを切り抜けられたらもっと面白い企画をDiscoversoftとしてやっていきますんでm(__)m