ここんところ、ずーっと気になっていたこと……。
それは自作のZ80エミュレータの速度が遅い!…という事(^^;
過去のブログでも嘆いているくらいなので、結構気にしてる!(=_=;
この処理が重いの考え方はいろいろあると思うけど、今感じてるのは「自分が想定している以上の速度低下がある」で、それが理解出来ないレベルにまで達しているのだ(T-T)
私が使っているZ80エミュレータコアは、私自身が1990年前半にC言語で書いたものが原型となっていて、それをずっとメンテナンスするカタチで使っている。遅いのは紛れもなく自分自身のプログラムが悪いはずだ(ToT)
ここ数年で書いた6809CPUエミュレータはARM CPUに特化してカリッカリにチューニングしてあるおかげでかなりパフォーマンスが出るようになっている。それに比較する以上にZ80が遅いなぁ…とか思っていた。
今回は意を決してZ80エミュレータプログラムを作り直したお話(^-^)
CPUエミュレーション
CPUのエミュレータと聞くと身構えてしまう人がいるかも知れないが、実際にはとても単純な構造でできている。構成としては主に3つかなぁと思う。
エミュレータ内で扱えるメモリ
ハードウェアとのやりとりをするI/O
CPU内部にはレジスタと呼ばれるとてもとても小さな変数みたいなものが存在する。これはCPUによって違うため、CPUが変わるごとにエミュレータプログラムを作る事になる。
Z80の場合は上の写真のような構成になっている。
これをプログラムで表現しようとする場合…
例えばAレジスタは8ビット長なので、
uint8_t A;
なんてすれば仮想的なレジスタが定義出来る。
ただZ80には「8ビット長のAレジスタとFレジスタをくっつけて、16ビット長のAFレジスタとして扱うことができる」というレジスタペアと呼ばれる面倒な仕組みがある。
C言語には共用体という仕組みがあり、コレを利用すればレジスタペアは簡単に実現出来る。
構造体は理解しているけど共用体は知らない…という人は、まずここで躓く(^^;;
Z80で面白いなぁ…と思うのは、裏レジスタというレジスタセットがもう1つある事。
メモリよりも高速にアクセス出来るものなので、たくさんある方がプログラムのパフォーマンスが上がりやすい、とても便利なのだ。でもワリと存在自体を忘れがちで、普通にプログラミングしているとほぼ使わない。使う人と使わない人が二分するような気がする(^^)
エミュレータ内で扱えるメモリ
エミュレーションするCPUが使えるメモリを管理する必要がある。Z80の場合は64KBのメモリ空間があり、メモリマップはエミュレーションするマシンに依存する。
このため、メモリアクセス関数はマシンにあわせて作り直す構造にしておく必要がある。
MSXの場合はスロット切り替え、PC-8801ならばバンク切り替えなどに対応した、メモリアクセス関数を用意する必要があるだろう。
私は1バイト読み込み、1バイト書き込みという2つの関数を別途用意して、これらを利用してエミュレータのメモリへアクセスするようにしている。
速度は速くないがエミュレータの汎用性は保たれる。
ハードウェアとのやりとりをするI/O
Z80はI/Oポートを介して外部のハードウェアとやりとりする仕組みが備わっている。キーボードとかメモリのバンク切り替えとかFDDとか、用途はさまざまだ。
しかもマシンごとにすべて違っている(もっと言えばシリーズ別で違ってたりもする)ので、これもプログラム的には汎用性をもたせる必要がある。
エミュレータを作ろうと思った時、CPUのエミュレータを実装するのは簡単なのだが、ハードウェアのエミュレーションを作るのがとても大変だ!エミュレータを作る上での敷居の高さは、95%くらいがこっちの難易度だと思う(T-T)
利用してるZ80エミュレータ
上にも書いた通り、今まで使っていたZ80のエミュレータは1990年代前半に設計したものだ。当時は68030CPUで動くワークステーション上で開発していた。この頃は「確実に動くもの」を目指していて、実行速度は特に気にしていなかった。
それにしても……自分が想像する以上に遅いのがとても気になっていた。プログラマは自分が書いたコードの実行速度は想像が出来る。しかしこのエミュレータはどう考えても遅い。なんでだ??(T-T)
そう思って、ながーい事見ていなかった自分のコードを見てみる事に…。そしたら……なんと人の手が加わっていた!しかも実行効率がより悪くなるコードが入っていて、これが想定していない速度低下の原因だと分かった(ToT)
過去にもプログラムを6行追加されただけで速度が1/3まで落ちるという経験をした事があり、その時にはキャッシュメモリというものを理解していない人が組むとこうなるのか…と思い知らされた(^^;
その部分を修正する事で速度低下は回復するだろうな…とは思ったものの、30年くらい使い続けたプログラムを一新するチャンスでもあるな…と思ったので、作り直しを決断!
新エミュレータの実装
新しいエミュレータを書く上で、いくつか気にした事があった。
プログラムサイズをコンパクトに!
昨今ではプログラムがキャッシュメモリに入るかどうかが大きなファクターとなる事が多く、複雑なプログラムであっても全体的に見てサイズが小さい方が速くなるケースがあり得る。
特に今回のエミュレータは1つ1つの関数が極単純なので、出来る限りプログラムをまとめて小さくする努力をした。ちなみに6809CPUエミュレータもサイズをコンパクトにする事で、大幅な高速化に成功した経験がある。
汎用性を犠牲!
PC-8801のようにCPUが複数入ってるマシンがあるので、プログラム中にCPUが1つという限定をせずに動かすようにしていた。しかし構造体のメンバをアクセスするアドレッシングが足を引っ張るため、少々汚いがグローバル変数をアクセスするようにしてリンク時にアドレスを確定するようにした。
CPUが複数ある場合は、グローバル変数に情報をコピーした上でエミュレーションを動かすようにすれば良い。汚い実装だけどここは速度優先だ(T-T)
プログラム実行をRAM上で!
これはRaspberryPi Picoに特化した話だが、プログラムをRAMで実行するようにした。
デフォルトのPicoプログラムはFlash上で実行される。Flashからプログラムを読み込まれる際、どうしても時間がかかってしまうはずなので、おそらく少なからずボトルネックになっているのでは?と思ったからだ。
しかし実際に試してみると、実行速度はほとんど変わらない事が分かった(ToT)
ボトルネックは別にあるって事なんだと思われる。
今は入れてないけれども…
6809エミュレータを書いた際、likely / unlikelyを入れる事でパフォーマンスを上げる事に成功していた。RP2040のコンパイラで有効かどうか不明だが、後ほどコードが固まってきたら入れていきたい(^^)
現状の結果
今の時点でのパフォーマンスだけど、大体2.6倍くらいの速度アップになった!
→
携帯のストップウォッチで測ってるので正確ではないが、大雑把な数字としてもかなりの違いとなった!
これは嬉しい!(^o^)/
何度も作ってみても毎回思うのだが、エミュレータの開発には根気が必要だ。
ずーっと画面に何も出ない状態でコードを書き続ける必要があり、かつ書くコードは単純なモノばかり。何かまとまったものが動くようになるまでひたすら頑張り続けるのだ(^^;
今回は慣れたZ80の実装だったので丸1日頑張ったら動くようになってきたが、一度も使ったことがないCPUだったら数日は掛かる気がする…。
今はまだ8080+αくらいの機能しか入っていないので、これをキレイにZ80まで仕上げていきたい(^^)
ではまた次回!(^-^)ノ
2020.03.25 追記
RAM上でプログラムを実行したけど速度が変わらなかった件。
もしかしたらプログラム自体がCPUのキャッシュに収まるサイズなのかも知れない。気になってmapファイルを見て見たが、エミュレータ全体でも32KBを超えていなかった。
なので「速くならない」のではなく「遅くなりにくい」だけなのかも知れないです!
もひとつ(^^)
これを見ると1分26秒という速さはZ80の11〜12MHzくらいの速度になるみたい。RaspberryPi Picoを120MHzで動作させている事を鑑みると、1/10の速度はワリと優秀?
コレ以上の大幅アップはありえないのかもなーと思う。
↑ちなみにPicoを200MHzで動作させてみると、ほぼきっちりクロック数分だけ速度が上がった!(^-^) 120MHzで1分切るのは夢ですねー(^^;;