GCCでMMXコードを書いてみた

### テストの条件がまちがっておりました


16KBのメモリ領域に対して処理を繰り返すだけでは、大半の処理はL1キャッシュ内で完了します。
SD-RAMに対してメモリアクセスが行われるのは、ループの最初の読み込み一回と、書き込み時のみです。
SD-RAMからL2に対するバーストリード(キャッシュフィル)が一度だけですし、ライトスルーキャッシュですから書き込み時はメモリとL1/L2キャッシュの両方が変更になります。


つまり、少なくとも読み込みに関しては、ループ二周目以降は1クロックで完了しているはずです。
ですので、以下の試行内容はまったく適用されません。

メモリ領域をキャッシュに対して十分に大きくとらなければ意味のない試行でした。

結論から言ってしまおう。
GCCの最適化は不完全。-mmmxや-msseをつけた程度ではほとんどなにも変わらない。



そういえばLinuxで普通にコード書いたことがないなと思い、MMXコードをちょこまかと書いてはgccの吐き出したコードを眺めてにやにやしております。


テスト環境は
debian(Lenny)
gcc 4.3.2
PC-PJ2-S3(Mobile cerelon 333Mhz:Dixon-128k)


プログラムは、16KBのメモリ空間を4バイト単位でひたすら+1していくという単純なもの。
for(j = 0; j < 0xffff; j++)
{
   for(i = 0; i < 4096; i++)
(*(mp + i))++;
}
条件分岐がほとんどないので、MMXでまとめて処理する場合と、一般の32bitで処理していく場合との差をばっちりと感じられるはず。
これくらい単純なルーフ゜であれば、最適化オプションをちょこっとつけて、SIMDの利用を宣言した程度で対応してくれる時代になっていてもおかしくないと思って始めてみたのですが、そこまでgccは賢くなかった。


コンパイルオプションは、以下のもの。全すべておなじ。
cc -O2 -mmmx -ftree-vectorize -falign-functions -mtune=i686 -march=pentium2 memtest.c


まずは上の一番基本的な奴を実行してみる。


>time ./a.out
Mem Test.
BufAddress : 08BBF008
End!

real 0m2.611s
user 0m2.480s 
sys 0m0.004s


userで2.5秒かかってます。


これをまずはSIMDを使わずに高速化することを考える。
どうやったら高速化するか?
このプログラムは、メモリに対しシーケンシャルなリードライトを繰り返す処理、いわゆるストリーミング演算である。
ということは、メモリやキャッシュメモリの存在を意識してコードを修正していけばいいことになる。メモリアクセス時に、CPUを待たせるペナルテイや条件分岐でのパイプラインのストールを減らしていけばいい。


PC-PJ2-S3は、CPUにモバイルセレロン(Dixon-128K)を搭載している。
このCPUのキャッシュメモリは、L1:32KB/L2:128KでL2はライトスルー方式。
メインメモリは64bit接続のSD-RAM128MB。
32bitのPentiumシリーズは、データバス幅64bitで、SD-RAMメモリをバースト転送するようになっている。
バースト転送というものは、アクセスする先頭位置のみ教えてやれば、それに続くアドレスのデータをまとめて転送するもの。Pentium2の場合も64bitデータを4連続で転送されるしくみになっている。
つまりメモリアクセスは基本的に(64/8)*4=32バイト単位で行われると考える。
実際L2キャッシュのキャッシュラインはこのアクセス単位にあわせて32バイトになっている。この単位ごとに処理を行うように変更する。


32バイト分、intで8回分まとめて処理するように変更する。
for(i = 0; i < 4096; i+=8)
{
(*(mp + i))++;
(*(mp + i + 1))++;
(*(mp + i + 2))++;
(*(mp + i + 3))++;
(*(mp + i + 4))++;
(*(mp + i + 5))++;
(*(mp + i + 6))++;
(*(mp + i + 7))++;
}
分岐が1/8になっているので、上の基本的な奴とは単純比較できないが、とりあえず実行してみる。


Mem Test.
BufAddress : 09FA8008
End!

real 0m1.377s
user 0m1.288s
sys 0m0.004s


大体半分の時間で終了。
この時点でコンパイラの-mmmx最適化オプションが当てにならないのがわかる。
最初のソースの単純な繰り返しすら、自動的にSIMD化し分岐回数を減らすをなぞしてはくれないのだ。
実際吐き出したアセンブラソースを調べてみてもMMX命令は使われていない。


次に、32バイトのキャッシュラインを意識し、対象のワークメモリを32bit境界に合わせてみる。


Mem Test.
BufAddress : 09163020 ;; ワークメモリの先頭アドレスは32バイト境界
End!

real 0m1.373s
user 0m1.252s
sys 0m0.004s


これだけでわずかながらも速度が向上する。
MMXでは規程されていないが、SSEを使う場合は対象となるメモリはからなず16バイトにアラインされていなければならないというルールがある(ずれていると例外起こして止まる)。ということは実は32バイトではなくて16バイトでもいいのかもしれないが、実測でわずかに速くなっているということは、やはり32バイトのほうが正解なのかもしれない。


ついでにわざとアラインをずらしメモリアクセスのペナルティを発生させてみる。32bitCPUは基本的に4バイト境界でアクセスするものだが、それを2バイトずらしてみる(ちなみにMIPSや68K等では例外起こして止まる)。


Mem Test.
BufAddress : 0976902A
End!

real 0m7.369s
user 0m7.024s
sys 0m0.040s


なんかの冗談じゃないかと思うくらい速度が落ちる。
メモリのペナルティは恐ろしい。
2バイトごとにアクセスを繰り返す16bitカラーでのプログラミングなどでは要注意。


次はいよいよMMXレジスタで2ワードまとめて、つまり8バイトまとめて処理をおこなってみる。
上のコンパイラに-Sオプションをつけ、アセンブラソースを吐き出させループ内をmmxレジスタを使ったものに修正する。


.L3:
movq (%eax), %mm1 ;; &mm0にはあらかじめ0x0000000100000001を入れておく。
movq 8(%eax), %mm2
paddd %mm0, %mm1
paddd %mm0, %mm2
movq %mm1, (%eax)
movq %mm2, 8(%eax)

movq 16(%eax), %mm1
movq 24(%eax), %mm2
paddd %mm0, %mm1
paddd %mm0, %mm2
movq %mm1, 16(%eax)
movq %mm2, 24(%eax)

add $32, %eax
cmpl %edx, %eax
jne .L3


現時点ではアセンブラ部分は最適化していない。この状態で実行してみる。


Mem Test.
BufAddress : 080E0020
End!

real 0m0.992s
user 0m0.920s
sys 0m0.012s


そこそこ高速化。


次は最適化をかけてみる。
Pemtiumシリーズは整数演算ユニットを2つ搭載しているので、同時に整数演算を二つ同時に実行できる(ただし直前の命令の演算結果を参照しない場合)。
ただしメモリにアクセスする命令や乗算/シフト命令は一つしか実行できない。しかし素の場合でもレジスタ間での整数演算とは同時に実行が可能である。


とのことなので、命令の実行順序を並び替え、同時実行させてみる。
.L3:
movq (%eax), %mm1
movq 8(%eax), %mm2
movq 16(%eax), %mm3
paddd %mm0, %mm1
movq 24(%eax), %mm4
paddd %mm0, %mm2
movq %mm1, (%eax)
paddd %mm0, %mm3
movq %mm2, 8(%eax)
paddd %mm0, %mm4
movq %mm3, 16(%eax)
add $32, %eax      ;; 分岐命令直前の一般命令もまぜた。
movq %mm4, -8(%eax)

cmpl %edx, %eax
jne .L3


さて速くなるだろうか...。なりました!!


Mem Test.
BufAddress : 096B7020
End!

real 0m0.789s
user 0m0.712s
sys 0m0.008s


命令を細切れにまぜこぜにした、人力最適化の効果がありました。
一番上の基本的なソースの4倍の速度がでたことになります。分岐数が同じものと比較下場合は、約6割の時間で終了しているのでレジスタ長が倍になった効果はでています。


しかしながら、結局昔ながらにすべて人力でやらねばいかんのだろうか>>gcc4.3.2
なんかgccSIMDプログラミングについて、知らないと損をするようなツールキットとか新設の最適化オプションとか知らないだけなんだろうか。