LLPMLには最適化機能がありません。アルファブレンドのコードを少し工夫するだけで動作速度が目に見えて変わりました。更にMMXやSSE2をサポートしたところ、MMXで3倍、SSE2で4.5倍程度の速度向上がありました。
コードの工夫
c2にc1をブレンドします。
c2.B = (c1.B * c1.A + c2.B * (256 - c1.A)) >> 8; c2.G = (c1.G * c1.A + c2.G * (256 - c1.A)) >> 8; c2.R = (c1.R * c1.A + c2.R * (256 - c1.A)) >> 8;
LLPML付属のサンプルw03で速度を計測します。Athlon 64 X2 4600+で200fps程度です。これでも充分に速いと思っていましたが、まだ改善の余地があります。
c1は単一色の長方形のため固定です。c1に対する計算をまとめるができます。
int b = c1.B * c1.A; int g = c1.G * c1.A; int r = c1.R * c1.A; int a = 256 - c1.A; c2.B = (b + c2.B * a) >> 8; c2.G = (g + c2.G * a) >> 8; c2.R = (r + c2.R * a) >> 8;
速度は300fpsに向上しました。
c2への代入を一度にまとめます。
c2 = (((r + c2.R * a) & 0xff00) << 8) | ((g + c2.G * a) & 0xff00) | ((b + c2.B * a) >> 8);
速度は350fpsに向上しました。これ以上の工夫は難しそうです。
MMX
MMXは同時に複数の値を計算できます。アルファブレンドのコードは同じ計算を複数の値に対して行っているため、うまく当てはまりそうです。考え方としては以下のようになります。
(c2.R, c2.G, c2.B) = ((c2.R, c2.G, c2.B) * (a, a, a) + (r, g, b)) >> 8
(a, a, a) と (r, g, b) をMMXレジスタに代入します。LLPMLではインラインアセンブラの記述方法として、MMX命令にプレフィックスのアンダーバーを付けた組み込み関数を使用します。
__m64w alpha = { a, a, a, 0 }; __m64w c1m = { b, g, r, 0 }; __movq(__mm0, alpha); __movq(__mm1, c1m);
__movd(__mm2, c2);
c2.Rなどの色素は8bitですが、計算に16bit必要になるため、byteからwordに拡張します。拡張の際に詰める上位byteが必要になるため、ゼロを代入したMMXレジスタを用意します。
__movd(__mm3, 0);
__punpcklbw(__mm2, __mm3);
(c2 * alpha + c1m) >> 8 の計算を行います。
__pmullw(__mm2, __mm0);
__paddw(__mm2, __mm1);
__psrlw(__mm2, 8);
wordからbyteに縮小します。c2が代入されているmm2はword*4です。MMXはword*8をbyte*8に縮小する仕様になっているため、ダミーとして先ほど用意したゼロのMMXレジスタを使い回します。
__packuswb(__mm2, __mm3);
これをc2に書き戻せば完了です。
__movd(c2, __mm2);
このMMX化によって1050fpsに高速化しました。3倍です。うまく当てはまるケースではかなりの効果を発揮するようです。
SSE2
SSE2はMMXの後継です。x64ではFPUやMMXは廃止してSSE/SSE2に完全移行するようです。SSE2にはMMXと同じ命令があるため、SSEのレジスタに書き直すだけです。
__m128iw alpha = { a, a, a, 0, 0, 0, 0, 0 }; __m128iw c1x = { b, g, r, 0, 0, 0, 0, 0 }; __movdqu(__xmm0, alpha); __movdqu(__xmm1, c1x); __movd(__xmm2, c2); __movd(__xmm3, 0); __punpcklbw(__xmm2, __xmm3); __pmullw(__xmm2, __xmm0); __paddw(__xmm2, __xmm1); __psrlw(__xmm2, 8); __packuswb(__xmm2, __xmm3); __movd(c2, __xmm2);
このように単純に書き直したものでは1000fpsでした。MMXより少し遅くなっています。これはSSEのレジスタはMMXのレジスタの倍のサイズになっていて、倍の計算を同時に行っているためのようです。半分の計算能力を遊ばせているのに速度低下がわずかなのは、むしろすごいことです。
一度に2ドット処理するようにしてみました。
__m128iw alpha = { a, a, a, 0, a, a, a, 0 }; __m128iw c1x = { b, g, r, 0, b, g, r, 0 }; __movdqu(__xmm0, alpha); __movdqu(__xmm1, c1x); __movq(__xmm2, c2); __movd(__xmm3, 0); __punpcklbw(__xmm2, __xmm3); __pmullw(__xmm2, __xmm0); __paddw(__xmm2, __xmm1); __psrlw(__xmm2, 8); __packuswb(__xmm2, __xmm3); __movq(c2, __xmm2);
ここでは省略していますが、実際には端数が出た場合の1ドット処理を付け足して辻褄を合わせています。これで1550fpsになりました。倍とまではいきませんでしたが、それでもかなりの高速化です。