はじめに
「X68000 Z」が話題になったようなので、当時の最適化で気にしていた点をいくつか思い出してみた。
sin, cos
当時は浮動小数点演算はコプロセッサで実装されてたが、残念ながらコプロセッサは標準搭載ではなかった。
sinとかcosを使う必要があるたびに値を求めていたのでは時間がかかる。
そこで、最初から一周分のsinとかcosを求めておいてテーブルを作っておく。
値が必要ならテーブルから読み出すだけだから処理が省ける。
そして、角度を常にバイトとして扱い、どう加減算してもテーブルの範囲外を指さないように構成すると、いろいろ省けてお買い得だ。
固定小数点
そもそも浮動小数点の扱いが重いから避けたいという話になる。
そこで固定小数点を採用する。
整数部と小数部を合わせて32ビットあれば対応できることも多い。
もし数値の範囲が狭くて荒い精度で良いなら、合計16ビットでも問題ない場合がある。
ループ
例えば、処理を16回繰り返したい場合、次のように書くのは最速ではない。
int i;
for(i = 0; i < 16; i++)
{
// ループの中身
}
コンパイルするとdbra命令が使われるように書くと速い。ループカウンタの操作/条件分岐が1命令になるので。
更に言うと、可能であれば処理を必要回数だけ並べたほうが速い。ループカウンタの操作/条件分岐が不要になるので。
当時は命令プリフェッチ程度はあったかもしれないがキャッシュはなかったので、キャッシュに納めることを考える必要はない。
スーパーバイザモード
モトローラの68000には非常に単純なメモリ保護があった。
ユーザーモードとスーパーバイザモードがあり、重要な領域は保護されており、スーパーバイザモードではアクセスできてユーザーモードではアクセスできなかった。
VRAMなどにアクセスすることは多いと思うが、それは保護されてる領域へのアクセスとなる。
モード移行のオーバーヘッドを省くため、最初からスーパーバイザモードに移行しておき、スーパーバイザモードのまま走り抜ける場合もあった。
行儀の良い振る舞いではないが、速度を稼ぐためには必要だった。
掛け算
モトローラの68000には掛け算命令があったが、あまり賢くない実装だったようで速くなかった。
速度が重要なら掛け算命令を使わずにシフトなどを組み合わせたりしてた。
なお、割り算も同様である。
16ビット
データレジスタもアドレスレジスタも32ビット幅なのでコードを書く側には32ビットCPUに見えるが、データの処理単位は16ビットだ。
だから、16ビットで収まるデータを扱ってるなら16ビットで計算したほうが速い。
Cで書くなら、速度が重要でないならintやunsigned intで構わないが、速度が重要ならshort intやunsigned short intを使う。
コンパイラ
当時のコンパイラの最適化技術は今ほど賢くなかった。
速度が重要ならコンパイルされた結果を意識してコードを書いてた。
コードの意図がコンパイラに伝わらず最適化が甘い場合、インラインアセンブラを使う場合もあった。
いまでも
いくつかは、いまでも同じ考え方が適用できる。
その処理はそこに必要なのか? 事前に処理して結果のテーブルを用意したほうが良くないか?
素直で分かりやすいコードは良い。だが、そこは速度より分かりやすさが必要な場面か? 別の方法を使うとわかりにくさと引き換えに速度が得られたりしないか?
そのオーバーヘッドは許容できるか? 省略できないか? もし省略するなら副作用を許容できるか?
できるだけ幅広く状況に応じて適切に判断することを心がけたいと思う。