* buntan-pc #5 -(by [[K]], 2025.06.11) ** (0) -https://github.com/buntan-pc/ -これにかかわる開発の話 ** 2025.06.13 Fri #0 -buntan-pcのCPUについて勉強したいっ! -[Q]っていうか今まで、分からないままアセンブラ作ってたの? -[A]ええそうなんです。仕様書やサンプルのアセンブラがよくできていて、CPUに対する理解が浅くてもアセンブラを作れてしまったのです・・・。 -まずここを見る。 https://github.com/buntan-pc/buntan-pc/blob/main/cpu/cpu.sv -たぶんfpがフレームポインタでローカル変数を置けばいいと思う。 -gpはグローバル変数を置けばいいと思う。 -ということは、kharcをこのCPUでやるとしたらこうかな? R0:[gp+0], R1:[gp+2], R2:[gp+4], R3:[gp+6], A0:[gp+8], A1:[gp+10], A2:[gp+12], RP:[gp+14], SP:fp -そうすると、こうすればいいかな? LodRMd(R0,SP,8); → LD fp+8; ST gp+0; StoRMd(R1,SP,12); → LD gp+2; ST fp+12; AddRI(R2,3); → LD gp+4; PUSH 3; ADD; ST gp+4; CmpJneRII(R0,0,lb); → LD gp+0; NOT; JZ xxx; -[Q]x86と違って、kharcの1命令がbuntan-pcの2命令とかになっているけど、それはいいの? -[A]スタックマシンなのでしょうがない。たとえ命令数が2倍でその分遅くなったとしても、kharcが動かせることが大事だって言われるようになりたい。 -まあレジスタマシンで32bitのやつがあればいいなーとは思う・・・。 ** 2025.06.14 Sat #0 -[Q]スタックトップの値をR0にすればいいんじゃないの? -[A]それは一案なのだけど、もしR0の値をどこかにSTしたら、R0はスタックからなくなってしまう。それだと困るので、DUPしてからSTすることになる。そうすると命令数は全く減らない・・・。 -[Q]いやでもさ、R0をどこかに書き込む命令があったとして、その直後にR0へのロード命令があるかどうかを確認して、もしそうならDUPを省略するようにすればいいじゃないか。これでbuntan-pcでも効率よく動かせるんじゃない? -[A]それもそうだなあ。じゃあfibどうなるかやってみよう。 -以下ではintのサイズが4バイトのままになっているけど、これはもちろん本番では2に直すとする。 LbI(9); // fib() SubAI(SP,24); ADD fp,-24; StoAMd(RP,SP,20); (なし) // あえてRPの管理をしないで、CPUの機能に任せるモードを作る。このモードの時はsetjmp/longjmp的なことはしない想定。 LodRMd(R0,SP,24); LD fp+24; CgtRI(R0,1); PUSH 1; LE; NOT; // GT命令がないので、LE+NOTにした。 StoRMd(R0,SP,8); (無視) // ムダ命令なので無視 LodRMd(R0,SP,8); (無視) FreI(8); (無視) CmpJeqRII(R0,0,12); NOT; JZ xxx; // これはNOTが連続しているので、あとで最適化でNOTを相殺させよう。そして直後にR0へのLodがあるので、DUPは入れない。 LodRMd(R0,SP,24); LD fp+24; SubRI(R0,2); PUSH 2; SUB; StoRMd(R0,SP,8); (無視) LodRMd(R0,SP,8); (無視) FreI(8); (無視) StoRMd(R0,SP,0); ST fp+0; // これはDUPするべきか迷うけど、Calの直前もDUPしないということにしよう。 CalII(9,10); CALL xxx; StoRMd(R0,SP,8); ST fp+8; // 次にR0へのLodがあるので、DUPしない。 LodRMd(R0,SP,24); LD fp+24; SubRI(R0,1); PUSH 1; SUB; // INCやINC2があるのに、DECはないんだなー。 StoRMd(R0,SP,12); (無視) LodRMd(R0,SP,12); (無視) FreI(12); (無視) StoRMd(R0,SP,0); ST fp+0; // Cal直前なのでDUPしない。 CalII(9,11); CALL xxx; StoRMd(R0,SP,12); ST fp+12; // 次にR0へのLodがあるので、DUPしない。 LodRMd(R0,SP,8); LD fp+8; LodRMd(R1,SP,12); LD fp+12; ST gp+2; AddRR(R0,R1); LD gp+2; ADD; // この時、ST gp+12; LD gp+12; のペアが無駄だと気づけるとよい。FreR(R1);があるなら最適化できるなあ。これは対応しよう。 StoRMd(R0,SP,16); (無視) FreI(8); (無視) FreI(12); (無視) LodRMd(R0,SP,16); (無視) StoRMd(R0,SP,24); ST fp+24; // 次にR0へのLodがあるので、DUPしない。 FreI(16); (無視) LbI(12); LodRMd(R0,SP,24); LD fp+24; JmpI(13); (無視) LbI(13); // fib().ret: (無視) LodAMd(RP,SP,20); (なし) AddAI(SP,24); ADD fp,24 Ret(); RET -なんとこのルールなら無駄なDUPを生成することは一度もなかった! kharcはスタックマシンに対しても有用ってことかー。最強じゃないか!! -uchanさんのCコンパイラでコード生成したときと大差ない気がする。 -これ書いてわかった。Cal/RetでRPを使わないモードにするなら、JmpIをCal由来かそうではないか区別できるべきだ。いやでもRPへの代入直後のJmpはCal由来っていう判定基準で十分なのでは? -そうだな、それで十分だ。これに従わないのなら、CPUの機能に任せるモードを使わないことにすればいい。 -ポインタとか使い始めてA0とかA1とかが出てきたら大変なことになるかもしれないけど、それはその時に考えよう。何かいい案を思いつけるかもしれない。 -GTやGEがないのは不便そうだなーって思ったけど、直前2つのPUSHやLDを入れ替えればLT←→GT、LE←→GEにできるから、問題ないってことかー。なるほど。 -JNZがなくても平気っていうのもたぶん同じ理由だな。 -JNZがなくても平気っていうのもたぶん同じ理由だな。・・・いやそんなことない。左辺と右辺を入れかえても、JNZはJZにはならないぞ?直前の比較を反転させる必要があるのかー。 -今頃になって気づいたけど、fib(20)ってやったら、fib(i-1)の項のせいで、最大で20個のコールスタックが必要になるのかな?(厳密には19とか18とかかもしれないけど)。buntan-pcは耐えられるだろうか・・・。まあ再帰fibをやらなければいいだけなんだけど。 -x86に対してもRP使わないモードっていうのはありだと思う。それで高速化できる可能性はある。 ** 2025.06.14 Sat #1 -これのbuntan-pc用のアセンブラ出力を作ってuchanさんに相談すればよさそう。「これくらいの性能のCコンパイラ作ったんだけど、どうかな?」って。 int main() { return fib(10); } int fib(int i) { if (i > 1) { i = fib(i - 2) + fib(i - 1); } return i; } -余裕だぜ! -x86のJITコンパイラでRPを使わないでCALL(e8)/RET(c3)で実行させたら、8%くらい速くなったー。fib()ではCALL/RETの実行時間が占める割合は大きいので、こんなにも差が出るのかー。 -uchanさんにアピールすることを考えると、まずやるべきは最適化じゃないかな。余計な記述を消して、こんな品質のコード生成ができるよ、って言うのが先だよね。そのあとでポインタとか配列とか構造体とかやればいい。 -uccはまだ構造体をサポートしてないっぽいので、それをやれば採用確率が上がりそう。