« テンプレートのインライン展開と最適化 | メイン | 小さく区切ってコピー »

2007年08月18日

グラフィックライブラリ:: アルファブレンドを計る

    

テンプレートのインライン展開と最適化で、VC と MinGW で期待通りの結果が得られたので、ついでにアルファブレンドの速度も計ることにした。
実行環境はAthlon 64 X2 3800+, DDR400のデュアルチャンネル。

コピー周りの処理は前回と一緒。
アルファブレンドは以下の式で行った。
1. dest = (src * alpha + dest * (256 - alpha) ) / 256

これをほぼそのまま実装したところ 234 msec ぐらいだった。
112 msec と比べるとだいぶ遅い。
これを以下のように式を変形して式3で実装したところ 210 msec。
2. dest = (src * alpha + dest * 256 - dest * alpha) / 256
3. dest = ((src - dest)*alpha)/256 + dest

そんなものか。
SIMD の組み込み関数を使用して、MMX や SSE2 で計ってみる。
SIMD の組み込み関数は、Linux* 版 インテル® C++ コンパイラ・ユーザーズ・ガイド のリファレンスの組み込み関数にリファレンスがある。
Linux 版 とあるけど、Windows でもそのままでいけた (Windows版のマニュアルもどこかにあると思うけど、どこにあるか知らない)

結果は、
MMX を使うと 128 msec
SSE2 を使うと 125 msec
単純コピーとの差 約10msecか。
800*600 を 50回も コピーしてこの結果なので、30 fps なら 800*600 を 12回はアルファブレンドコピーできるか。そんなに速いのか。

800*600 のコピーなので、アライメントされていること前提で書いたけど、実際は MMX の時は X が奇数値、SSE2 の時は4で割り切れない値の場合、別に処理する必要がある。
ただ、SSE3 が使えると _mm_lddqu_si128 を使えばアライメントされていない時でも、ある程度読込み速度が改善されるらしい。(指定アドレスの前後含めて256ビット読み込んで要らないところを捨てる処理とか)
でも、VC2005 ではまだ SSE3 の組み込み関数は使えなさそう。 pmmintrin.h をインクルードしたらないと言われた。
MinGW は問題ないよう。

ソースコードは以下の通り。
まだ、実際にBMPなどに書き出して確かめてはいない。
その内確かめて間違っていたら直す。
確認したら間違ったいたので修正した。
で、よく考えると MMX で 2pixel ずつ処理するメリットは少ないかも。
1pixel ずつの方が柔軟に出来るので良さそうな気がする。

class CAlphaBlendMMX : public std::binary_function<__m64,__m64,__m64>
{
private:
  const WORD op_val_;
  __m64 alpha_;
  __m64 zero_;

public:
  CAlphaBlendMMX( WORD op_val ) : op_val_(op_val)
  {
    alpha_ = _mm_set1_pi16(op_val);
    zero_ = _mm_setzero_si64();
  }

  __m64 operator()( __m64 d, __m64 s )
  {
    __m64 ms = _mm_unpacklo_pi8( s, zero_ ); // 00 mt 00 mt 00 mt 00 mt (下位半分)
    __m64 md = _mm_unpacklo_pi8( d, zero_ ); // 00 mt 00 mt 00 mt 00 mt (下位半分)
    ms = _mm_sub_pi16( ms, md ); // s - d;
    ms = _mm_mullo_pi16( ms, alpha_ ); // t * a
    ms = _mm_srai_pi16( ms, 8 ); // t >> 8
    __m64 mlo = _mm_add_pi16( ms, md ); // d + t

    ms = _mm_unpackhi_pi8( s, zero_ );
    md = _mm_unpackhi_pi8( d, zero_ );
    ms = _mm_sub_pi16( ms, md );
    ms = _mm_mullo_pi16( ms, alpha_ );
    ms = _mm_srai_pi16( ms, 8 );
    __m64 mhi = _mm_add_pi16( ms, md );
    return _mm_packs_pu16( mlo, mhi );
  }
};
class CAlphaBlendSSE2 : public std::binary_function<__m128i,__m128i,__m128i>
{
private:
  const WORD op_val_;
  __m128i alpha_;
  __m128i zero_;

public:
  CAlphaBlendSSE2( WORD op_val ) : op_val_(op_val)
  {
    alpha_ = _mm_set1_epi16(op_val);
    zero_ = _mm_setzero_si128();
  }

  __m128i operator()( __m128i d, __m128i s )
  {
    __m128i ms = _mm_unpacklo_epi8( s, zero_ );
    __m128i md = _mm_unpacklo_epi8( d, zero_ );
    ms = _mm_sub_epi16( ms, md );
    ms = _mm_mullo_epi16( ms, alpha_ );
    ms = _mm_srai_epi16( ms, 8 );
    __m128i mlo = _mm_add_epi16( ms, md );

    ms = _mm_unpackhi_epi8( s, zero_ );
    md = _mm_unpackhi_epi8( d, zero_ );
    ms = _mm_sub_epi16( ms, md );
    ms = _mm_mullo_epi16( ms, alpha_ );
    ms = _mm_srai_epi16( ms, 8 );
    __m128i mhi = _mm_add_epi16( ms, md );
    return _mm_packus_epi16( mlo, mhi );
  }
};

これより速くすることを考えるとすると dest が何度も参照されているので、それがキャッシュにのるように微小区間に区切って実行することかな?
それで本当に速くなるのか、どの程度速くなるのかはわからないが。



投稿者 Takenori : 2007年08月18日 21:35



コメント

mmxのアルファブレンドなのですが、

d = 255
s = 0
alpha_ = 255

この時に、

(0 - 255) * 255 = -65025

となってしまい、16bitの範囲を超えてしまわないでしょうか?
もし勘違いなら、ご指摘頂けると助かります。


投稿者 ねこ : 2009年10月18日 01:03

ねこさん、こんにちは。
桁あふれが発生したまま計算するとどうなりますか?

投稿者 Takenori : 2009年10月18日 15:16

お返事ありがとうございます。

_mm_sub_pi16(ms, md) = 0x00FF
_mm_mullo_pi16(ms, mblend) = 0xFE01
_mm_srai_pi16(ms, 8) = 0xFFFE
_mm_add_pi16(ms, md) = 0xFFFE

これをアンパックして代入すると値が 0 になってしまいます。
_mm_srai_pi16 の代わりに _mm_srli_pi16 を使えば、

_mm_srli_pi16(ms, 8) = 0x00FE
_mm_add_pi16(ms, md) = 0x00FE

となって 254 の値が取得出来ますが、
今度は(ms-md)が正の値の時に問題が出ます。

投稿者 ねこ : 2009年10月18日 22:20

>>今度は(ms-md)が正の値の時に問題が出ます。

すみません、最後の所間違えました。
正の値ではなく、符号付きの範囲を超えない時です。

投稿者 ねこ : 2009年10月18日 22:44

上の計算途中経過は d = 255, s = 0, alpha_ = 255 の時のものですか?
異なるように見えますが。

記事のソースを確認すると最後飽和演算になっているので、マスクを加えないと正しい結果にならないようですね。

投稿者 Takenori : 2009年10月19日 00:44


comments powered by Disqus
Total : Today : Yesterday : なかのひと