昔書いたプログラムを書き直す

MMX-Pentuim時代に書いた、ゲーム用画面表示ルーチンを書き直している。


MMXでCPUにSIMDが搭載されたあの時期、ゲーム画面表示のトレンドといえば半透明処理だった。
コンシューマゲーム機ではVDP(GPU)で半透明ポリゴンを矩形で描画することで楽々使えるものも、PCでやるとなると非常に苦しく重たい処理になる。
当時のGPUは2Dベースのものが主流。
DirectXの2D画面で、GPUのハードウェア描画コマンドによるキャラクタの描画はできても、そのキャラクタを半透明にすることはできなかった。
ということで、半透明処理をCPUでプログラム処理してやることになるわけだが、不透明/半透明キャラを混在させるとなるとCPU/GPUの処理を分割することが極めてむつかしくなり、結局全部CPUで処理する羽目になりキモになる処理はすべてアセンブラで書いた。


別にアセンブラ自体は好きなんで苦痛はないが時間がかかる。すくなくとも三倍はかかる。
また書き方次第はでは遅くなる。
PC98時代のi386sxでアセンブラ技術を培った私の書き方では、今から考えると当時のMMX-Pentiumにはそぐわないコードを書いてたような気がする。
具体的にはめちゃくちゃにジャンプが多いのです。
別にスパゲティプログラムを書いていたわけではなく、メモリアクセスを一バイトでも減らすために条件分岐を多用してたことが主な理由。


このあたりで昨今のプロセッサに習熟してる人ならば何が問題か気が付くでしょう。
ジャンプが多ければ、パイプラインが乱れます。
パイプラインが乱れれば、CPUはインストラクションキャッシュにコードを充当するため、2ndキャッシュからデータを読みに行くし、書き込み処理によってセカンドキャッシュが更新されててコードが落ちていれば、SD-RAMのメインメモリから4-3-3-3みたいな64bitx4のバーストリードを行います。
要するにCPUは、コード/データがキャッシュに載るまで、いちいち動作を止めて待ってなきゃいけない羽目に陥ります。
当時は私には、まったくこの知識がありませんでした。


i386sxの時代はインストラクションキューは数段しかなく、またキャッシュという概念もなく、DRAMメモリも16Mhz(!?)アクセスのノーウェイト。
メモリアクセスを一回でも減らすため、32bitレジスタの上位16bitにデータを突っ込んだりと、涙ぐましぃ努力をすればそれだけ確実に速度が上がりました。
しかし、PCにキャッシュが搭載されてからはその書き方はかえって遅くなりました。
Pentiumの時代以降は、データをシリアルで32バイトの倍数まとめて読み込んで、キャッシュから外さないように処理して、読み込んだ時と同様にまとめて書き込んだほうが高速になります。
加えてジャンプなんかせず、上から下につつがなく流れるコードを書いたほうが、パイプラインはきれいに流れてパフォーマンスは落ちません。


分岐を減らそうビットマップ処理

画面上にキャラクタを表示するときは、キーカラーで背景画像をくりぬく処理があります。


キャラクタデータを読んで、ドットがキーカラーと同じだったら背景への転送処理を省いて次のドット、違うならそれを転送してから次のドット、みたいな条件分岐処理込みのルーチン、処理手順からプログラムを書いていった、いかにも朝から夕方まで働く人間みたいな発想で書いたこういうプログラム遅いんですね。


四則演算でカラーキーからマスクを作って、ビット演算で穴をあけてからORで合成、合成したらまとめて書き込んでフラッシュ処理。
そういう風に、データ/ビットベースでの変遷に視点を移して、コードを書き直していきます。


この考え方はSIMDにコードを持っていくときに必要になる考え方です。
MMXSIMDではまとめてビット/バイトを処理する関係上、特定画素に対して条件分岐をかけることができません。
平行4/8ドットの画素に対して同時にまったく同じ演算をして、上記のようなくりぬき処理をキメなければならない。
まるでパズルです。頭がこんがらがって吐きそうになりますけど。
ですが、これを実現させると、コードは上から下にパイプラインを乱さずきれいに流れ、データも一切の無駄なく読み書きできるためバスの利用効率は高まりパフォーマンスが上がるわけです。
一般的にSIMDはデータをまとめて演算するため高速になると紹介されていますが、実際にコード書いてみた分にはバス効率を高めるようにコードを書き直すことが、主な速度向上の要因だと思います(実際SIMD命令セットにはキャッシュをバイパスして書き込む転送命令も含まれてます)。
つかったことはないですが、GPUプログラミングも似たような処理を多用することになるんじゃないかな。


また、このSIMDで動くようにしたコードは、ハードウェア実装する場合にも役に立ちます。
条件分岐ののちに数回の処理をするとかいう一般的なコードや、SSEなど浮動小数演算を多用したコードは、そのままFGPAに持っていくのは実に困難ですが、四則演算とビット演算、整数比較とキャリー/ゼロビットをレジスタに充填するようなことたけを使うならば、結構あっさりFPGAで同等の処理も実装できるはず。

だからと言って分岐を完璧に消す必要性はない

MMX-Pentuimの時代にはありませんでしたが、Pentium2以降のプロセッサにはCMov命令*1が盛り込まれました。
条件比較してヒットすれば、レジスタ/メモリにデータ転送するという命令を一命令にしたものです。
この命令は条件分岐命令にもかかわらずパイプラインをみだすことはない。
条件が満たされなければ、スキップ〜nopとして扱われる〜するだけです。
つかジャンプのJがついてない以上分岐命令ではないですね。


この命令は具体的にどんなところで使われるかというと、

if (dat != keycolor) { dst = dat;}

みたいなC言語で多用される、『◇◇なら、変数○○に●●を入れる』みたいなとこで使われます。


だからPCやARMなんかではそれほど悩む必要もないかも。
SHの分岐も、2オペ先にジャンプして、パイプライン止めないようなことしてた気がする。
ただし、SIMDに持っていく前提のコードならば、これは使えないのは上記で書いた通りです。


そういうことも最近....、というか数年前になって気が付いたんですけどね。

気が付いたときにはアセンブラコードなんて書いちゃいねーよ。


当時は、今考えるといろいろおかしい書き方でも、それでもまだアセンブラで書いたほうが速かった。
しかしコンパイラの最適化がだいぶ良くなってきたのと、マザーボードや中古PCが1000円3000円で買えるようになって、いろいろな環境で走らせて試せるようになると状況は変わった。
PCは自作で組んでも新品で10万超えるのが当たり前の頃は、数年の間同じスペックで使うわけでアセンブラ使ってでも高速化するメリットがあったが、もういまとなっては速度がいるなら速いマシンに買い替えたらいいし、強力な描画機能がいるならビデオカード買えばいい。
それでも遅いなら、複数コアに挑戦するとか、さらに進めて横にPC並べるまでする並列化を考えていけばいい。


ということで昔とった杵柄

昔書いた2BGレイヤ+1スプライトレイヤー+メッセージレイヤの4レイヤスクリーンシステムを、1チップマイコンに乗せるグラフィックシステムに調整中です。
1チップマイコンはRAMがSRAMでノーウェイト、メモリバスはハーバードなんで、PCほどバスを意識する必要はないですが、PCよりは遅い。
純粋なRISCに近いので、PCでアセンブラ一命令で済むのが三命令くらいになったりする。
だいぶ違う。


PCのP3/河童600MhzでVGAで60FPSでてましたが、180MhzのQVGAで果たして動くかどうか…。
233MhzのMMXで、めちゃくちゃ遅いビデオカードの組み合わせでは、20くらいしか出なかったような。
いやでも解像度1/4だし、なんとか。

*1:コンベアムーブ命令