a25_kharcs4
の編集
https://essen.osask.jp/?a25_kharcs4
[
トップ
] [
編集
|
差分
|
バックアップ
|
添付
|
リロード
] [
新規
|
一覧
|
単語検索
|
最終更新
|
ヘルプ
]
-- 雛形とするページ --
BracketName
EssenRev4
FormattingRules
FrontPage
Help
InterWiki
InterWikiName
InterWikiSandBox
K
MenuBar
PHP
PukiWiki
PukiWiki/1.4
PukiWiki/1.4/Manual
PukiWiki/1.4/Manual/Plugin
PukiWiki/1.4/Manual/Plugin/A-D
PukiWiki/1.4/Manual/Plugin/E-G
PukiWiki/1.4/Manual/Plugin/H-K
PukiWiki/1.4/Manual/Plugin/L-N
PukiWiki/1.4/Manual/Plugin/O-R
PukiWiki/1.4/Manual/Plugin/S-U
PukiWiki/1.4/Manual/Plugin/V-Z
RecentDeleted
SDL2_01
SandBox
WikiEngines
WikiName
WikiWikiWeb
YukiWiki
a21
a21_acl01
a21_bbs01
a21_challengers
a21_count
a21_edu01
a21_edu02
a21_edu03
a21_edu04
a21_edu05
a21_edu06
a21_edu07
a21_edu08
a21_edu09
a21_edu10
a21_edu11
a21_hlx000
a21_hlx001
a21_hlx001_1
a21_hlx001_2
a21_hlx001_3
a21_hlx002
a21_hlx002_1
a21_hlx003
a21_hlx003_1
a21_hlx004_1
a21_memo01
a21_opt
a21_opt02
a21_opt03
a21_p01
a21_special
a21_tl9a
a21_todo
a21_txt01
a21_txt01_10
a21_txt01_1a
a21_txt01_2
a21_txt01_2a
a21_txt01_2b
a21_txt01_3
a21_txt01_4
a21_txt01_5
a21_txt01_6
a21_txt01_6a
a21_txt01_7
a21_txt01_8
a21_txt01_8a
a21_txt01_9
a21_txt01_9a
a21_txt02
a21_txt02_10
a21_txt02_10a
a21_txt02_10b
a21_txt02_11
a21_txt02_11a
a21_txt02_12
a21_txt02_12a
a21_txt02_12b
a21_txt02_1a
a21_txt02_1b
a21_txt02_2
a21_txt02_2a
a21_txt02_3
a21_txt02_3a
a21_txt02_4
a21_txt02_4a
a21_txt02_5
a21_txt02_5a
a21_txt02_6
a21_txt02_6a
a21_txt02_6b
a21_txt02_6b_rev0
a21_txt02_6x
a21_txt02_7
a21_txt02_7a
a21_txt02_8
a21_txt02_8a
a21_txt02_9
a21_txt02_9a
a22_acl2_01
a22_acl2_02
a22_edu12
a22_intro01
a22_intro02
a22_intro03
a22_memman01
a22_memman02
a22_memman03
a22_memman04
a22_memman05
a22_memman06
a22_memman07
a22_memo01
a22_mingw_debug
a22_txt03
a22_txt03_1a
a22_txt03_1b
a22_txt03_2
a22_txt03_2a
a22_ufcs01
a23_bbs
a23_ec001
a23_ec002
a23_intro00
a23_intro000
a23_intro01
a23_intro02
a23_intro03
a23_intro04
a23_intro05
a23_intro06
a23_intro07
a23_intro08
a23_intro09
a23_intro10
a23_intro10wk1
a23_intro10wk2
a23_intro10wk3
a23_intro11
a23_intro12
a23_intro13
a23_intro13wk1
a23_intro14
a23_intro15
a23_intro16
a23_intro17
a23_intro17wk1
a23_intro18
a23_intro19
a23_intro90
a23_intro91
a23_neopixel1
a23_os01
a23_useSelfMade
a23_usm001
a23_usm002
a23_usm003
a23_usm004
a23_usm005
a23_usm006
a23_usm007
a23_usm008
a23_usm009
a24_AMap11
a24_AMapSim11
a24_AMemFile
a24_AMemMan
a24_aErrExit
a24_aFnv
a24_aOsFunc
a24_aQSort
a24_aXorShift32
a24_acl1T_doc01
a24_acl1Tiny
a24_acpp0
a24_buntan01
a24_cMin
a24_getTyp
a24_goodvalues
a24_idea001
a24_longdef
a24_memo01
a24_memo02
a24_osc20240310
a24_osc20241026
a24_picoLcd13
a24_picoTrain1
a24_programs
a24_raspberrypi01
a24_raspberrypi02
a24_schedule
a24_spc2tab
a24_tab2spc
a24_useSelfMade
a25_acl3
a25_buntan02
a25_buntan03
a25_buntan04
a25_buntan05
a25_kcas01
a25_kharc01
a25_kharc02
a25_kharc03
a25_kharc04
a25_kharc05
a25_kharc06
a25_kharcs1
a25_kharcs2
a25_kharcs3
a25_kharcs4
a25_kharcs5
a25_kharcs6
a25_kharcs7
a25_kharcs8
a25_kharcs9
aclib00
aclib01
aclib02
aclib03
aclib04
aclib05
aclib06
aclib07
aclib08
aclib09
aclib10
aclib11
aclib12
aclib13
aclib14
aclib15
aclib16
aclib17
aclib18
aclib19
aclib20
aclib21
aclib22
aclib23
aclib24
aclib25
aclib_bbs
arm64_01
avm0001
edu0001
edu0002
edu0003
esb02b_hrb
esb_dbg
esbasic0001
esbasic0002
esbasic0003
esbasic0004
esbasic0005
esbasic0006
esbasic0007
esbasic0008
esbasic0009
esbasic0010
esbasic0011
esbasic0012
esbasic0013
esbasic0014
esbasic0015
esbasic0016
esbasic0017
esbasic02a
esc0001
escm0001
essen_hist
esvm0001
esvm0002
esvm0003
esvm0004
esvm0005
esvm0006
esvm_i0
hh4a
idea0001
idea0002
idea0003
impressions
jck_0000
jck_0001
kawai
kbcl0_0000
kbcl0_0001
kbcl0_0002
kbcl0_0003
kbcl0_0004
kbcl0_0005
kbcl0_0006
kbcl0_0007
kclib1_0000
kclib1_0001
kclib1_0002
kclib1_0003
kclib1_0004
kclib1_0005
kclib1_0006
kclib1_0007
kclib1_0008
kclib1_0009
kclib1_0010
kpap0001
members
memo0001
osask4g
osask4g_r2
p20200311a
p20200610a
p20200610b
p20200624a
p20200711a
p20200716a
p20250813a
p20250813b
p20250813c
p20250815a
p20250903a
p20251006a
page0001
page0002
page0003
page0004
page0005
page0006
page0007
page0008
page0009
page0010
page0011
page0012
page0013
page0014
page0015
page0016
page0017
page0018
page0019
page0020
page0021
page0022
page0023
populars
seccamp
seccamp2019
sechack
sechack2019
seclang01
sh3_2020
sh3_2020_kw
sh3_2020_nk
sh3_2021_kw
sh3_2021_nk
sh3_2022_kw
sh3_2023_kw
sh3_2024_kw
sh3_2025_kw
sh3_kw_hist
termux001
termux002
text0001
text0001a
text0002
text0002a
text0003
text0004
text0005
text0006
text0006a
text0007
text0008
text0010
text0011
text0012
text0013
text0014
text0015
text0016
text0017
text0018
text0019
text0020
text0021
tl1c
tl2c
tl3c
tl3d
* kharcs #4 -(by [[K]], 2025.06.15) ** (0) これはなに? -(kharcsページは、kharc開発でわかったことを整理して説明するためのページ群です。) -今回説明したいこと: ~この順番でCコンパイラを作ったら早くできたよ!~ --[1]~[6]→[[a25_kharcs2]] --[7]→[[a25_kharcs3]] --[8]最適化1(9日目) --[9]最適化2(10日目) --[10]最適化3、buntan-pcに挑戦(11日目) --[11]リファクタリング(12~14日目) --[12]最適化4、ベンチマーク(15日目) --[13]ポインタ実装1(16日目) --[14]ポインタ実装2(17日目) ** (8) 最適化1(9日目:6/14) -「1+a+2+b+3」という式があった時に、これを「a+b+6」にできたらいいなと考えました。 -加算だけではなく、乗算や除算や減算が混ざっていてもうまく定数項をまとめたいです。「1+a/2-3*4」→「a/2-11」。 -この日はSecHack365のオンラインイベントがあって、21時くらいまで開発はできませんでしたが、しかし若者が頑張っている様子を見ていたら自分も何かしたくなって、夜中に開発しました。 -途中でバグに悩まされたりすることもありましたが、最終的にはうまくできました。 -ということで、9日目の成果はこれだけです。・・・いや違った! -オンラインイベントの休み時間にちょっと試したことがあります。 -これまでkharcの関数呼び出しは、リターン先をRPレジスタに代入してjmpするという方法を取っていました。しかしx86にJITコンパイルする際に、これをx86のCALL(e8)/RET(c3)に翻訳することができないかというと、できなくはないのです。 -そして試しにやってみると、fib()のJITコンパイル後の実行速度が8%も短くなりました。おおおー。 ** (9) 最適化2(10日目:6/16) -今までたくさん生成されて、ややうんざりしていたムダ命令を、きれいに全部消すようにしました。 -最適化はkharcのバイナリを使って行いました。命令長が固定長なのでその方が作りやすいです。 -しかしこれだけだと、最適化を終えたコードを確認するのが大変なので、逆アセンブラも作りました。31行です。 -fib()でどのくらい改善したかを見るとこんな感じです。 LbI(9); // fib() SubAI(SP,24); StoAMd(RP,SP,20); LodRMd(R0,SP,24); CgtRI(R0,1); StoRMd(R0,SP,8); // 消される. LodRMd(R0,SP,8); // 消される. FreI(8); CmpJeqRII(R0,0,12); LodRMd(R0,SP,24); AddRI(R0,-2); StoRMd(R0,SP,8); // 消される. LodRMd(R0,SP,8); // 消される. FreI(8); StoRMd(R0,SP,0); CalII(9,10); FraI(8); StoRMd(R0,SP,8); LodRMd(R0,SP,24); AddRI(R0,-1); StoRMd(R0,SP,12); // 消される. LodRMd(R0,SP,12); // 消される. FreI(12); StoRMd(R0,SP,0); CalII(9,11); FraI(8); StoRMd(R0,SP,12); // 消される. LodRMd(R0,SP,12); // 消される. LodRMd(R1,SP,8); AddRR(R0,R1); FreR(R1); StoRMd(R0,SP,16); // 消される. FreI(8); FreI(12); LodRMd(R0,SP,16); // 消される. StoRMd(R0,SP,24); FreI(16); LbI(12); LodRMd(R0,SP,24); JmpI(13); LbI(13); // fib().ret: LodAMd(RP,SP,20); AddAI(SP,24); Ret(); -fib(46)の実行速度の改善はこんな感じです。 |mod=16|RIGHT:273.185[sec]|エミュレータ実行:最適化なし・ノーマルモード| |mod=17|RIGHT:221.202[sec]|エミュレータ実行:最適化なし・高速モード| |mod=18|RIGHT:14.931[sec]|x86JITコンパイラ実行:最適化なし・ノーマルモード| |mod=19|RIGHT:13.653[sec]|x86JITコンパイラ実行:最適化なし・e8/c3利用モード| |mod=0|RIGHT:241.998[sec]|エミュレータ実行:最適化あり・ノーマルモード| |mod=1|RIGHT:''165.459[sec]''|エミュレータ実行:最適化あり・高速モード| |mod=2|RIGHT:9.654[sec]|x86JITコンパイラ実行:最適化あり・ノーマルモード| |mod=3|RIGHT:''8.321[sec]''|x86JITコンパイラ実行:最適化あり・e8/c3利用モード| -JITコンパイラで8.3秒まで高速化できたので、とりあえず満足です! ** (10) 最適化3、buntan-pcに挑戦(11日目:6/17) -(9)の結果を見ると、 if (i > 1) { ... の部分のコンパイル結果が、 LodRMd(R0,SP,24); CgtRI(R0,1); CmpJeqRII(R0,0,12); -となっているわけですが、CgtRI()命令で比較式を評価して、それでその結果が==0か!=0かで分岐するというのは、全くうまくありません(動作としては正しいけど、遅い)。Cgt+CmpJeq=CmpJleなので、これを利用して1命令減らしたいです。 -でもそれをこのままやると、比較式の値を変数に代入して、さらにその値に応じて分岐したい場合にはうまくいかなくなりそうなので、CmpJcc命令の仕様を変更し、第一引数のレジスタを破壊する(かもしれない)という仕様にしました。こうすることで、もし比較式の値が必要なら、CmpJccより前に保存するようになります。そしてCgtとCmpJccの間にNop以外の命令が入っているなら、Cgt+CmpJeq=CmpJleの変換をしないことにします。 -これに合わせて命令名も変えました。CmpRIしてFreRしてJccIするので、CmFJccRIIです。 -fib(46)の実行速度の改善はこんな感じです。 |mod=16|RIGHT:256.992[sec]|エミュレータ実行:最適化なし・ノーマルモード| |mod=17|RIGHT:216.348[sec]|エミュレータ実行:最適化なし・高速モード| |mod=18|RIGHT:14.958[sec]|x86JITコンパイラ実行:最適化なし・ノーマルモード| |mod=19|RIGHT:13.648[sec]|x86JITコンパイラ実行:最適化なし・e8/c3利用モード| |mod=0|RIGHT:231.065[sec]|エミュレータ実行:最適化あり・ノーマルモード| |mod=1|RIGHT:''157.459[sec]''|エミュレータ実行:最適化あり・高速モード| |mod=2|RIGHT:8.345[sec]|x86JITコンパイラ実行:最適化あり・ノーマルモード| |mod=3|RIGHT:''8.085[sec]''|x86JITコンパイラ実行:最適化あり・e8/c3利用モード| ---- -buntan-pc用のアセンブラ出力に挑戦しました。 -以下のfib()をコンパイルして、標準のuccの出力と比較します。 int fib(int i) { if (i > 1) { i = fib(i - 2) + fib(i - 1); } return i; } -出力結果比較。左:kharcのCコンパイラkcc。右:buntan-pcの標準Cコンパイラucc。 L_0: fib: add fp,-10 add fp,-2 st fp+0 ld fp+10 push 1 push 1 ld fp+0 le lt not jz L_3 jz L_1 ld fp+10 ld fp+0 push 2 push 2 sub sub st fp+0 call L_0 call fib st fp+2 ld fp+10 ld fp+0 push 1 push 1 sub sub st fp+0 call L_0 call fib ld fp+2 add add st fp+10 st fp+0 L_3: L_1: ld fp+10 ld fp+0 add fp,10 add fp,2 ret ret -考察。 --(1)まずuccのほうが命令が少ないです。つまり速いです。ただし、これがいいかどうかは一概には言えません。 --kharcでは、関数の引数をすべてメモリに書き込んで渡します。したがってレジスタスタックを使い切ってしまう心配がありません。そのかわりメモリに書き込む分だけ命令数は増えます。 --また関数から戻ってきたときにkharcでは結果をローカル変数に保存しています。uccはレジスタスタックに積んだままにして、最後のaddでうまく計算しています。 --uccのやり方はレジスタスタックに十分な余裕があれば大変有効な方法ですが、そうでないならあふれる心配があります。 --ちなみにkharcでは、レジスタスタックは常に2段までしか使わないようにしています(どのくらい余裕があるのかわからなかったので)。 --(2)一方で、kharcがうまくいっていないところは、ローカル変数域として10バイトを要求しているところです。 --次の呼び出しのための2バイト、fib(i-2)の結果をしまっておくための2バイトは必須ですが、それなら合計4バイトであるべきです。 --じゃあ残りの6バイトは何なのかというと、2バイトがリターンアドレスをしまうための領域で、4バイトは一時変数のために確保したものの、結局最適化で使わなくなってしまったものです。 --(3)ちなみにkharcでは https://github.com/buntan-pc/buntan-pc/blob/main/cpu/cpu.sv#L50 に書いてある「binop uimm10」に期待していて、これが使えるならpush命令を減らせます。 --使い方がわかればすぐに対応できるようにしてあります。 --(4)レジスタマシンを想定したコンパイラがここまでスタックマシンに寄り添ったコード生成ができているということで、なかなか頑張ったと言えると自分では思っているのですが・・・。 ---- -ここまでの結果を踏まえて考えました。 -今のkccは内部でいきなりテキスト状態のアセンブラを出力しているのですが、このために文字列処理が必要になって、小回りが利きません。バイナリからテキストに変換するのはすごく簡単になっているので、バイナリで生成すべきです。 -また最適化するタイミングもよくないです。スタックフレームのサイズを決めてから最適化したら、こうやって無駄が出てしまいます。スタックフレームのサイズを決める前に最適化する必要があります。 -ということで、そろそろリファクタリングですかね・・・。 ** (11) リファクタリング(12~14日目) -12日目(6/18)はリファクタリングで、主に内部機械語の仕様を変更しました。 --変更前: struct KhaBinInst { unsigned char op, prm[3]; int imm; }; // 8バイト. --変更後: struct KhaBinInst { int op; unsigned char prm[4]; int imm[2]; }; // 16バイト. -こうすることで、Aux命令は不要になります(最適化の時にAux命令は不便だなと思ったのです)。 -命令番号も8bitに収めるために工夫するよりは余裕を持たせて楽をしようと考えました。 ---- -13日目(6/19)もリファクタリングです。今回のリファクタリング、ちょっといつもと違った方法でやっています。 -コンパイラはアセンブラを生成せず、いきなり機械語を生成するように書き換えています。しかし一気に全部書き換えが終わるまでテストランできないという状況になるのがなんだか嫌だなと思って、機械語を生成した場合はそれを逆アセンブルしてテキストに変換し、テキストで生成した場合はそのまま追記して、結局従来通りの出力をするという仕組みを作りました。こうすることで、段階的に移行できます。バグが大きくなる前に直せます。 -それで全部移行できたら、今度はテキストで保持する方法をやめることにして、機械語のまま処理するようにします。・・・とりあえずインラインアセンブラ以外の部分は全部移行できました。 ---- -14日目(6/20)もリファクタリングです。 -インラインアセンブラをどうするか悩みましたが、結局インラインアセンブラを使わなくてもプログラムを書けるように改良して、インラインアセンブラの実現方法は後日考えることにしました。 -インラインアセンブラで本当にアセンブラだけ書いてあるなら解決はできるのですが、kccではインラインアセンブラでC言語を書いたりすることもあるので、その対応に苦慮していたんです。 -残りの部分も組み立てて「コンパイラはバイナリを出力して、それを一度もテキストに変換することなく、バイナリのまま最適化してバイナリのまま実行するという仕組み」ができました。たぶんコンパイル速度も上がっています。 -ソースコードを出力する必要がある時は、逆アセンブルして出力します。 ** (12) 最適化4、ベンチマーク(15日目:6/23) -[最適化4] 最適化によって一度もアクセスしなくなった一時変数をスタック上からも削除する。 --ついにこれができるようになりました。だからbuntan-pc向けの出力もよくなりました! L_1: add fp,-4 ld fp+4 push 1 le not jz L_4 ld fp+4 push -2 add st fp+0 call L_1 st fp+2 ld fp+4 push -1 add st fp+0 call L_1 ld fp+2 add st fp+4 L_4: ld fp+4 add fp,4 ret --これで意図通りです! ---- -ベンチマークをしました。 --(1)fib46を普通のC言語で書いて、gcc -O3でビルドした場合。 --(2)fib46をkccで書いて、kccのvmd=4でhkarcのアセンブラを出力させ、これをgcc -O3でビルドした場合。 --(3)fib46をkccで書いて、kccのmod=3でJITコンパイル実行した場合。 --同様のことをmandelでも行いました。 --ちなみにどれも32bit版でやっています。 | |fib46 |mandel | |(1)|RIGHT:4.608[sec](1.00倍)|RIGHT:1.874[sec](1.00倍)| |(2)|RIGHT:7.279[sec](1.57倍)|RIGHT:3.373[sec](1.80倍)| |(3)|RIGHT:8.179[sec](1.77倍)|RIGHT:2.919[sec](1.56倍)| -これをみると、kcc用でプログラムを書いた場合、最初からgcc用に書いた場合と比べて1.5倍くらいの速度差があるとわかりました。 -つまりもし仮にkccにgccを超えるような便利機能を入れたとして、kccを普段使いのコンパイラにすると、実行速度では1.5倍くらい遅くなるというわけです。 -これを気にならない差だと思うなら、kccの中で暮らせるということになります。 -うーん、もうちょっと速くなって、1.2倍くらいで収まってくれればなあ・・・。 ** (13) ポインタ実装1(16日目:6/24) -ポインタの実装を始めました。今日はここまでできるようになりました。 #include "kharc.h" int main() { int i, *p; p = &i; i=12345678; *p = 123; return i; } -ちゃんと123が返ってきます。 ** (14) ポインタ実装2(17日目:6/25) -ポインタの実装を進めて、配列が使えるようになりました。まだ完ぺきではないですが、以下のコードくらいなら動くようになりました! #include "kharc.h" int main() { int i, a[11]; a[0] = 0; a[1] = 1; for (i = 2; i < 11; i++) { a[i] = a[i - 2] + a[i - 1]; } i = a[10]; return i; }
タイムスタンプを変更しない
* kharcs #4 -(by [[K]], 2025.06.15) ** (0) これはなに? -(kharcsページは、kharc開発でわかったことを整理して説明するためのページ群です。) -今回説明したいこと: ~この順番でCコンパイラを作ったら早くできたよ!~ --[1]~[6]→[[a25_kharcs2]] --[7]→[[a25_kharcs3]] --[8]最適化1(9日目) --[9]最適化2(10日目) --[10]最適化3、buntan-pcに挑戦(11日目) --[11]リファクタリング(12~14日目) --[12]最適化4、ベンチマーク(15日目) --[13]ポインタ実装1(16日目) --[14]ポインタ実装2(17日目) ** (8) 最適化1(9日目:6/14) -「1+a+2+b+3」という式があった時に、これを「a+b+6」にできたらいいなと考えました。 -加算だけではなく、乗算や除算や減算が混ざっていてもうまく定数項をまとめたいです。「1+a/2-3*4」→「a/2-11」。 -この日はSecHack365のオンラインイベントがあって、21時くらいまで開発はできませんでしたが、しかし若者が頑張っている様子を見ていたら自分も何かしたくなって、夜中に開発しました。 -途中でバグに悩まされたりすることもありましたが、最終的にはうまくできました。 -ということで、9日目の成果はこれだけです。・・・いや違った! -オンラインイベントの休み時間にちょっと試したことがあります。 -これまでkharcの関数呼び出しは、リターン先をRPレジスタに代入してjmpするという方法を取っていました。しかしx86にJITコンパイルする際に、これをx86のCALL(e8)/RET(c3)に翻訳することができないかというと、できなくはないのです。 -そして試しにやってみると、fib()のJITコンパイル後の実行速度が8%も短くなりました。おおおー。 ** (9) 最適化2(10日目:6/16) -今までたくさん生成されて、ややうんざりしていたムダ命令を、きれいに全部消すようにしました。 -最適化はkharcのバイナリを使って行いました。命令長が固定長なのでその方が作りやすいです。 -しかしこれだけだと、最適化を終えたコードを確認するのが大変なので、逆アセンブラも作りました。31行です。 -fib()でどのくらい改善したかを見るとこんな感じです。 LbI(9); // fib() SubAI(SP,24); StoAMd(RP,SP,20); LodRMd(R0,SP,24); CgtRI(R0,1); StoRMd(R0,SP,8); // 消される. LodRMd(R0,SP,8); // 消される. FreI(8); CmpJeqRII(R0,0,12); LodRMd(R0,SP,24); AddRI(R0,-2); StoRMd(R0,SP,8); // 消される. LodRMd(R0,SP,8); // 消される. FreI(8); StoRMd(R0,SP,0); CalII(9,10); FraI(8); StoRMd(R0,SP,8); LodRMd(R0,SP,24); AddRI(R0,-1); StoRMd(R0,SP,12); // 消される. LodRMd(R0,SP,12); // 消される. FreI(12); StoRMd(R0,SP,0); CalII(9,11); FraI(8); StoRMd(R0,SP,12); // 消される. LodRMd(R0,SP,12); // 消される. LodRMd(R1,SP,8); AddRR(R0,R1); FreR(R1); StoRMd(R0,SP,16); // 消される. FreI(8); FreI(12); LodRMd(R0,SP,16); // 消される. StoRMd(R0,SP,24); FreI(16); LbI(12); LodRMd(R0,SP,24); JmpI(13); LbI(13); // fib().ret: LodAMd(RP,SP,20); AddAI(SP,24); Ret(); -fib(46)の実行速度の改善はこんな感じです。 |mod=16|RIGHT:273.185[sec]|エミュレータ実行:最適化なし・ノーマルモード| |mod=17|RIGHT:221.202[sec]|エミュレータ実行:最適化なし・高速モード| |mod=18|RIGHT:14.931[sec]|x86JITコンパイラ実行:最適化なし・ノーマルモード| |mod=19|RIGHT:13.653[sec]|x86JITコンパイラ実行:最適化なし・e8/c3利用モード| |mod=0|RIGHT:241.998[sec]|エミュレータ実行:最適化あり・ノーマルモード| |mod=1|RIGHT:''165.459[sec]''|エミュレータ実行:最適化あり・高速モード| |mod=2|RIGHT:9.654[sec]|x86JITコンパイラ実行:最適化あり・ノーマルモード| |mod=3|RIGHT:''8.321[sec]''|x86JITコンパイラ実行:最適化あり・e8/c3利用モード| -JITコンパイラで8.3秒まで高速化できたので、とりあえず満足です! ** (10) 最適化3、buntan-pcに挑戦(11日目:6/17) -(9)の結果を見ると、 if (i > 1) { ... の部分のコンパイル結果が、 LodRMd(R0,SP,24); CgtRI(R0,1); CmpJeqRII(R0,0,12); -となっているわけですが、CgtRI()命令で比較式を評価して、それでその結果が==0か!=0かで分岐するというのは、全くうまくありません(動作としては正しいけど、遅い)。Cgt+CmpJeq=CmpJleなので、これを利用して1命令減らしたいです。 -でもそれをこのままやると、比較式の値を変数に代入して、さらにその値に応じて分岐したい場合にはうまくいかなくなりそうなので、CmpJcc命令の仕様を変更し、第一引数のレジスタを破壊する(かもしれない)という仕様にしました。こうすることで、もし比較式の値が必要なら、CmpJccより前に保存するようになります。そしてCgtとCmpJccの間にNop以外の命令が入っているなら、Cgt+CmpJeq=CmpJleの変換をしないことにします。 -これに合わせて命令名も変えました。CmpRIしてFreRしてJccIするので、CmFJccRIIです。 -fib(46)の実行速度の改善はこんな感じです。 |mod=16|RIGHT:256.992[sec]|エミュレータ実行:最適化なし・ノーマルモード| |mod=17|RIGHT:216.348[sec]|エミュレータ実行:最適化なし・高速モード| |mod=18|RIGHT:14.958[sec]|x86JITコンパイラ実行:最適化なし・ノーマルモード| |mod=19|RIGHT:13.648[sec]|x86JITコンパイラ実行:最適化なし・e8/c3利用モード| |mod=0|RIGHT:231.065[sec]|エミュレータ実行:最適化あり・ノーマルモード| |mod=1|RIGHT:''157.459[sec]''|エミュレータ実行:最適化あり・高速モード| |mod=2|RIGHT:8.345[sec]|x86JITコンパイラ実行:最適化あり・ノーマルモード| |mod=3|RIGHT:''8.085[sec]''|x86JITコンパイラ実行:最適化あり・e8/c3利用モード| ---- -buntan-pc用のアセンブラ出力に挑戦しました。 -以下のfib()をコンパイルして、標準のuccの出力と比較します。 int fib(int i) { if (i > 1) { i = fib(i - 2) + fib(i - 1); } return i; } -出力結果比較。左:kharcのCコンパイラkcc。右:buntan-pcの標準Cコンパイラucc。 L_0: fib: add fp,-10 add fp,-2 st fp+0 ld fp+10 push 1 push 1 ld fp+0 le lt not jz L_3 jz L_1 ld fp+10 ld fp+0 push 2 push 2 sub sub st fp+0 call L_0 call fib st fp+2 ld fp+10 ld fp+0 push 1 push 1 sub sub st fp+0 call L_0 call fib ld fp+2 add add st fp+10 st fp+0 L_3: L_1: ld fp+10 ld fp+0 add fp,10 add fp,2 ret ret -考察。 --(1)まずuccのほうが命令が少ないです。つまり速いです。ただし、これがいいかどうかは一概には言えません。 --kharcでは、関数の引数をすべてメモリに書き込んで渡します。したがってレジスタスタックを使い切ってしまう心配がありません。そのかわりメモリに書き込む分だけ命令数は増えます。 --また関数から戻ってきたときにkharcでは結果をローカル変数に保存しています。uccはレジスタスタックに積んだままにして、最後のaddでうまく計算しています。 --uccのやり方はレジスタスタックに十分な余裕があれば大変有効な方法ですが、そうでないならあふれる心配があります。 --ちなみにkharcでは、レジスタスタックは常に2段までしか使わないようにしています(どのくらい余裕があるのかわからなかったので)。 --(2)一方で、kharcがうまくいっていないところは、ローカル変数域として10バイトを要求しているところです。 --次の呼び出しのための2バイト、fib(i-2)の結果をしまっておくための2バイトは必須ですが、それなら合計4バイトであるべきです。 --じゃあ残りの6バイトは何なのかというと、2バイトがリターンアドレスをしまうための領域で、4バイトは一時変数のために確保したものの、結局最適化で使わなくなってしまったものです。 --(3)ちなみにkharcでは https://github.com/buntan-pc/buntan-pc/blob/main/cpu/cpu.sv#L50 に書いてある「binop uimm10」に期待していて、これが使えるならpush命令を減らせます。 --使い方がわかればすぐに対応できるようにしてあります。 --(4)レジスタマシンを想定したコンパイラがここまでスタックマシンに寄り添ったコード生成ができているということで、なかなか頑張ったと言えると自分では思っているのですが・・・。 ---- -ここまでの結果を踏まえて考えました。 -今のkccは内部でいきなりテキスト状態のアセンブラを出力しているのですが、このために文字列処理が必要になって、小回りが利きません。バイナリからテキストに変換するのはすごく簡単になっているので、バイナリで生成すべきです。 -また最適化するタイミングもよくないです。スタックフレームのサイズを決めてから最適化したら、こうやって無駄が出てしまいます。スタックフレームのサイズを決める前に最適化する必要があります。 -ということで、そろそろリファクタリングですかね・・・。 ** (11) リファクタリング(12~14日目) -12日目(6/18)はリファクタリングで、主に内部機械語の仕様を変更しました。 --変更前: struct KhaBinInst { unsigned char op, prm[3]; int imm; }; // 8バイト. --変更後: struct KhaBinInst { int op; unsigned char prm[4]; int imm[2]; }; // 16バイト. -こうすることで、Aux命令は不要になります(最適化の時にAux命令は不便だなと思ったのです)。 -命令番号も8bitに収めるために工夫するよりは余裕を持たせて楽をしようと考えました。 ---- -13日目(6/19)もリファクタリングです。今回のリファクタリング、ちょっといつもと違った方法でやっています。 -コンパイラはアセンブラを生成せず、いきなり機械語を生成するように書き換えています。しかし一気に全部書き換えが終わるまでテストランできないという状況になるのがなんだか嫌だなと思って、機械語を生成した場合はそれを逆アセンブルしてテキストに変換し、テキストで生成した場合はそのまま追記して、結局従来通りの出力をするという仕組みを作りました。こうすることで、段階的に移行できます。バグが大きくなる前に直せます。 -それで全部移行できたら、今度はテキストで保持する方法をやめることにして、機械語のまま処理するようにします。・・・とりあえずインラインアセンブラ以外の部分は全部移行できました。 ---- -14日目(6/20)もリファクタリングです。 -インラインアセンブラをどうするか悩みましたが、結局インラインアセンブラを使わなくてもプログラムを書けるように改良して、インラインアセンブラの実現方法は後日考えることにしました。 -インラインアセンブラで本当にアセンブラだけ書いてあるなら解決はできるのですが、kccではインラインアセンブラでC言語を書いたりすることもあるので、その対応に苦慮していたんです。 -残りの部分も組み立てて「コンパイラはバイナリを出力して、それを一度もテキストに変換することなく、バイナリのまま最適化してバイナリのまま実行するという仕組み」ができました。たぶんコンパイル速度も上がっています。 -ソースコードを出力する必要がある時は、逆アセンブルして出力します。 ** (12) 最適化4、ベンチマーク(15日目:6/23) -[最適化4] 最適化によって一度もアクセスしなくなった一時変数をスタック上からも削除する。 --ついにこれができるようになりました。だからbuntan-pc向けの出力もよくなりました! L_1: add fp,-4 ld fp+4 push 1 le not jz L_4 ld fp+4 push -2 add st fp+0 call L_1 st fp+2 ld fp+4 push -1 add st fp+0 call L_1 ld fp+2 add st fp+4 L_4: ld fp+4 add fp,4 ret --これで意図通りです! ---- -ベンチマークをしました。 --(1)fib46を普通のC言語で書いて、gcc -O3でビルドした場合。 --(2)fib46をkccで書いて、kccのvmd=4でhkarcのアセンブラを出力させ、これをgcc -O3でビルドした場合。 --(3)fib46をkccで書いて、kccのmod=3でJITコンパイル実行した場合。 --同様のことをmandelでも行いました。 --ちなみにどれも32bit版でやっています。 | |fib46 |mandel | |(1)|RIGHT:4.608[sec](1.00倍)|RIGHT:1.874[sec](1.00倍)| |(2)|RIGHT:7.279[sec](1.57倍)|RIGHT:3.373[sec](1.80倍)| |(3)|RIGHT:8.179[sec](1.77倍)|RIGHT:2.919[sec](1.56倍)| -これをみると、kcc用でプログラムを書いた場合、最初からgcc用に書いた場合と比べて1.5倍くらいの速度差があるとわかりました。 -つまりもし仮にkccにgccを超えるような便利機能を入れたとして、kccを普段使いのコンパイラにすると、実行速度では1.5倍くらい遅くなるというわけです。 -これを気にならない差だと思うなら、kccの中で暮らせるということになります。 -うーん、もうちょっと速くなって、1.2倍くらいで収まってくれればなあ・・・。 ** (13) ポインタ実装1(16日目:6/24) -ポインタの実装を始めました。今日はここまでできるようになりました。 #include "kharc.h" int main() { int i, *p; p = &i; i=12345678; *p = 123; return i; } -ちゃんと123が返ってきます。 ** (14) ポインタ実装2(17日目:6/25) -ポインタの実装を進めて、配列が使えるようになりました。まだ完ぺきではないですが、以下のコードくらいなら動くようになりました! #include "kharc.h" int main() { int i, a[11]; a[0] = 0; a[1] = 1; for (i = 2; i < 11; i++) { a[i] = a[i - 2] + a[i - 1]; } i = a[10]; return i; }
テキスト整形のルールを表示する