【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

アルファブレンド

n7shi2008-03-23


MMXSSE2を一部サポートしました。

LLPMLには最適化機能がありません。アルファブレンドのコードを少し工夫するだけで動作速度が目に見えて変わりました。更にMMXSSE2をサポートしたところ、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); 

c2をMMXレジスタに代入します。

__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

SSE2MMXの後継です。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になりました。倍とまではいきませんでしたが、それでもかなりの高速化です。

感想

今まで難しそうという食わず嫌いでMMXやSSEを避けていましたが、重い処理ではSSEを無視できないと感じました。CPUに依存してしまいますが、LLPMLの場合x86Windowsにしか対応していないので、非依存性を考えるのはナンセンスです。最終的にはx64にも対応したいと思いますが、x64でもSSEは共通です。