* kharcs #5 -(by [[K]], 2025.06.26) ** (0) これはなに? -(kharcsページは、kharc開発でわかったことを整理して説明するためのページ群です。) -今回説明したいこと: ~この順番でCコンパイラを作ったら早くできたよ!~ --[1]~[6]→[[a25_kharcs2]] --[7]→[[a25_kharcs3]] --[8]~[14]→[[a25_kharcs4]] --[15]3dwavei(18日目) --[16]kcubei(19日目) --[17]高速化(20日目) --[18]ここまでのまとめ ** (15) 3dwavei(18日目:6/26) -配列が使えれば3dwaveiができるはずなので、足りない機能(sqrtとかsinとかを固定小数点で求める関数)を補って作りました。 -ちゃんとうねうねと動きました! --https://essen.osask.jp/files/pic20250626a.png #include "kharc.h" int main() { int w, t, x, y, d, z, x0, y0, x1, y1, y0x0, y0x1, y1x0, y1x1, gx[1764], gy[1764]; w = openWin(640, 480); for (t = 0; isClose(w) == 0; t++) { wait(8); fillRect(w, 640, 480, 0, 0, 0x000000); for (y = -20; y <= 21; y++) { for (x = -20; x <= 21; x++) { d = ff16Sqrt((x * x + y * y) * 65536); z = ff16Sin(((d * 652) >> 12) - 1043 * t) * 50 / (d + 327680); x0 = x + 19; y0 = y + 19; x1 = x + 20; y1 = y + 20; y1x1 = y1 * 42 + x1; y0x0 = y0 * 42 + x0; y0x1 = y0 * 42 + x1; y1x0 = y1 * 42 + x0; gx[y1x1] = (x * 2 - y * 2 + z * 0) * 4 + 320; gy[y1x1] = (x * 2 + y * 2 + z * 1) * 2 + 240; if (x0 >= 0) { if (y0 >= 0) { drawLine(w, gx[y0x0], gy[y0x0], gx[y0x1], gy[y0x1], 0x00ffff); drawLine(w, gx[y0x0], gy[y0x0], gx[y1x0], gy[y1x0], 0x00ffff); }} } } } return 0; } ** (16) kcubei(19日目:6/27) -配列処理を多用するプログラムとして、キューブ回転プログラムがあるのですが、それもううまく動くようになりました。C言語で90行程度です。 --https://essen.osask.jp/files/pic20250627a.png --ちなみにこのキューブの面の色の計算方法は、各面の外向きの法線ベクトル(面に垂直なベクトル)を計算して(ついでに単位ベクトルにもしておく)、Z軸の単位ベクトルと内積をとって、+1から-1の値を出して、これが0から+1のときに0xffff00との積をとって色としています。負の時は面が向こうを向いている時で、これはどうせ他の面に隠れてしまいますので、描画しなくてもいいのです。 --Z軸は光線の方向として想定したものです。・・・この、面の法線ベクトルと光線のベクトルの内積で面の明るさを決めるやり方は、たぶん一般的なものです。 --面の法線ベクトルは、隣り合った2つの辺の外積を計算すれば簡単に求められます。 ** (17) 高速化(20日目:6/30) -さてここで少し冷静になって考えてみました。 -私はこのkccを常用するようになるでしょうか。・・・もし作っても使う気がないなら、開発を続ける必然性はありません。それなら別の常用しそうなものを作るべきです(時間がもったいない)。 -まずkccはgccよりも便利になるでしょうか。・・・はい、私にとってはgccよりも便利になると思います。どんどん機能を追加していくはずだからです。アイデアはいっぱいあります。 -しかし便利になっても、その機能を使うかどうかはまた別です。kccの独自拡張を使って書かれたプログラムは、当然ですがgccではそんなソースコードはコンパイルできません(もちろんMSVCやclangでもできません)。 -gccでコンパイルできないとなると、実行速度としてgcc比で1.5倍程度の速度までしか出せないということです(15日目の実験結果より)。 -まあでも、それなら速度が欲しい部分(たぶんソースコード中の1割くらい)だけ標準的なC言語で書いて、残りは便利で書きやすいkccで書けばいいでしょう。それでリンクして実行ファイルを作ればいいんです。 -速度比1.5倍くらいなら、この使い分けでほとんど問題にならないと思います。・・・おおこれはいい。じゃあ心配しないでどんどんやっていこう! -と、ここまで考えたとき、ふと思いつきました。 -kccはbuntan-pcのコード生成にも使えるほどの柔軟性があるのに、どうして「gccがうまく最適化できそうなコード」を出力できないと決めつけているんだろう? -・・・現状ではメモリイメージを真似することにこだわって、すべての変数を char mem[...] 上において、配列としてアクセスするから、gccはうまく最適化できないだけじゃないか? -普通の変数として宣言してアクセスするようにすれば、gccが使用頻度の高いものを勝手にレジスタ変数化して、問題なく普通に高速に実行できるようになるかもしれない。 -これは試す価値がありそうなので、やってみることにします。 -[1]コンパイル前のコード int fib(int i) { if (i > 1) { i = fib(i - 2) + fib(i - 1); } return i; } -[2]kccでコンパイルした結果(このバージョンから補助情報がつきました。でもエミュレータやJITコンパイラはこれを無視します。[4]のためにつけました。) Lb_I(5); Sub_AI(SP,16); Sto_AMd(RP,SP,12); Lod_RMd(R0,SP,16); CmFrJle_RII(R0,1,8); Lod_RMd(R0,SP,16); Add_RI(R0,-2); Sto_RMd(R0,SP,0); Mov_AI(RP,6); Jmp_I(5); Lb_I(6); Sto_RMd(R0,SP,8); Lod_RMd(R0,SP,16); Add_RI(R0,-1); Sto_RMd(R0,SP,0); Mov_AI(RP,7); Jmp_I(5); Lb_I(7); Lod_RMd(R1,SP,8); Add_RR(R0,R1); Sto_RMd(R0,SP,16); Lb_I(8); Lod_RMd(R0,SP,16); Lod_AMd(RP,SP,12); Add_AI(SP,16); Jmp_A(RP); Iap_XII(X0,48,-1); Ivr_Md(SP,8); // SP+8 に割り当てられた変数はint型だよいう補助情報. Iap_XII(X0,17,-1); Ivr_Md(SP,12); // SP+12に割り当てられた変数はvoid*型だよという補助情報. Iap_XII(X0,48,-3); Ivr_Md(SP,16); // この関数の第一引数はint型だよ、SP+16で参照しているよという補助情報. Iap_XII(X0,48,-4); Ief_II(1,8); // この関数はintを返す関数だよという補助情報. -[3]これを従来の方法でC言語化した場合(違いが分かりやすいようにマクロを展開してあります。これをうまく最適化しろというのはgccには荷が重かったわけです。) vJmp: switch (pc) { L_5: case 5: sp -= 16; *(int*)&mem[sp+12] = rp; r0 = *(int*)&mem[sp+16]; if (r0 <= 1) goto L_8; r0 = *(int*)&mem[sp+16]; r0 += -2; *(int*)&mem[sp+0] = r0; rp = 6; goto L_5; case 6: *(int*)&mem[sp+8] = r0; r0 = *(int*)&mem[sp+16]; r0 += -1; *(int*)&mem[sp+0] = r0; rp = 7; goto L_5; case 7: r1 = *(int*)&mem[sp+8]; r0 += r1; *(int*)&mem[sp+16] = r0; L_8: case 8: r0 = *(int*)&mem[sp+16]; rp = *(int*)&mem[sp+12]; sp += 16; pc = rp; goto vJmp; } -[4]今回の方法でC言語化した場合(お?これならいけるんじゃないか??) int L_5(int sp16) { int sp8, sp12; int r0, r1; r0 = sp16; if (r0 <= 1) goto L_8; r0 = sp16; r0 += -2; sp8 = r0; r0 = L_5(sp8); sp8 = r0; r0 = sp16; r0 += -1; sp12 = r0; r0 = L_5(sp12); r1 = sp8; r0 += r1; sp16 = r0; L_8: r0 = sp16; return r0; } -補足: --kharcの機械語から[4]のコードに変換するプログラムは116行で書けました。つまり大した労力じゃないです。 --kharcのアセンブラから[3]のコードに変換するプログラムは20行です。 -ベンチマークテスト結果 | |fib46 |mandel | |[1]をgccでコンパイルして実行|RIGHT:4.608[sec](1.00倍)|RIGHT:1.874[sec](1.00倍)| |[1]をkccでJITコンパイル実行|RIGHT:8.179[sec](1.77倍)|RIGHT:2.919[sec](1.56倍)| |[3]をgccでコンパイルして実行|RIGHT:7.279[sec](1.57倍)|RIGHT:3.373[sec](1.80倍)| |[4]をgccでコンパイルして実行|RIGHT:4.592[sec](''1.00倍'')|RIGHT:1.811[sec](''0.97倍'')| --ということで、[4]の方法なら実行速度での問題はなさそうです。だから将来kccの独自機能の利用をためらう必要はなさそうです! ---- -現状のソースコード規模(小規模感を伝えたいです) |kcpp0.c|RIGHT:378行|プリプロセッサ処理| |khbn.c|RIGHT:186行|kharcアセンブラ・逆アセンブラ| |ccmp.c|RIGHT:1114行|Cコンパイラ処理| |opt.c|RIGHT:265行|最適化処理| |bej.c|RIGHT:220行|エミュレータ実行(124行)& x86JITコンパイラ実行(96行)| |kcc.c|RIGHT:67行|kccのmain関数| |acl3|RIGHT:651行|自分の開発を楽にするための自作ライブラリ| |acl3g||自分の開発を楽にするための自作グラフィックライブラリ| |||| |buntan.c|RIGHT:68行|kharc→buntan-pc向けのアセンブラに変換| |k2c.c|RIGHT:116行|kharc→gcc向けのc言語に変換| --kcc.exe : 実行ファイルは 47.0KB(x86用) ** (18) ここまでのまとめ -[[a25_kharcs3]]の最後の方に「しばらくはお休み」とか書いていたのに、結局全然休めてないです。だからとにかく休みます。 -休む前に、ここまでのことを振り返って、まとめをしようと思います。 -[1]なぜこんなにテンポよく開発できたのだろうか? --[1-1] 2025.04.11~2025.04.25の期間で私はkuasというアセンブラを作りました。buntan-pcプロジェクトのuchanさんからのお誘いでした。→[[a25_buntan03]], [[a25_buntan04]] --その時に「この関数は多分将来の開発でもまた使いたくなりそうだなって思ったものをacl3として分離しておきました(当時はaclminiという名前でしたが)。 --kuasの開発が終わると、さっそくacl3を使って何か作りたい気分になり、2025.05.07~2025.05.23でC言語用のプリプロセッサを作りました。その過程でacl3はさらに強化されました。 --このさらに強くなったacl3があれば、そろそろCコンパイラくらいちょろっと作れるようになったんじゃないかな?いやさすがにそれは無理かー、とか思いながら、2025.06.04から軽い気持ちでCコンパイラを作り始めました。 --つまり、「自作ライブラリを育てるための開発ネタを探す」くらいの軽い気構えが良かったのかなと思うのです。 --[1-2] kccはパーサーもトークナイザーもありません。多分それらを作っていたらもっと行数が増えて開発に時間がかかったはずです。最初は「パーサーとか作るのめんどくさいから、とりあえずパーサーなしで雑に作ろう」でした。途中からは「なんかどんどん行けそうだから、パーサーなしでどこまで行けるかやってみよう」になりました。 --acl3ライブラリがあると、この '(' に対応する ')' はどこにあるか?、みたいなのが簡単に求められてしまうので、なんか雑にやってても何とかなってしまったのです。 --[1-3] Cコンパイラは一般的にC言語のソースコードをアセンブラに変換するわけですが、そのターゲットのkharcが非常にシンプルなので、それが良かった気がします。 --そのシンプルさは、上記のエミュレータ、JITコンパイラ、2つのアセンブラ変換の規模の小ささにも表れています。 --[1-4] プリプロセッサ、グラフィックライブラリはすでに作ったものがあったので、それをマージするだけでできました。 --またエミュレータやJITコンパイラについては別の開発で経験済みだったので、要領よくできました。 -[2]何を目的に開発するといいのか? --もちろん自分が普段使いするためです。使う気がないなら作りません。誰も使わなものを作っても楽しくないです。 --私は今までOSや言語をいくつも作ってきました。その中で普段使いするまでに至ったものはそれほど多くはないです。なぜ使わないのかを何度も自問して、次こそは使い続けられるものを作ろう、ということを繰り返してきています。 -[3]言語を自作するときに最適化はどうするべきか? --普段使いするということを考えると、最適化が弱すぎたら使いたくなくなると思います。最適化が強ければ、ろくに工夫しなくてもいきなり2倍とか3倍とか速いわけです。そんなの嬉しすぎですよ。逆に何を書いても遅いとかだと悲しすぎます。 --でも、最適化に強い言語を作るのは相当に大変です。個人が趣味的に作れる範囲を超えています(たぶん)。だから既存のCコンパイラやLLVMなどを併用して最適化をするのがいいと私は思います。すでにあるものが使えるなら、それを利用して楽をする方が得だと思うのです。