はじめに
こんにちは。フリュー Advent Calendar 2022 も20日目になります。
今日はグッと低階層な話として、画像処理で使用する画素データの代入を最適化する話を取り上げてみようと思います。
使用する言語はC++です。
32bitの画素データを代入する
みなさん、画素データってどういうふうに扱っていますか?
ここでは試しに、シンプルな32bitのARGB色構造体を想定してみましょう。
struct ARGB
{
uint8_t A;
uint8_t R;
uint8_t G;
uint8_t B;
};
このデータ構造体に、色の値を代入する事を考えます。
最もわかりやすい形での実装は、メンバにそれぞれコピーするような形になると思います。
struct ARGB
{
//(中略)
inline void SetColor( uint8_t a, uint8_t r, uint8_t g, uint8_t b )
{
A = a;
R = r;
G = g;
B = b;
}
};
では、これを使って画素ごとの値計算を行う場合のことを想定してみます。
次のようなコードを見てみましょう。
for( int i = 0; i < 1000*1000; i++ )
{
uint8_t a = 0;
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
//(なんやかやあってa,r,g,bの値が決まる)
ARGB color;
color.SetColor( a, r, g ,b );
//(ここでcolorを使った処理をする)
}
画素ごとにA,R,G,Bの値をそれぞれ計算し、ARGBのcolor構造体を作った上で何かの処理をします。
この処理を手元の環境で実行してみたところ、所要時間は550msになりました。(100回実行した合計)
ではこのSetColorを高速化してみましょう。
struct ARGB
{
//(中略)
inline void SetColor( uint8_t a, uint8_t r, uint8_t g, uint8_t b )
{
(*((uint32_t*)this)) = (((uint32_t)b << 24) | ((uint32_t)g << 16) | ((uint32_t)r << 8) | a);
}
};
ARGBを4バイトの変数とみなし、ビットシフトで4バイト整数(uint32_t)を作ることでコピー回数を減らしています。
これはリトルエンディアンを想定しているのでバイトごとに降順で配置しています。
アーキテクチャによっては読み替える必要があります。
これを採用して前述の処理を動かしてみたところ、所要時間は96msになりました!
見事5倍以上の高速化ができました。めでたしめでたし…
所要時間 | |
---|---|
4回コピー式 | 550ms |
uint32_t式 | 96ms |
24bitの画素データを代入する
では次は、24bitの画素を扱う場合を考えてみましょう。
同じような構造体を考えてみます。
struct RGB
{
uint8_t R;
uint8_t G;
uint8_t B;
inline void SetColor( uint8_t r, uint8_t g, uint8_t b )
{
R = r;
G = g;
B = b;
}
};
前提として、構造体のアラインメントが4バイト単位の場合は特に考える必要はありません。
構造体に1バイトのパディングがあるため、計4バイトの構造体とみなすことで32bit画素のときと同じ方法が使えます。
しかし、アラインメントが1バイト単位を想定したパディングのない24bit画素の場合はどうでしょう?
4バイト整数のうち、3バイトをマスクしてコピーする方法が考えられますが…
struct RGB
{
//(中略)
inline void SetColor( uint8_t r, uint8_t g, uint8_t b )
{
(*((uint32_t*)this)) = ((*((uint32_t*)this)) & 0xFF000000) | (((uint32_t)b << 16) | ((uint32_t)g << 8) | r);
}
};
しかし、この方法には問題があります。
構造体のサイズは3バイトですが、uint32_tは4バイトですから、
コピー先が構造体の範囲をはみ出す恐れがあります。
運が良ければ何も起きない可能性もありますが、さすがにこれではいけません。
正しい範囲にのみコピーする方法を考える必要があります。
たとえば次のような方法はどうでしょうか。
struct RGB
{
//(中略)
inline void SetColor( uint8_t r, uint8_t g, uint8_t b )
{
uint32_t binary = ((b << 16) | (g << 8) | r);
memcpy( (uint8_t*)this, (uint8_t*)(&binary), 3 );
}
};
いったん4バイトの変数に値をまとめ、memcpyで3バイトコピーします。
これならコピーされる範囲を超えることはありませんし、安全です。
では速度はどうでしょうか?
なんとなく、memcpy関数呼び出しによるオーバーヘッドが気になるかもしれません。
実際に測ってみましょう!
32bit画素のときと同様のやりかたで計測したところ次のようになりました。
所要時間 | |
---|---|
3回コピー式 | 556ms |
memcpy式 | 118ms |
※100回実行した合計
…ということで、これでも十分高速になることがわかりました!何事も実際に測ってみることが大事ですね。
なぜこれで速くなるのでしょうか?
memcpyは展開されて早くなる?あるいは変数を3回コピーすることがmemcpyのオーバーヘッドよりも遅い?
など、理由はいくつか考えられます。機会があれば更に追いかけてみたいところです。
おわりに
- 低階層の最適化は、意外なところで成果が出ることもある。
- 先入観を置いておいて、何事も測ってみることが大事。
みなさんもこれを機に、画素データ処理の最適化を志していただければと思います。
それではまた、良い最適化ライフを!