* 「10日くらいでできる!プログラミング言語自作入門」の続編#1-4
-(by [[K]], 2021.04.21)
** (1) HL-14
-さて、とにかくJITコンパイラにはなったものの、生成している機械語の質が低いので、gcc-O3と比べるとまだまだ速度で負けています。
|ページ名|名前|行数|.exeの大きさ|説明|速度のめやす1|速度のめやす2|
|[ [[a21_txt01]] ]|HL-1~HL-9a|RIGHT:49行~772行|RIGHT:6.0KB~20.0KB|「10日くらいでできる!プログラミング言語自作入門」|RIGHT:1520倍~5.5倍|RIGHT:6.4倍|
|[[a21_txt02]]|HL-11|RIGHT:692行|RIGHT:14.5KB|JITコンパイラ版1号|RIGHT:計測不能|RIGHT:計測不能|
|[[a21_txt02_1a]]|HL-11a|RIGHT:707行|RIGHT:15.0KB|簡単な演算もサポート|RIGHT:計測不能|RIGHT:計測不能|
|[[a21_txt02_2]]|HL-12|RIGHT:745行|RIGHT:16.0KB|ループ速度の測定ができるところまで|RIGHT:5.3倍|RIGHT:計測不能|
|[[a21_txt02_2a]]|HL-12a|RIGHT:766行|RIGHT:16.5KB|codedumpコマンドと、codeコマンドの追加|RIGHT:5.3倍|RIGHT:計測不能|
|[[a21_txt02_3]]|HL-13|RIGHT:827行|RIGHT:21.0KB|配列以外は全部JITコンパイラ対応させる(mandel.cで速さを実感!)|RIGHT:5.3倍|RIGHT:2.0倍|
|[[a21_txt02_3a]]|HL-13a|RIGHT:819行|RIGHT:21.0KB|配列もJITコンパイラ対応させる(ついに機能的にはHL-9a相当に!)|RIGHT:5.3倍|RIGHT:2.0倍|
-さすがに追いついたり追い越したりはできないと思いますが、でももうちょっと近づきたいとは思うので、ここからしばらくは最適化をやってスピードアップをしたいと思います。
----
-最適化をどこからやるかですが、おそらく効果が一番あるのは「レジスタ変数」のサポートです。HL-13aでは、変数はすべてメモリ上にありました。これをEAXレジスタに読み込ませて、演算をして、EAXに結果が生成されて、それをメモリ書き戻しています。多くの場合この一連の処理の中で一番のボトルネックはメモリへの読み書きで、これをなくしてレジスタ内の演算だけにすればきっと速くなると思われます。
-x86の場合、演算に使えそうなレジスタは、EAXのほかにECX、EDX、EBX、EBP、ESI、EDIがあって合計7個です。でもこのうちのEAX、ECX、EDXの3つは、割り算やシフト命令や配列アクセスなどで使用するために、そこそこの頻度で値が壊される危険があります。そうなると残りはEBX、EBP、ESI、EDIの4つしかありません。
-一方で、プログラムで使われる変数は4個よりも多いことが普通で、だからすべての変数をレジスタ変数に割り当てることはできません。一般には使用頻度の高い変数をレジスタ変数に割り当てることになります。・・・それで、どの変数をレジスタ変数に割り当てるべきか、gccなどのコンパイラはプログラムを解析してすごい努力をして決定しています。おそらコンパイルに時間がかかっている主たる理由はこれなのではないかと私は思っています。
-ではHL-14ではどうしましょうか。人間が見れば、この変数をレジスタ変数にしたらよさそうだなっていうのは結構簡単にわかります。ループの最も内側で使っている変数とかを速くすればいいだけなのです。しかしそれを自動判定するプログラムは、きっとすごく大変なので、私はここで扱いたくありません。ということで、人間が自由に指定することにします。機械が得意なことは機械がやって、人間が得意なことは人間がやればいいのです。
-ということで regVar()コマンドを作ります。
regVar(0, x); regVar(1, y); regVar(2, z);
-とすると、xを優先度0でレジスタ変数化してほしくて(=最優先)、yを優先度1でレジスタ変数化してほしくて、zを優先度2でレジスタ変数化してほしい、ということにしました。というのは、HLシリーズはこれからx64やARMに対応していくかもしれません。そしたら使えるレジスタ変数の数もきっと変わるでしょう。その場合は優先度を見て、順番に割り当てさせることにします。もしレジスタが足りなくなったら、優先度の低いものについてはメモリ割り付けのままにします。
-上記のように三回もregVarと入力するのは面倒なので、
regVar(0, x, y, z);
-という省略形も使えるようにします。
-関数の形にしているのは、プログラム内で使った時にC言語との互換性を維持できるようにするためです。C言語で使うときは、
#define regVar(...)
-などと書いて、regVar()命令を無視させるようにします。
----
-[1]putIcX86_sub()関数の前に以下を追加
IntP regVarTbl[4]; // レジスタ変数の割り当て状況を記憶.
int regVarNo[4] = { 3, 5, 6, 7 }; // レジスタ変数番号から、レジスタ番号に変換.
int regVar(IntP v) // もし変数vがレジスタに割り当てられていれば、0~3を返す. そうでなければ-1を返す.
{
int i;
for (i = 0; i < 4; i++) {
if (regVarTbl[i] == v)
return i;
}
return -1;
}
return -1;
}
-[2]putIcX86_sub()関数の一部を改造
if (s[i + 2] == 'm') { // mod r/m.
k = s[i + 3] - '0';
+ if (regVar(a[j]) < 0) {
*icq = 0x05 + k * 8;
put32(icq + 1, (int) a[j]);
icq += 5;
+ } else {
+ *icq = 0xc0 + regVarNo[regVar(a[j])] + k * 8;
+ icq++;
+ }
i += 4;
continue;
}
-[3]putIcX86()関数の宣言の後に以下を追加
enum { RvSave = 0x89, RvLoad = 0x8b };
void regVarSaveLoad(int op) // レジスタ変数を準備するor書き戻す.
{
int i;
for (i = 0; i < 4; i++) {
if (regVarTbl[i] != 0) {
putIcX86("%0c_%1c_%2i;", (IntP) op, (IntP) (0x05 + regVarNo[i] * 8), regVarTbl[i], 0);
}
}
}
-[4]compile()関数に1行追加(1)
icq = ic;
jp = 0;
putIcX86("60; 83_ec_7c;", 0, 0, 0, 0); // PUSHAD(); SUB(ESP,124);
+ regVarSaveLoad(RvLoad);
dump0 = icq;
-[5]compile()関数に14行追加
+ } else if (phrCmp(37, "regVar(!!*0", pc)) {
+ i = tc[wpc[0]] - Tc0;
+ for (pc = ppc1; tc[pc] != TcBrCls; pc++) {
+ if (tc[pc] == TcComma) continue;
+ if (0 <= i && i <= 3) {
+ regVarSaveLoad(RvSave);
+ regVarTbl[i] = &var[tc[pc]];
+ if (tc[pc] == Tc0)
+ regVarTbl[i] = 0;
+ regVarSaveLoad(RvLoad);
+ }
+ i++;
+ }
+ ppc1 = pc + 2;
-[6]compile()関数に1行追加(2)
defLabel(toExit);
dump1 = icq;
+ regVarSaveLoad(RvSave);
putIcX86("83_c4_7c; 61; c3;", 0, 0, 0, 0); // ADD(ESP,124); POPAD(); RET();
icq1 = icq;
----
-以上すべての改造を終えると、プログラムは865行になります。
** (2) プログラムの説明
-今回の改造で最も重要なのは、putIcX86_sub()関数の改造のところです。
if (s[i + 2] == 'm') { // mod r/m.
k = s[i + 3] - '0';
+ if (regVar(a[j]) < 0) {
*icq = 0x05 + k * 8;
put32(icq + 1, (int) a[j]);
icq += 5;
+ } else {
+ *icq = 0xc0 + regVarNo[regVar(a[j])] + k * 8;
+ icq++;
+ }
i += 4;
continue;
}
-これはつまり、%mで変数を指定されたときにレジスタ変数でなければ従来通りの機械語を生成しますが、レジスタ変数であればmod r/mのmodをメモリ指定モードではなくレジスタ指定モードに切り替えて、r/mのところでレジスタ番号を指定するようにしているのです。
-あとはcompile()の最初のところで、レジスタ変数の場合はメモリ上にある変数の値をレジスタに読み込ませます。そしてcompile()の最後のところで、レジスタの内容をメモリに書き戻します。そうすれば次回実行時にもレジスタ変数の値は引き継がれます。
-またregVar()命令でレジスタ変数の設定状況を変更するときは、まずregVarSaveLoad()でレジスタ変数の値をメモリ書き戻させて、その次に設定を変更し、最後に変更された設定に従ってレジスタ変数を読み込ませています。
-なおregVar()命令で、変数名の代わりに0を指定すると、その優先度のレジスタ変数の設定は解除されます。つまり regVar(0, 0, 0, 0, 0); とすればレジスタ変数を一切指定していない初期状態に戻るというわけです。
** (3) さて、これでどのくらい高速化されるか?
-10億回ループを実行する前に、 regVar(0, i); を実行しておきます。
-また、 run mandel.c する前には、 regVar(0, zx, zy, xx, yy); を実行しておきます。
|ページ名|名前|行数|.exeの大きさ|説明|速度のめやす1|速度のめやす2|
|[ [[a21_txt01]] ]|HL-1~HL-9a|RIGHT:49行~772行|RIGHT:6.0KB~20.0KB|「10日くらいでできる!プログラミング言語自作入門」|RIGHT:1520倍~5.5倍|RIGHT:6.4倍|
|[[a21_txt02]]|HL-11|RIGHT:692行|RIGHT:14.5KB|JITコンパイラ版1号|RIGHT:計測不能|RIGHT:計測不能|
|[[a21_txt02_1a]]|HL-11a|RIGHT:707行|RIGHT:15.0KB|簡単な演算もサポート|RIGHT:計測不能|RIGHT:計測不能|
|[[a21_txt02_2]]|HL-12|RIGHT:745行|RIGHT:16.0KB|ループ速度の測定ができるところまで|RIGHT:5.3倍|RIGHT:計測不能|
|[[a21_txt02_2a]]|HL-12a|RIGHT:766行|RIGHT:16.5KB|codedumpコマンドと、codeコマンドの追加|RIGHT:5.3倍|RIGHT:計測不能|
|[[a21_txt02_3]]|HL-13|RIGHT:827行|RIGHT:21.0KB|配列以外は全部JITコンパイラ対応させる(mandel.cで速さを実感!)|RIGHT:5.3倍|RIGHT:2.0倍|
|[[a21_txt02_3a]]|HL-13a|RIGHT:819行|RIGHT:21.0KB|配列もJITコンパイラ対応させる(ついに機能的にはHL-9a相当に!)|RIGHT:5.3倍|RIGHT:2.0倍|
|[[a21_txt02_4]]|HL-14|RIGHT:865行|RIGHT:21.5KB|レジスタ変数の導入(46行しか増えない簡単な改造だけど、結構な効果がある)|RIGHT:2.0倍|RIGHT:1.2倍|
-はっきりと効果が出ています!!
** 次回に続く
-次回: [[a21_txt02_4a]]
*こめんと欄
#comment