組み込み関数(intrinsic)によるsimd入門
TRANSCRIPT
組み込み関数(intrinsic)を用いた組み込み関数(intrinsic) を用いたSIMD化による高速画像処理入門
名古屋工業大学 福嶋慶繁名古屋工業大学 福嶋慶繁
Twitter @fukushima1981
サンプルコードはこちらhttps://github.com/norishigefukushima/IntrinsicsSample
整数の足し算整数の足し算
2進数表現2進数表現
実際は?(shortの例)実際は?(shortの例)
0000000000000011++
00000000000001000000000000000100 = ?
実際は?(shortの例)実際は?(shortの例)
0000000000000011++
00000000000001000000000000000100 = ?
もったいなくない?もったいなくない?
目標目標
OpenCVによりも高速なプログラムを簡OpenCVによりも高速なプログラムを簡単につくれる書き方を覚えよう単 くれる書き方を覚えよう出来るようになること・高速コピー src.copyTo(dest);高速コピ src.copyTo(dest);・高速な加減乗算 dest = src1 + src2;・高速型変換 src.convert(dest,CV_16U);・高速BGR2RGB cvtColor(src,dest,CV_BGR2RGB);・高速BGR2Gray cvtColor(src,dest,CV_BGR2Gray);
※出来ないこと
整数演算だけ 浮動小数点の命令は使わない 比較演算は使整数演算だけ,浮動小数点の命令は使わない,比較演算は使わない,水平演算は使わない,AVXは含まない(SSE4.1まで)
目的目的
を使えば 化は難しくないよ• Intrinsicを使えばSIMD化は難しくないよ
でもOpenCVはちょくちょく関数のSIMD化アップデート– OpenCVはちょくちょく関数のSIMD化アップデ トしてるよ
• Changeset [7055]: optimized cmp and cvtscale(16s‐Changeset [7055]: optimized cmp and cvtscale(16s>16s) using SSE2 (thanks to Victoria): optimized cmpand cvtsc…
– コアな処理の関数ならIPP使えばAVXまで使った適化してあるよ(Linuxなら研究用は無料)
http://software.intel.com/en‐us/non‐commercial‐software‐development
MenuMenu
ピピコピー a.copyTo(b)変換 a.convertTo(b,CV 16S)コピー a.copyTo(b)変換 a.convertTo(b,CV 16S)変換 a.convertTo(b,CV_16S)加算 c = a + b, cv::add(a,b,c)乗算 l(b) lti l ( b )
変換 a.convertTo(b,CV_16S)加算 c = a + b, cv::add(a,b,c)乗算 l(b) lti l ( b )乗算 c = a.mul(b) , cv::multiply(a,b,c)変換2 a.convertTo(b,CV_16S,5,10)乗算 c = a.mul(b) , cv::multiply(a,b,c)変換2 a.convertTo(b,CV_16S,5,10)( , _ , , )反転 cv::flip(a,b,‐1)色変換1 cvtColor(a b CV BGR2RGB)
( , _ , , )反転 cv::flip(a,b,‐1)色変換1 cvtColor(a b CV BGR2RGB)色変換1 cvtColor(a,b,CV_BGR2RGB)分離 cv::slipt(a,v)色変換1 cvtColor(a,b,CV_BGR2RGB)分離 cv::slipt(a,v)色変換2 cvtColor(a,b,CV_BGR2Gray)色変換2 cvtColor(a,b,CV_BGR2Gray)
先に結果先に結果
ピ 効果なしピ 効果なしコピー 効果なし変換 1.5倍コピー 効果なし変換 1.5倍変換 1.5倍加算 1.4倍乗算 4 0倍
変換 1.5倍加算 1.4倍乗算 4 0倍乗算 4.0倍変換2 9.8倍乗算 4.0倍変換2 9.8倍反転 7.0倍色変換1 5 7倍反転 7.0倍色変換1 5 7倍色変換1 5.7倍分離 3.8倍色変換1 5.7倍分離 3.8倍色変換2 4.5倍色変換2 4.5倍
通常のプログラミングでは,意識しないこと
レジスタレジスタ一番CPUに近い記憶装置番CPUに近い記憶装置ハード
ディスクメモリ
L2
キャッシュ
L1
キャッシュレジスタ
演算回路(+ー×÷)
SIMD演算ではもったいない部分を有効活用できるレジスタを使う
m128__m128128bitを記憶できるレジスタ(SSE2の場合)
アセンブラアセンブラ
プ ジ 開始mul_asm proc ; プロシジャ開始 ;push ebp ; 引数処理:後述 ;mov ebp, esp
di [ b 8]mov edi, [ebp + 8]mov esi, [ebp + 12] ; ここまで引数処理 ;mov ecx, 32
LL01LL01:movaps xmm0, [edi]mulps xmm0, [esi]movaps [edi] xmm0movaps [edi], xmm0add edi, 16add esi, 16loop LL01loop LL01pop ebp ; 引数処理:後述 ;ret
mul asm endp ;プロシジャ終了 ;mul_asm endp ; プ シジャ終了 ;
http://www1.icnet.ne.jp/nsystem/simd_tobira/simd_cording.html
アセンブラアセンブラ
プ ジ 開始mul_asm proc ; プロシジャ開始 ;push ebp ; 引数処理:後述 ;mov ebp, esp
di [ b 8]よっぽど訓練されmov edi, [ebp + 8]mov esi, [ebp + 12] ; ここまで引数処理 ;mov ecx, 32
LL01
よっぽど訓練されないと読めない て
LL01:movaps xmm0, [edi]mulps xmm0, [esi]movaps [edi] xmm0
ないと読めないってmovaps [edi], xmm0add edi, 16add esi, 16loop LL01loop LL01pop ebp ; 引数処理:後述 ;ret
mul asm endp ;プロシジャ終了 ;mul_asm endp ; プ シジャ終了 ;
http://www1.icnet.ne.jp/nsystem/simd_tobira/simd_cording.html
Intrinsic関数Intrinsic関数
{• __asm { mov ch1, 0x41mov ch2, 0x42 }, }– とか魔法を唱えなくてもOK
言語 関数 た 使 る• C言語の関数みたいに使える– 例:ucharが16個のデータ”a”をレジスタ”ra”にロードuchar a[16];uchar a[16];__m128i ra = _mm_load_si128((__m128i*)(a));
注意:自分でアセンブラを書いたほうが 適コードを書くことが出来ます.
目次目次
基本編 O CV編• 基本編– __m128– ロード(load),ストア(store),
整数の加算
• vs OpenCV編– コピー
• a.copyTo(b)型変換整数の加算
– アライメントについて
• 応用編
– 型変換• a.convertTo(b,CV_16S)
– 加算( )– 型変換
• Pack• Unpack
• add(a,b,c);– 乗算
• multiply(a,b,c)色のスワ プ• cvt
– シャッフル– Stream書き込み
– 色のスワップ• cvtColor(a,b,CV_BGR2RGB)
– 色の分離li ( b)• slipt(a,b);
– グレイスケール変換• cvtColor(a,b,BGR2Gray)
基本編
メモリからレジスタにロードメモリからレジスタにロ ド
レジスタ同士のSIMD演算
レジスタからメモリにストア
m128__m128
b のレジスタを自由に切 て使う• 128 bitのレジスタを自由に切って使う– 整数 (__m128i)
• 8bit(char,uchar) x 16個• 16bit(short) x 8個• 32bit(int) x 4個• 64 bit(long) x2
小数( )– 小数(__m128, __m128d)• 16bit(float)• 32bit(double)• 64bit(long double)
レジスタ( m128)レジスタ(__m128)128 bitのレジスタを自由に切って使う128 bitのレジスタを自由に切って使う
128bit x 1long double
64bit x 2long , double
32bit x 4int, float
16bit x 8short
8bit x 16char
SIMD演算SIMD演算
“1回の命令”で16個のデータを同時に足し算する例1回の命令 で16個のデータを同時に足し算する例
8bit x 16char
8bit x 16
+ + + + + + + + + + + + + + + +
8bit x 16char
8bit x 16charchar
unsigned charの加算のコードunsigned charの加算のコード
void loadaddstoreBF(uchar* src1, uchar* src2, uchar* dest, const int size)
{{for(int i=0;i<size;i++){{
dest[i] = src1[i]+src2[i];}
}
ただの足し算
Intrinsicによる加算コードの 適化Intrinsicによる加算コードの 適化
id l d dd t SSEvoid loadaddstoreSSE(uchar* src1, uchar* src2, uchar* dest, const int size)
{{for(int i=0;i<size;i+=16){
//データをレジスタa,bに128bit(8bit16個)ロードする__m128i a = _mm_load_si128((__m128i*)(src1+i));
128i b l d i128(( 128i*)( 2 i))__m128i b = _mm_load_si128((__m128i*)(src2+i));//aとbのデータを同時に16個足し算し,aに結果を書きこむ
a = mm add epi8(a b);a _mm_add_epi8(a,b);//レジスタaのデータをメモリのdestに128bit(8bit16個)ストアする
_mm_store_si128((__m128i*)(dest+i),a);}
}
実験結果(uchar)実験結果(uchar)
大きな配列(5Gb t )を入力して計算大きな配列(5Gbyte)を入力して計算
66 ms (SSE)( )vs
770 ms (ベタ)
11 6倍の高速化11.6倍の高速化
shortの加算のコードshortの加算のコード
void loadaddstoreBF(short* src1, short* src2, short* dest, const int size)
{{for(int i=0;i<size;i++){{
dest[i] = src1[i]+src2[i];}
}
shortの足し算
Intrinsicによる加算コードの 適化Intrinsicによる加算コードの 適化
id l d dd t SSEvoid loadaddstoreSSE(short* src1, short* src2, short* dest, const int size)
{{for(int i=0;i<size;i+=8){
//データをレジスタa,bに128bit(16bit8個)ロードする__m128i a = _mm_load_si128((__m128i*)(src1+i));
128i b l d i128(( 128i*)( 2 i))__m128i b = _mm_load_si128((__m128i*)(src2+i));//aとbのデータを同時に8個足し算し,aに結果を書きこむ
a = mm add epi16(a b);a _mm_add_epi16(a,b);//レジスタaのデータをメモリのdestに128bit(16bit8個)ストアする
_mm_store_si128((__m128i*)(dest+i),a);}
}
実験結果(short)実験結果(short)
大きな配列(5Gb t )を入力して計算大きな配列(5Gbyte)を入力して計算
120 ms (SSE)( )vs
770 ms (ベタ)
6 4倍の高速化6.4倍の高速化
ロードとストア(SSE2)ロードとストア(SSE2)
から ジ タ 転送Load(メモリsrcからレジスタaへ転送)__m128i a = _mm_load_si128((__m128i*)(src));
Store(レジスタaからメモリdestにコピー)Store(レジスタaからメモリdestにコピ )
_mm_store_si128((__m128i*)(dest),a);
メモリの型に関係なく,128bitのデータをメモリの型に関係なく,128bitのデ タをメモリーレジスタ間で転送する関数
Loadのデータの並び順Loadのデータの並び順
{ }をロData ={0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}をロード
dataの型の型がcharのとき
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15shortのときshortのとき
0,0,1,0,2,0,3,0,4,0,5,0,6,0,7,0のときintのとき
0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0
加算 mm add epi(SSE2)加算 _mm_add_epi(SSE2)
レジスタ bをSIMDで加算する関数レジスタa,bをSIMDで加算する関数
a = _mm_add_epi8(a,b);//uchar 16個dd ( b) // h 個a = _mm_add_epi16(a,b);//short 16個
a = _mm_add_epi32(a,b);//int 16個
原理上,Ucharは16倍速,shortは8倍速,intは4倍速で加算が出来る算が出来る
その他:
dd i8 飽和演算付き i d足し算_mm_adds_epi8 飽和演算付きsigned足し算
_mm_adds_epu8 飽和演算付きunsigned 足し算
他にも演算はたくさんある他にも演算はたくさんある
Add 加算Add 加算Adds 飽和演算付き加算
減算Sub 減算Subs 飽和演算付き減算Avg 平均Min 小値Max 大値Mul 乗算乗算Sad 絶対誤差|a‐b|
アライメントアライメント
128i l d i128(( 128i*)( ))__m128i a = _mm_load_si128((__m128i*)(src));_mm_store_si128((__m128i*)(dest),a);メモリの番地が16の倍数である必要がある※そろってないとプログラムが強制終了orz
こんな感じでメモリ確保をすればOKこんな感じでメモリ確保をすればOK宣言
_declspec( align(16) ) uchar data[size];動的確保
_aligned_malloc(size_t size, 16) (VCの場合)aligned free(void *memblock) (VCの場合)_aligned_free(void memblock) (VCの場合)
※OpenCVのMatで中身をつくるとアライメントがそろってます
アライメントによるの自由度の損失アライメントによるの自由度の損失
__m128i a = _mm_load_si128((__m128i*)(src));
_mm_store_si128((__m128i*)(dest),a);
src+=16,dest+=16など16区切りでしか処理できない...
メモリの区切りをずらした処理が出来ない...
アライメントがずれる場合アライメントがずれる場合
void loadaddstoreSSE(uchar* src1 uchar* src2 uchar* dest const int size)(uchar src1, uchar src2, uchar dest, const int size)
{for(int i=0;i<size;i+=16){
__m128i a = _mm_load_si128((__m128i*)(src1+i));//1画素ずらして足し算したい//1画素ずらして足し算したい
__m128i b = _mm_load_si128((__m128i*)(src2+i+1));a = mm add epi8(a,b);a _mm_add_epi8(a,b);_mm_store_si128((__m128i*)(dest+i),a);
}} この関数は落ちる
アライメントがずれる場合アライメントがずれる場合
void loadaddstoreSSE(uchar* src1 uchar* src2 uchar* dest const int size)(uchar src1, uchar src2, uchar dest, const int size)
{for(int i=0;i<size;i+=16){
__m128i a = _mm_loadu_si128((__m128i*)(src1+i));//1画素ずらして足し算したい//1画素ずらして足し算したい
__m128i b = _mm_loadu_si128((__m128i*)(src2+i+1));a = mm add epi8(a,b);a _mm_add_epi8(a,b);_mm_storeu_si128((__m128i*)(dest+i),a);
}} この関数は落ちない(すこし遅い)
応用編
型変換型変換
シャッフル
Stream書き込み
実際に画像処理をするときは実際に画像処理をするときは...
ジ タ ド• メモリからレジスタにロード(8bit)
• レジスタの型変換レジスタの型変換
• レジスタ同士のSIMD演算(16bit)
• レジスタの型変換
• レジスタからメモリにストア(8bit)レジスタからメモリにストア(8bit)
• など,ucharの計算だけではおさまらない....
SIMD化しようとしてつまずくポイントSIMD化しようとしてつまずくポイント
画像処理のデ タは大体• 画像処理のデータは大体8bit入力,8bit出力だけれども,その中間は16bit(short),32bit(int)など,大きな型で計算( ), ( )な ,大 な 計算– 型の変換
• uchar a, b;• int c (int)a + (int)b• int c = (int)a + (int)b;
– こんな便利な型変換がない.saturate_cast<>とかはもちろんない
• さらに,カラー画像はRGBRGBRGB...とデータが並ぶため 個デ タを ドするとため16個データをロードすると– RGBRGBRGBRGBRGBR←一個余った...
• 処理が煩雑• 処理が煩雑
型変換型変換
さ ズ 大き ズ• 小さいサイズから大きいサイズへ
– uchar→ short→
– uchar→ int
• 大きいサイズから小さいサイズへ
– short→ uchar
int→uchar– int→uchar
uchar→ short (アライメント無視)uchar→ short (アライメント無視)
void cvtuchar2short(uchar* src, short* dest, int size){
for(int i=0;i<size;i+=8)for(int i=0;i<size;i+=8){
//8バイト刻みでロードする(下半分は無視)//8 イト刻みで ドする(下半分は無視)__m128i a = _mm_loadu_si128((__m128i*)(src+i));
//下位ビットをshortに変換する命令a =_mm_cvtepu8_epi16(a);
//shortのデータは16ごとに書きこめるmm store si128(( m128i*)(dest+i) a);_mm_store_si128((__m128i*)(dest+i),a);
}}}
uchar→ short (アライメント考慮)uchar→ short (アライメント考慮)void cvtuchar2shortAligned(uchar* src, short* dest, int size){{
for(int i=0;i<size;i+=16){{
//16バイト刻みでロードする__m128i a = _mm_load_si128((__m128i*)(src+i));
//レジスタ全体を8バイト右シフトする__m128i b = _mm_srli_si128(a,8);
//下位ビ トを h tに変換する命令とストアを2回づつ//下位ビットをshortに変換する命令とストアを2回づつa =_mm_cvtepu8_epi16(a);b = mm cvtepu8 epi16(b);b _mm_cvtepu8_epi16(b);_mm_store_si128((__m128i*)(dest+i),a);_mm_store_si128((__m128i*)(dest+i+8),b);
}}
uchar→ short (アライメント考慮)uchar→ short (アライメント考慮)void cvtuchar2shortAligned2(uchar* src, short* dest, int size){{//全て0のレジスタを作る
const m128i zero = mm setzero si128();const __m128i zero = _mm_setzero_si128();for(int i=0;i<size;i+=16){
//16バイト刻みでロードする__m128i a = _mm_load_si128((__m128i*)(src+i));
//ゼロで入力のHigh Lowをアンパック128i b khi i8( )__m128i b = _mm_unpackhi_epi8(a,zero);
a = _mm_unpacklo_epi8(a,zero);//ストア//ストア
_mm_store_si128((__m128i*)(dest+i),a);_mm_store_si128((__m128i*)(dest+i+8),b);
}}
比較比較
• C++: 355.772 ms
• アライメント無視: 64 8431 msアライメント無視: 64.8431 ms
• アライメント考慮1: 44.4918 ms
• アライメント考慮2: 37.3332 ms
大9.5倍の高速化
uchar→ int (アライメント無視)uchar→ int (アライメント無視)
void cvtuchar2int(uchar* src, int* dest, int size){
for(int i=0;i<size;i+=4)for(int i=0;i<size;i+=4){
//4バイト刻みでロードする(12バイト分無視)//4 イト刻みで ドする( イト分無視)__m128i a = _mm_loadu_si128((__m128i*)(src+i));
//下位ビットをintに変換する命令a =_mm_cvtepu8_epi32(a);_mm_store_si128((__m128i*)(dest+i),a);
}}}
uchar→ short (アライメント考慮)uchar→ short (アライメント考慮)void cvtuchar2intAligned(uchar* src, int* dest, int size){{
for(int i=0;i<size;i+=16){
//16バイト刻みでロードする// イ 刻 する__m128i a = _mm_load_si128((__m128i*)(src+i));
//intにしてストア__m128i b =_mm_cvtepu8_epi32(a);_mm_store_si128((__m128i*)(dest+i),b);
//レジスタ全体を4バイト右シフトして変換してストアを4回繰り返すa = _mm_srli_si128(a,4);b =_mm_cvtepu8_epi32(a);_mm_store_si128((__m128i*)(dest+i+4),b);a = _mm_srli_si128(a,4);b ( )b =_mm_cvtepu8_epi32(a);_mm_store_si128((__m128i*)(dest+i+8),b);a = _mm_srli_si128(a,4);b t 8 i32( )b =_mm_cvtepu8_epi32(a);_mm_store_si128((__m128i*)(dest+i+12),b);
}}
比較比較
• C++: 365.9 ms
• アライメント無視: 125 0 msアライメント無視: 125.0 ms
• アライメント考慮: 65.6 ms
大5.6倍の高速化
変換 mm cvt (SSE4 1)変換_mm_cvt (SSE4.1)
0 0_mm_cvtepu8_epi16 _mm_cvtepu8_epi32
下位ビットを任意の整数に変換する命令下位ビットを任意の整数に変換する命令__m128i _mm_cvtepi#_epi#(__m128i a)m128i mm cvtepu# epi#( m128i a)__m128i _mm_cvtepu#_epi#(__m128i a)
#8,16,32 #16,32,64
シフト mm slli si128 (SSE2)シフト_mm_slli_si128 (SSE2)
mm slli si128( m128i 4)0 0
mm srli si128( m128i 4)_mm_slli_si128(__m128i, 4) _mm_srli_si128(__m128i, 4)
レジスタ全体を右 左にシフトする関数レジスタ全体を右,左にシフトする関数右シフトで任意のビットを下位に持ってこれる
個別のシフト mm slli epi32 (SSE2)個別のシフト_mm_slli_epi32 (SSE2)
0 0
_mm_slli_epi32(__m128i, 2) _mm_srli_epi32 (__m128i, 2)
si128が全体シフトとなるのに対して,epi16,32,64など,個別に区切ってシフトも可能
整数シフト(SSE2)整数シフト(SSE2)
全体でシフト(論理)• Si128全体でシフト(論理)__m128i _mm_srli_si128(__m128i , int)__m128i _mm_slli_si128(__m128i , int)
• 個別にシフト(論理)– Slli,srli _mm_slli_epi32(__m128i, 2)など
• 全部同量のシフト場合
– Sll,Srl _mm_sll_epi32(__m128i, __m128i)• 要素ごとにシフト量が異なる場合
算• 算術右シフト– Sra,Srai
アンパック mm unpack (SSE2)アンパック _mm_unpack (SSE2)_mm_unpackhi_epi8(a,b);
数 位 バ を タ ブ2つの引数の上位8バイトをインタリーブ
_mm_unpacklo_epi8(a,b);_ _ p _ p ( )2つの引数の下位8バイトをインタリーブ
HIGH LHIGH Low
0とアンパック0とアンパック0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 150 1 2 3 4 5 6 7 8 9 101112131415
0 0 1 0 2 0 3 0 4 0 5 0 6 0 7 08 0 9 0 10 0 11 0 12 0 13 0 14 0 15 0
ゼロとアンパックすると
HIGH Lowゼロとアンパックすると
shortのデータがうまく取り出せる
_mm_setzero_si128() レジスタを全て0にセットする関数
大きなものから小さなものへ大きなものから小さなものへ
• Short uchar
• Int ucharInt uchar
shortからucharshort からuchar
void cvtshort2ucharAligned (short* src, uchar* dest, int size){
for(int i=0;i<size;i+=16)for(int i=0;i<size;i+=16){
//shortのデータ16個を2つのレジスタにロード//shortのデ タ 6個を のレジスタに ド__m128i a = _mm_load_si128((__m128i*)(src+i));__m128i b = _mm_load_si128((__m128i*)(src+i+8));
ジ タをパ デ タ 変換//二つのレジスタをパックしてcharのデータに変換してストアa = _mm_packs_epi16(a,b);mm store si128(( m128i*)(dest+i) a);_mm_store_si128((__m128i*)(dest+i),a);
}}}
intからucharintからuchar
void cvtint2ucharAligned(int* src, uchar* dest, int size){
for(int i=0;i<size;i+=16){
//intのデータ16個を4つのレジスタにロードm128i a = mm load si128(( m128i*)(src+i));__m128i a _mm_load_si128((__m128i )(src i));
__m128i b = _mm_load_si128((__m128i*)(src+i+4));__m128i c = _mm_load_si128((__m128i*)(src+i+8));m128i d = mm load si128(( m128i*)(src+i+12));__m128i d = _mm_load_si128((__m128i )(src+i+12));
//上位下位二つのレジスタをパックして2つのshortのデータに変換a = _mm_packs_epi32(a,b);
k i32( d)c = _mm_packs_epi32(c,d);//shortをパックしてストア
a = _mm_packs_epi16(a,c);_mm_store_si128((__m128i*)(dest+i),a);
}}
実験結果実験結果
Short to uchar
• C++: 341.8 mC++: 341.8 m
• SSE: 35.4 ms 9 7倍の高速化Int to ucahr
9.7倍の高速化Int to ucahr
• C++: 372.1 ms
• SSE: 45.3 ms8.2倍の高速化8.2倍の高速化
パック mm packsパック _mm_packs
_mm_packs_epi16
2つのレジスタの各値が飽和演算されながらつぶされ,上位ビ ト 位ビ ト 整列される上位ビット下位ビットへ整列される
パックx2int x int→short パックx2int x int→short_mm_packs_epi32
short x short →uchar_mm_packs_epi16
データの並び替えデータの並び替え
な き 必• こんなときに必要
– 逆順ソート(フリップ)逆順ソ ( リッ )
– スワップ
RGBRGBRGB RRRGGGBBB– RGBRGBRGB RRRGGGBBB
などの並び替え
逆順ソート逆順ソートvoid flipdata(uchar* src, uchar* dest, const int size){{//逆順を設定するシャッフル用のマスクを設定
const __m128i mask = _mm_setr_epi8(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
//ソースのポインタを 後から16バイト前の位置に移動uchar* s = src+size‐16;for(int i=0;i<size;i+=16){
m128i a = mm load si128(( m128i*)(s));__m128i a _mm_load_si128((__m128i )(s));//逆順にシャッフル
a = _mm_shuffle_epi8(a,mask);mm store si128(( m128i*)(dest+i) a);_mm_store_si128((__m128i )(dest+i),a);
//ソースのポインタを16バイトデクリメントs‐=16;
}}}
比較比較
• C++ 50ms
• SSE 5.7msSSE 5.7ms
8 8倍高速化8.8倍高速化
シャッフル mm shuffle(SSE2)シャッフル _mm_shuffle(SSE2)mm shuffle epi8(a,mask);_mm_shuffle_epi8(a,mask);Maskには入力の行き先を設定mm setr epi8:レジスタを個別にセットする関数_mm_setr_epi8:レジスタを個別にセットする関数
__m128i mask=_mm_setr_epi8(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
__m128i mask=_mm_setr_epi8(2,1,0,5,4,3,8,7,6,11,10,9,14,13,12,15);
vs OpenCV編
MenuMenu
ピピコピー a.copyTo(b)変換 a.convertTo(b,CV 16S)コピー a.copyTo(b)変換 a.convertTo(b,CV 16S)変換 a.convertTo(b,CV_16S)加算 c = a + b, cv::add(a,b,c)乗算 l(b) lti l ( b )
変換 a.convertTo(b,CV_16S)加算 c = a + b, cv::add(a,b,c)乗算 l(b) lti l ( b )乗算 c = a.mul(b) , cv::multiply(a,b,c)変換2 a.convertTo(b,CV_16S,5,10)乗算 c = a.mul(b) , cv::multiply(a,b,c)変換2 a.convertTo(b,CV_16S,5,10)( , _ , , )反転 cv::flip(a,b,‐1)色変換1 cvtColor(a b CV BGR2RGB)
( , _ , , )反転 cv::flip(a,b,‐1)色変換1 cvtColor(a b CV BGR2RGB)色変換1 cvtColor(a,b,CV_BGR2RGB)分離 cv::slipt(a,v)色変換1 cvtColor(a,b,CV_BGR2RGB)分離 cv::slipt(a,v)色変換2 cvtColor(a,b,CV_BGR2Gray)色変換2 cvtColor(a,b,CV_BGR2Gray)
実験条件実験条件
ま ポ• Intel Core i7 920 SSE4.2までサポート
• 2 66→2 93GHzまでオーバークロックして使用2.66→2.93GHzまでオ バ クロックして使用
• OpenCVの関数
• も簡単?と思われる実装を自前で(C++)も簡単?と思われる実装を自前で(C++)
• SSEによる実装
の3つを比較
先に結果先に結果
ピ 効果なしピ 効果なしコピー 効果なし変換 1.5倍コピー 効果なし変換 1.5倍変換 1.5倍加算 1.4倍乗算 4 0倍
変換 1.5倍加算 1.4倍乗算 4 0倍乗算 4.0倍変換2 9.8倍乗算 4.0倍変換2 9.8倍反転 7.0倍色変換1 5 7倍反転 7.0倍色変換1 5 7倍色変換1 5.7倍分離 3.8倍色変換1 5.7倍分離 3.8倍色変換2 4.5倍色変換2 4.5倍
コピー a copyTo(b)コピー a.copyTo(b)void copy8SSE(const Mat& src, Mat& dest)void copy8SSE(const Mat& src, Mat& dest){
const uchar* s = src.data;uchar* d = dest data;uchar d = dest.data;const int size = src.size().area()*src.channels();for(int i=0;i<size;i+=16){{
__m128i a = _mm_load_si128((__m128i*)(s+i));_mm_store_si128((__m128i*)(d+i),a);
}}
16バイト単位でロードとストアを繰り返すだけ!繰り返すだけ!
結果結果
• OpenCV 68.2 ms• C++ 308.4ms• SSE 62.8 ms
コメント– さすがにコピーは変わらない
– SSEを使ってでコピーしているところをmemcpyでpy書いても同じ速度
– 自前でループ展開して代入するよりは速い.
変換 a convertTo(b CV 16U)変換 a.convertTo(b,CV_16U)void cvtuchar2ushortMatSSE(Mat& a,Mat& b){{
const int size = a.size().area()*a.channels();const uchar* s = a.ptr<uchar>(0);
i d h * d b i d h (0)unsigned short* d = b.ptr<unsigned short>(0);const __m128i zero = _mm_setzero_si128();for(int i=0;i<size;i+=16){
__m128i a = _mm_load_si128((__m128i*)(s+i));__m128i b = _mm_unpackhi_epi8(a,zero);__ _ _ p _ p ( , );a = _mm_unpacklo_epi8(a,zero);_mm_store_si128((__m128i*)(d+i),a);mm store si128(( m128i*)(d+i+8),b);_mm_store_si128((__m128i )(d+i+8),b);
}} 必要な型変換を実装するだけ必要な型変換を実装するだけ
(本当はax+bが出来る関数だけど今回は無視)
結果結果
• OpenCV 159.2 ms
• C++ 206.5 msC++ 206.5 ms
• SSE 100.7 ms
コメントコメント
– 型変換はOpenCVに実装されてないため高速化されるれる.
加算 c = a + b cv::add(a b c)加算 c = a + b, cv::add(a,b,c)void add8SSE(Mat& src1, Mat& src2, Mat& dest){
uchar* pa = src1.data;uchar* pb = src2.data;uchar* pc = dest datadest.data;
const int size = src.size().area()*src.channels();for(int i=0;i<size;i+=16)for(int i 0;i size;i 16){
__m128i a = _mm_load_si128((__m128i*)(pa+i));__m128i b = _mm_load_si128((__m128i*)(pb+i));
//飽和演算つきunsignedの加算a mm adds epu8(a b);a = _mm_adds_epu8(a,b);_mm_store_si128((__m128i*)(pc+i),a);
}}}
結果結果
• OpenCV 101.1 ms
• C++ 524.9 msC++ 524.9 ms
• SSE 71.4 ms
コメントコメント
– 例外処理が無いことやアライメントをそろえている分O CVの関数より高速分OpenCVの関数より高速
乗算(1/2) c = a mul(b) cv::multiply(a b c)乗算(1/2) c = a.mul(b) ,cv::multiply(a,b,c)void multiply8SSE(const Mat& src1, const Mat& src2,Mat& dest){//ロードしたりする前処理
uchar* s1 src1 data; uchar* s2 src2 data;uchar* duchar* s1 = src1.data; uchar* s2 = src2.data;uchar* d = dest.data;
const int size = src1.size().area()*src1.channels();const int si e src .si e().area() src .channels();const __m128i zero = _mm_setzero_si128();for(int i=0;i<size;i+=16){
//ロードデータを2本ストアm128i a = mm load si128(( m128i*)(s1+i));__m128i a = _mm_load_si128((__m128i*)(s1+i));
__m128i b = _mm_load_si128((__m128i*)(s2+i));
乗算(2/2) c = a mul(b) cv::multiply(a b c)乗算(2/2) c = a.mul(b) ,cv::multiply(a,b,c)//入力をhigh,lowにアンパック
__m128i a1 = _mm_unpackhi_epi8(a,zero);__m128i a2 = _mm_unpacklo_epi8(a,zero);m128i b1 mm unpackhi epi8(b zero);__m128i b1 = _mm_unpackhi_epi8(b,zero);
__m128i b2 = _mm_unpacklo_epi8(b,zero);//High,Lowごとに乗算(8bit乗算命令はない)して下位ビットをとりだ//High,Lowごとに乗算(8bit乗算命令はない)して下位ビットをとりだす
a = _mm_mullo_epi16(a1,b1);b = _mm_mullo_epi16(a2,b2);
//2つの計算結果をアンパックしてストアa = mm packus epi16(a b);a = _mm_packus_epi16(a,b);_mm_store_si128((__m128i*)(d+i),a);
}}}
結果結果
• OpenCV 405.8 ms
• C++ 409.3 msC++ 409.3 ms
• SSE 106.1 ms
コメントコメント
– OpenCVには実装されていない命令のためかなり高速化に成功高速化に成功
変換2(1/2) a convertTo(b CV 16U 5 10)変換2(1/2) a.convertTo(b,CV_16U,5,10)void convertTouchar2ushortMatSSE(Mat& src,Mat& dest, const intl h i b )alpha, const int beta){
const int size = src size() area()*src channels();const int size = src.size().area() src.channels();const uchar* s = src.data;unsigned short* d = dest.ptr<unsigned short>(0);
//0をセットするのと16ビットで8つの定数alpha.betaをセットconst __m128i zero = _mm_setzero_si128();
t 128i A t1 i16( l h )const __m128i A = _mm_set1_epi16(alpha);const __m128i B = _mm_set1_epi16(beta);for(int i=0;i<size;i+=16)for(int i 0;i<size;i+ 16){
__m128i a = _mm_load_si128((__m128i*)(s+i));
変換2(2/2) a convertTo(b CV 16U 5 10)変換2(2/2) a.convertTo(b,CV_16U,5,10)//ロードしたレジスタを上位でアンパックして乗算
128i b khi i8( )__m128i b = _mm_unpackhi_epi8(a,zero);b = _mm_mullo_epi16(b,A);
//ロードしたレジスタを下位でアンパックして乗算//ロ ドしたレジスタを下位でアンパックして乗算a = _mm_unpacklo_epi8(a,zero);a = _mm_mullo_epi16(a,A);
//計算結果を加算(shotrでの計算結果を得る)a = _mm_adds_epi16(a,B);b dd i16(b B)b = _mm_adds_epi16(b,B);
//shortは8ごとでも16境界なので2つストアmm store si128(( m128i*)(d+i) a);_mm_store_si128((__m128i )(d+i),a);_mm_store_si128((__m128i*)(d+i+8),b);
}}
結果結果
• OpenCV 1067.5 ms
• C++ 307.6 msC++ 307.6 ms
• SSE 109.9 ms
• コメントコメント
– convertToとかかなり使うのに,OpenCVはどんな実装してるんだか実装してるんだか...
– 乗算と加算,型変換のSSE化なのでかなり速い
反転 cv::flip(a b 1)反転 cv::flip(a,b,‐1)void flipSSE__(Mat& src, Mat& dest) //上下左右反転のみ{//反転用のマスク生成
const m128i mask =const __m128i mask = _mm_setr_epi8(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);const int size =src.size().area()*src.channels();const int si e src.si e().area() src.channels();uchar* s = src.data+size‐16;uchar* d = dest.data;逆 ド 転//入力を逆からロードして反転してストア
for(int i=0;i<size;i+=16){{
__m128i a = _mm_load_si128((__m128i*)(s));a = mm shuffle epi8(a,mask);_ _ _ p ( , );_mm_store_si128((__m128i*)(d+i),a);s‐=16;
反転 cv::flip(a b code)全対応反転 cv::flip(a,b,code)全対応void flip8UC1SSE(Mat& src, Mat& dest, int flipCode){
if(flipCode==0){
t i t i l /16const int size = src.cols/16;uchar* ss = src.data;uchar* dd = dest.data+(src.rows‐1)*src.cols;for(int j=src.rows;j‐‐;){
__m128i* s = (__m128i*)(ss);__m128i* d = (__m128i*)(dd);for(int i=size;i‐‐;){
m128i a = mm load si128(s++);__m128i a _mm_load_si128(s );_mm_store_si128((d++),a);
}ss+=src.cols;dd‐=src.cols;
}}else if(flipCode==1){
const int size = src.cols/16;const __m128i mask = _mm_setr_epi8(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);uchar* ss = src.data + src.cols ‐16;uchar* dd = dest.data;for(int j=src.rows;j‐‐;){
__m128i* s = (__m128i*)(ss);__m128i* d = (__m128i*)(dd);for(int i=size;i‐‐;){
m128i a = mm load si128(s );__m128i a = _mm_load_si128(s‐‐);a = _mm_shuffle_epi8(a,mask);_mm_store_si128((d++),a);
}ss+=src.cols;dd+=src.cols;
}}else if(flipCode==‐1){{
const __m128i mask = _mm_setr_epi8(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);const int size =src.size().area()*src.channels();uchar* s = src.data+size‐16;uchar* d = dest.data;for(int i=0;i<size;i+=16){
__m128i a = _mm_load_si128((__m128i*)(s));a = _mm_shuffle_epi8(a,mask);_mm_store_si128((__m128i*)(d+i),a);
6s‐=16;}
}}
結果結果
フリップには3種類(左右反転 上下反転 上下左右反転)• フリップには3種類(左右反転,上下反転,上下左右反転)• 左右 :OpenCV 375.6 ms C++ 313.4 ms SSE 68.8 ms• 上下 :OpenCV 88 30 ms C++ 59 59 ms SSE 64 0 ms上下 :OpenCV 88.30 ms C++ 59.59 ms SSE 64.0 ms• 上下左右 :OpenCV 467.3 ms C++ 306.5 ms SSE 65.8 msコメント
– 上下反転はSSEでもほとんど変わらない(逆順にmemcpyするだけ)
– 上下左右反転 左右反転の結果はかなりいい上下左右反転,左右反転の結果はかなりいい– Flipはビデオ入力とかでも良く使うので有望?– ただしRGBのカラー処理ではないので,ちゃんとRGBように作ら
ないと使えないかなないと使えないかな
色変換色変換
問題 画素を16個ロ ドすると先頭は問題:画素を16個ロードすると先頭は...RGBRGBRGBRGBRGB R 一個余る
戦略戦略1. 後のあまりを無視する2 3個集めて 8個づつ処理すればあまりなし!2. 3個集めて48個づつ処理すればあまりなし!
RGBRGBRGBRGBRGBR
GBRGBRGBRGBRGBRGswap
swap
BRGBRGBRGBRGBRGBswap
色変換 cv::cvtColor(a b CV BGR2RGB)色変換 cv::cvtColor(a,b,CV_BGR2RGB)void cvtBGR2RGB_SSE_nonal(Mat& src,Mat& dest){
i i i () ()* h l ()const int size = src.size().area()*src.channels();const uchar* s = src.data; uchar* d = dest.data;
m128i mask =__m128i mask = _mm_setr_epi8(2,1,0,5,4,3,8,7,6,11,10,9,14,13,12,
15);//ここを捨てる.//BGRBGRBGRBGRBGR Bfor(int i=0;i<size;i+=15)//画素5x3づつ処理する{
//境界をそろ てなくても使えるロ ド//境界をそろってなくても使えるロード__m128i a = _mm_loadu_si128((__m128i*)(s+i));
//シャッフル//シャッフルa = _mm_shuffle_epi8(a,mask);
//境界をそろってなくても使えるストア_mm_storeu_si128((__m128i*)(d+i),a);
}}
色変換高速版(1/3) cv::cvtColor(a b CV BGR2RGB)色変換高速版(1/3) cv::cvtColor(a,b,CV_BGR2RGB)
void cvtBGR2RGB_SSE(Mat& src,Mat& dest){{//前準備
const int size = src size() area()*src channels();const int size = src.size().area() src.channels();uchar* s = src.data;uchar* d = dest.data;__m128i mask1 = //BGR BGR BGR BGR BGR B
_mm_setr_epi8(2,1,0,5,4,3,8,7,6,11,10,9,14,13,12,15);__m128i mask2 = //GR BGR BGR BGR BGR BGt i8(0 14 4 3 2 7 6 5 10 9 8 13 12 14 1 15)_mm_setr_epi8(0,14,4,3,2,7,6,5,10,9,8,13,12,14,1,15);__m128i mask3 = //R BGR BGR BGR BGR BGR
mm setr epi8(0 3 2 1 6 5 4 9 8 7 12 11 10 15 14 13);_mm_setr_epi8(0,3,2,1,6,5,4,9,8,7,12,11,10,15,14,13);//48画素一気に処理
for(int i=0;i<size;i+=48){
色変換高速版(1/3) cv::cvtColor(a b CV BGR2RGB)色変換高速版(1/3) cv::cvtColor(a,b,CV_BGR2RGB)
//16画素x3個3つのレジスタに分けてロード128i l d i128(( 128i*)( i))__m128i a = _mm_load_si128((__m128i*)(s+i));
__m128i b = _mm_load_si128((__m128i*)(s+i+16));m128i c = mm load si128(( m128i*)(s+i+32));__m128i c = _mm_load_si128((__m128i )(s+i+32));
//レジスタをまたぐ値を抽出int Ba = _mm_extract_epi8(a,15);int Rb = _mm_extract_epi8(b,1);int Bb = _mm_extract_epi8(b,14);i t R t t i8( 0)int Rc = _mm_extract_epi8(c,0);
//シャッフルしてRGBRGBRGBRGBRっぽい値にスワップa = mm shuffle epi8(a mask1);a _mm_shuffle_epi8(a,mask1);b = _mm_shuffle_epi8(b,mask2);c = _mm_shuffle_epi8(c,mask3);
色変換高速版(1/3) cv::cvtColor(a b CV BGR2RGB)色変換高速版(1/3) cv::cvtColor(a,b,CV_BGR2RGB)
//レジスタ間でまたぐ値を挿入してi i8( Rb 15)a=_mm_insert_epi8(a,Rb,15);
b=_mm_insert_epi8(b,Ba,1);b= mm insert epi8(b Rc 14);b=_mm_insert_epi8(b,Rc,14);c=_mm_insert_epi8(c,Bb,0);
//16画素x3個ストア_mm_store_si128((__m128i*)(d+i),a);_mm_store_si128((__m128i*)(d+i+16),b);
t i128(( 128i*)(d i 32) )_mm_store_si128((__m128i*)(d+i+32),c);}
}}
結果結果
• OpenCV 392.3 ms
• C++ 265.2 msC++ 265.2 ms
• SSE 99.3 ms
• SSE 68.8 ms (アライメントまで考慮)
コメントコメント
– 良く使う割にOpenCVの関数は遅い
分離 cv::split(a v)分離 cv::split(a,v)
素を 個 ドする 先問題:画素を16個ロードすると先頭は...
RGBRGBRGBRGBRGB R 一個余るRGBRGBRGBRGBRGB R 個余る
戦略
づ– 3個集めて48個づつ処理
RGBRGBRGBRGBRGBR RRRRRRRRRRRRRRRR
GBRGBRGBRGBRGBRG GGGGGGGGGGGGGGG
BRGBRGBRGBRGBRGB BBBBBBBBBBBBBBBB
16個づつに並びかえて書きこみ
分離(1/8) cv::split(a v)分離(1/8) cv::split(a,v)void splitSSE(Mat& src, vector<Mat>& dest){//サイズやポインタをセット
const int size = src.size().area()*src.channels();uchar* s = src.ptr<uchar>(0);uchar* B = dest[0] ptr<uchar>(0);uchar B = dest[0].ptr<uchar>(0);uchar* G = dest[1].ptr<uchar>(0);uchar* R = dest[2].ptr<uchar>(0);
//48個(16x3)ロードするとこんな並び順//BGR BGR BGR BGR BGR B//GR BGR BGR BGR BGR BG//R BGR BGR BGR BGR BGR// Bを作る1本目の1回目シャッフル:BBBBBBGGGGGRRRRRconst __m128i mask1 = _mm_setr_epi8(0,3,6,9,12,15,1,4,7,10,13,2,5,8,11,14);// Gを作る1本目の2回目シャッフル:GGGGGBBBBBBRRRRRconst __m128i smask1 = _mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15);// Rを作る1本目の3回目シャッフル:RRRRRGGGGGBBBBBB//const __m128i ssmask1 = _mm_setr_epi8(11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10);
// Bを作る2本目の1回目シャッフル: GGGGGGBBBBBRRRRRconst __m128i mask2 = _mm_setr_epi8(0,3,6,9,12,15, 2,5,8,11,14,1,4,7,10,13);// Gを作る2本目の2回目シャッフル:BBBBBGGGGGGRRRRR//const __m128i smask2 = _mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15);//Rを作る2本目の3回目シャッフル:BBBBBRRRRRGGGGGGconst __m128i ssmask2 = _mm_setr_epi8(0,1,2,3,4,11,12,13,14,15,5,6,7,8,9,10);
// Bを作る3本目の1回目シャッフル: RRRRRRGGGGGBBBBB//__m128i mask3 = _mm_setr_epi8(0,3,6,9,12,15, 2,5,8,11,14,1,4,7,10,13);//同じマスクがあるので代用
があ//const __m128i smask3 = _mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,6,7,8,9,10); //同じマスクがあるので代用//const __m128i ssmask3 = _mm_setr_epi8(11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10); //同じマスクがあるので代用
//ブレンドマスクconst __m128i bmask1 = _mm_setr_epi8
(255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0);const __m128i bmask2 = _mm_setr_epi8(255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0);const __m128i bmask3 = _mm_setr_epi8
(255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0);const __m128i bmask4 = _mm_setr_epi8(255 255 255 255 255 255 255 255 255 255 0 0 0 0 0 0)(255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0);
__m128i a,b,c;for(int i=0;i<size;i+=48){
a = _mm_stream_load_si128((__m128i*)(s+i));b = _mm_stream_load_si128((__m128i*)(s+i+16));c = _mm_stream_load_si128((__m128i*)(s+i+32));a = _mm_load_si128((__m128i*)(s+i));b = _mm_load_si128((__m128i*)(s+i+16));c = _mm_load_si128((__m128i*)(s+i+32));
__m128i v = _mm_blendv_epi8(b,a,bmask1);v = _mm_blendv_epi8(c,v,bmask2);_mm_stream_si128((__m128i*)(B),v);
a = _mm_shuffle_epi8(a,smask1);b = _mm_shuffle_epi8(b,smask1);c = _mm_shuffle_epi8(c,smask1);v = _mm_blendv_epi8(b,a,bmask3);v = _mm_blendv_epi8(c,v,bmask2);_mm_stream_si128((__m128i*)(G),v);
a = _mm_shuffle_epi8(a,ssmask1);c = _mm_shuffle_epi8(c,ssmask1);b = _mm_shuffle_epi8(b,ssmask2);
v = _mm_blendv_epi8(b,a,bmask3);v = _mm_blendv_epi8(c,v,bmask4);_mm_stream_si128((__m128i*)(R),v);
B+=16;G+=16;R+=16;}
}
分離(2/8) cv::split(a v)分離(2/8) cv::split(a,v)void splitSSE(Mat& src, vector<Mat>& dest){//サイズやポインタをセット
const int size = src size() area()*src channels();const int size = src.size().area() src.channels();uchar* s = src.ptr<uchar>(0);uchar* B = dest[0].ptr<uchar>(0);[ ] p ( );uchar* G = dest[1].ptr<uchar>(0);uchar* R = dest[2].ptr<uchar>(0);
// 個( ) ドすると んな並び順//48個(16x3)ロードするとこんな並び順//BGR BGR BGR BGR BGR B//GR BGR BGR BGR BGR BG//GR BGR BGR BGR BGR BG//R BGR BGR BGR BGR BGR
分離(3/8) cv::split(a v)分離(3/8) cv::split(a,v)// Bを作る1本目の1回目シャッフル:BBBBBBGGGGGRRRRRconst __m128i mask1 =
_mm_setr_epi8(0,3,6,9,12,15,1,4,7,10,13,2,5,8,11,14);// Gを作る1本目の2回目シャッフル:GGGGGBBBBBBRRRRR// Gを作る1本目の2回目シャッフル:GGGGGBBBBBBRRRRRconst __m128i smask1 =
mm setr epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15);_ _ _ p ( , , , , , , , , , , , , , , , );// Rを作る1本目の3回目シャッフル:RRRRRGGGGGBBBBBBconst __m128i ssmask1 =
( )_mm_setr_epi8(11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10);
分離(4/8) cv::split(a v)分離(4/8) cv::split(a,v)// Bを作る2本目の1回目シャッフル: GGGGGGBBBBBRRRRRconst __m128i mask2 = _mm_setr_epi8(0,3,6,9,12,15,
2,5,8,11,14,1,4,7,10,13);// Gを作る2本目の2回目シャッフル:BBBBBGGGGGGRRRRR// Gを作る2本目の2回目シャッフル:BBBBBGGGGGGRRRRR//const __m128i smask2 =
mm setr epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15);_ _ _ p ( , , , , , , , , , , , , , , , );//Rを作る2本目の3回目シャッフル:BBBBBRRRRRGGGGGGconst __m128i ssmask2 =
( )_mm_setr_epi8(0,1,2,3,4,11,12,13,14,15,5,6,7,8,9,10);
分離(5/8) cv::split(a v)分離(5/8) cv::split(a,v)// Bを作る3本目の1回目シャッフル: RRRRRRGGGGGBBBBB//__m128i mask3 = _mm_setr_epi8(0,3,6,9,12,15,
2,5,8,11,14,1,4,7,10,13);//同じマスクがあるので代用//const m128i smask3 =//const __m128i smask3 =
_mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,6,7,8,9,10); //同じマスクがあるので代用代用
//const __m128i ssmask3 = _mm_setr_epi8(11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10); //同じマスクがあるの 代用があるので代用
分離(6/8) cv::split(a v)分離(6/8) cv::split(a,v)//ブレンドマスク
const __m128i bmask1 = _mm_setr_epi8(255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0);
const m128i bmask2 = mm setr epi8const __m128i bmask2 = _mm_setr_epi8(255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0);const m128i bmask3 = mm setr epi8__ _ _ _ p
(255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0);const __m128i bmask4 = _mm_setr_epi8( )(255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0);
//48(16x3)画素ごと処理開始//48(16x3)画素ごと処理開始__m128i a,b,c;
for(int i=0;i<size;i+=48)( ; ; ){
分離(7/8) cv::split(a v)分離(7/8) cv::split(a,v)//16個づつa,b,cにロード
a = _mm_load_si128((__m128i*)(s+i));b = _mm_load_si128((__m128i*)(s+i+16));c = mm load si128(( m128i*)(s+i+32));c = _mm_load_si128((__m128i )(s+i+32));
//ブレンドを使ってBBBBBBBBBBBBBBBBに変換m128i v = mm blendv epi8(b,a,bmask1);__ _ _ _ p ( , , );
v = _mm_blendv_epi8(c,v,bmask2);_mm_stream_si128((__m128i*)(B),v);
//シ とブ ドを使 に変換//シャッフルとブレンドを使ってGGGGGGGGGGGGGGGGに変換a = _mm_shuffle_epi8(a,smask1);b = mm shuffle epi8(b smask1);b = _mm_shuffle_epi8(b,smask1);c = _mm_shuffle_epi8(c,smask1);v = _mm_blendv_epi8(b,a,bmask3);_ _ _ p ( , , );v = _mm_blendv_epi8(c,v,bmask2);_mm_stream_si128((__m128i*)(G),v);
分離(8/8) cv::split(a v)分離(8/8) cv::split(a,v)//シャッフルとブレンドを使ってRRRRRRRRRRRRRRRRに変換
a = _mm_shuffle_epi8(a,ssmask1);c = _mm_shuffle_epi8(c,ssmask1);b = mm shuffle epi8(b ssmask2);b = _mm_shuffle_epi8(b,ssmask2);v = _mm_blendv_epi8(b,a,bmask3);v = mm blendv epi8(c,v,bmask4);_ _ _ p ( , , );_mm_stream_si128((__m128i*)(R),v);
//16画素分進めるB+=16;G+=16;R+=16;
}}}
結果結果
• OpenCV 697.1 ms
• C++ 468.7 msC++ 468.7 ms
• SSE 171.4 ms
コメント
– RGBを並べ替えるのは結構めんどくさい...RGBを並 替えるのは結構めんどくさい...
– これベースにカラー画像のSSE化が可能
RRRRGGGGBBBBに並べ替えるのは 「速度的にこ– RRRRGGGGBBBBに並べ替えるのは,「速度的にこれってベストなの?」という疑問は残るが...
色変換 cv::cvtColor(a b CV BGR2Gray)色変換 cv::cvtColor(a,b,CV_BGR2Gray)
戦略戦略• 固定小数点をshortの精度で行う
(精度は悪いけど 気にしない)– (精度は悪いけど,気にしない)
• 48個ロードして,BGRをB16個G16個C16個に並び変え変え
• Shortにアンパックして,R,G,Bの固定小数点演算算
• R,G,Bを加算加算の結果を8ビットシフト• 加算の結果を8ビットシフト
• Shortをパックしてストア
色変換(1/11)cv::cvtColor(a,b,CV_BGR2Gray)
void cvtBGR2GraySSEShort(Mat& src, Mat& dest){
const int size = src.size().area()*src.channels();uchar* s = src.ptr<uchar>(0);uchar s src.ptr uchar (0);uchar* d = dest.ptr<uchar>(0);//スプリットをしてから,RGBからグレイに変換//BBBBBBGGGGGRRRRRへシャッフル(分離)const __m128i mask1 = _mm_setr_epi8(0,3,6,9,12,15,1,4,7,10,13,2,5,8,11,14);const __m128i smask1 = _mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15);const __m128i ssmask1 = _mm_setr_epi8(11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10);
//GGGGGGBBBBBRRRRRへシャッフルconst __m128i mask2 = _mm_setr_epi8(0,3,6,9,12,15, 2,5,8,11,14,1,4,7,10,13);//const __m128i smask2 = _mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15);smask1と同じconst __m128i ssmask2 = _mm_setr_epi8(0,1,2,3,4,11,12,13,14,15,5,6,7,8,9,10);
//RRRRRRGGGGGBBBBBへシャッフル// m128i mask3 = mm setr epi8(0,3,6,9,12,15, 2,5,8,11,14,1,4,7,10,13);//mask2と同じ//__ _ _ _ p ( , , , , , , , , , , , , , , , );// 同//const __m128i smask3 = _mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,6,7,8,9,10);//smask1と同じ//const __m128i ssmask3 = _mm_setr_epi8(11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10);//ssmask1と同じ
//ブレンドマスクconst __m128i bmask1 = _mm_setr_epi8
(255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0);
const __m128i bmask2 = _mm_setr_epi8(255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0);
const __m128i bmask3 = _mm_setr_epi8(255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0);
const __m128i bmask4 = _mm_setr_epi8__ _ _ _ p(255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0);
const int shift = 8;const int amp = 1<<shift;const int _R_=(int)(amp*0.299);const int _G_=(int)(amp*0.587);const int _B_=(int)(amp*0.114);const __m128i R = _mm_set1_epi16(_R_);const __m128i G = _mm_set1_epi16(_G_);const __m128i B = _mm_set1_epi16(_B_);const __m128i zero = _mm_setzero_si128();
for(int i=0;i<size;i+=48){
__m128i a = _mm_shuffle_epi8(_mm_load_si128((__m128i*)(s+i)),mask1);__m128i b = _mm_shuffle_epi8(_mm_load_si128((__m128i*)(s+i+16)),mask2);__m128i c = _mm_shuffle_epi8(_mm_load_si128((__m128i*)(s+i+32)),mask2);const __m128i aaaa = _mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask1),bmask2);
a = _mm_shuffle_epi8(a,smask1);b = _mm_shuffle_epi8(b,smask1);c = _mm_shuffle_epi8(c,smask1);const __m128i bbbb =_mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask3),bmask2);
a = _mm_shuffle_epi8(a,ssmask1);c = _mm_shuffle_epi8(c,ssmask1);b = _mm_shuffle_epi8(b,ssmask2);const __m128i cccc =_mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask3),bmask4);
__m128i a1 = _mm_unpackhi_epi8(aaaa,zero);__m128i a2 = _mm_unpacklo_epi8(aaaa,zero);a1 = _mm_mullo_epi16(a1,B);a2 = _mm_mullo_epi16(a2,B);
__m128i b1 = _mm_unpackhi_epi8(bbbb,zero);__m128i b2 = _mm_unpacklo_epi8(bbbb,zero);b1 = _mm_mullo_epi16(b1,G);b2 = _mm_mullo_epi16(b2,G);
__m128i c1 = _mm_unpackhi_epi8(cccc,zero);__m128i c2 = _mm_unpacklo_epi8(cccc,zero);c1 = _mm_mullo_epi16(c1,R);c2 = _mm_mullo_epi16(c2,R);
a1 = _mm_add_epi16(a1,b1);a1 = _mm_add_epi16(a1,c1);a2 = _mm_add_epi16(a2,b2);a2 = _mm_add_epi16(a2,c2);
a1 = _mm_srli_epi16(a1,8);a2 = _mm_srli_epi16(a2,8);
a = _mm_packus_epi16(a1,a2);
_mm_stream_si128((__m128i*)(d),a);d+=16;
色変換(2/11)cv::cvtColor(a,b,CV_BGR2Gray)
void cvtBGR2GraySSEShort(Mat& src Mat& dest)void cvtBGR2GraySSEShort(Mat& src, Mat& dest){
const int size = src.size().area()*src.channels();uchar* s = src.ptr<uchar>(0);uchar* d = dest.ptr<uchar>(0);//スプリ トをしてから RGBからグレイに変換//スプリットをしてから,RGBからグレイに変換//BBBBBBGGGGGRRRRRへシャッフル(分離)const m128i mask1 =const __m128i mask1
_mm_setr_epi8(0,3,6,9,12,15,1,4,7,10,13,2,5,8,11,14);const __m128i smask1 =
_mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15);const __m128i ssmask1 = t i8(11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10)_mm_setr_epi8(11,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10);
色変換(3/11)cv::cvtColor(a,b,CV_BGR2Gray)//GGGGGGBBBBBRRRRRへシャッフル//GGGGGGBBBBBRRRRRへシャッフルconst __m128i mask2 =
_mm_setr_epi8(0,3,6,9,12,15, 2,5,8,11,14,1,4,7,10,13);//const __m128i smask2 =
_mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,11,12,13,14,15); // k1と同じ//smask1と同じ
const __m128i ssmask2 = mm setr epi8(0 1 2 3 4 11 12 13 14 15 5 6 7 8 9 10);_mm_setr_epi8(0,1,2,3,4,11,12,13,14,15,5,6,7,8,9,10);
//RRRRRRGGGGGBBBBBへシャッフル//__m128i mask3 =
_mm_setr_epi8(0,3,6,9,12,15, 2,5,8,11,14,1,4,7,10,13);//mask2と同じ
// t 128i k3//const __m128i smask3 = _mm_setr_epi8(6,7,8,9,10,0,1,2,3,4,5,6,7,8,9,10);//smask1と同じ
色変換(4/11)cv::cvtColor(a,b,CV_BGR2Gray)//ブレンドマスク//ブレンドマスクconst __m128i bmask1 = _mm_setr_epi8(255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0);const __m128i bmask2 = _mm_setr_epi8(255,255,255,255,255,255,255,255,255,255,255
0 0 0 0 0),0,0,0,0,0);const __m128i bmask3 = _mm_setr_epi8(255 255 255 255 255 0 0 0 0 0 0 0 0 0 0 0);(255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0);const __m128i bmask4 = _mm_setr_epi8(255,255,255,255,255,255,255,255,255,255,0,0,
0,0,0,0);
色変換(5/11)cv::cvtColor(a,b,CV_BGR2Gray)
//固定小数点で色変換の整数演算を定義//固定小数点で色変換の整数演算を定義const int shift = 8;const int amp = 1<<shift;const int _R_=(int)(amp*0.299);const int _G_=(int)(amp*0.587);
t i t B (i t)( *0 114)const int _B_=(int)(amp*0.114);const __m128i R = _mm_set1_epi16(_R_);const m128i G = mm set1 epi16( G );const __m128i G _mm_set1_epi16(_G_);const __m128i B = _mm_set1_epi16(_B_);const __m128i zero = _mm_setzero_si128();
//48( 16x3)画素づつ処理for(int i=0;i<size;i+=48){{
色変換(6/11)cv::cvtColor(a,b,CV_BGR2Gray)
__m128i a = _mm_shuffle_epi8(_mm_load_si128((__m128i*)(s+i)),mask1);
__m128i b = _mm_shuffle_epi8(_mm_load_si128((__m128i*)(s+i+16)),mask2);
128i h ffl i8__m128i c = _mm_shuffle_epi8(_mm_load_si128((__m128i*)(s+i+32)),mask2);
const m128i aaaa = mm blendv epi8const __m128i aaaa = _mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask1),bmask2);
a = _mm_shuffle_epi8(a,smask1);b = _mm_shuffle_epi8(b,smask1);c = _mm_shuffle_epi8(c,smask1);
t 128i bbbb bl d i8const __m128i bbbb =_mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask3),bmask2);
色変換(7/11)cv::cvtColor(a,b,CV_BGR2Gray)
//頑張ってRRRRR GGGGG BBBBB を作る//頑張ってRRRRR.. GGGGG.. BBBBB..を作る__m128i a = _mm_shuffle_epi8
(_mm_load_si128((__m128i*)(s+i)),mask1);__m128i b = _mm_shuffle_epi8
(_mm_load_si128((__m128i*)(s+i+16)),mask2);128i h ffl i8__m128i c = _mm_shuffle_epi8
(_mm_load_si128((__m128i*)(s+i+32)),mask2);const m128i aaaa = mm blendv epi8const __m128i aaaa = _mm_blendv_epi8
(c,_mm_blendv_epi8(b,a,bmask1),bmask2);a = _mm_shuffle_epi8(a,smask1);b = _mm_shuffle_epi8(b,smask1);c = _mm_shuffle_epi8(c,smask1);
t 128i bbbb bl d i8const __m128i bbbb =_mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask3),bmask2);
色変換(8/11)cv::cvtColor(a,b,CV_BGR2Gray)
//頑張ってRRRRR GGGGG BBBBB を作る続き//頑張ってRRRRR.. GGGGG.. BBBBB..を作る続きa = _mm_shuffle_epi8(a,ssmask1);c = _mm_shuffle_epi8(c,ssmask1);b = _mm_shuffle_epi8(b,ssmask2);const __m128i cccc
bl d i8( bl d i8(b b k3)=_mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask3),bmask4);
__m128i a1 = _mm_unpackhi_epi8(aaaa,zero);
__m128i a2 = _mm_unpacklo_epi8(aaaa,zero);
1 ll i16( 1 B)a1 = _mm_mullo_epi16(a1,B);a2 = _mm_mullo_epi16(a2,B);
色変換(9/11)cv::cvtColor(a,b,CV_BGR2Gray)
//頑張ってRRRRR GGGGG BBBBB を作る続き//頑張ってRRRRR.. GGGGG.. BBBBB..を作る続きa = _mm_shuffle_epi8(a,ssmask1);c = _mm_shuffle_epi8(c,ssmask1);b = _mm_shuffle_epi8(b,ssmask2);const __m128i cccc
bl d i8( bl d i8(b b k3)=_mm_blendv_epi8(c,_mm_blendv_epi8(b,a,bmask3),bmask4);//完成//完成//aaaa:BBBB BBBB BBBB BBBB//bbbb:GGGGGGGGGGGGGGGG//cccc :RRRR RRRR RRRR RRRR
色変換(10/11)cv::cvtColor(a,b,CV_BGR2Gray)
//shortにアンパックして乗算上位 下位に分けて乗算//shortにアンパックして乗算上位,下位に分けて乗算__m128i a1 = _mm_unpackhi_epi8(aaaa,zero);
__m128i a2 = _mm_unpacklo_epi8(aaaa,zero);a1 = _mm_mullo_epi16(a1,B);a2 = _mm_mullo_epi16(a2,B);
128i b1 khi i8(bbbb )__m128i b1 = _mm_unpackhi_epi8(bbbb,zero);__m128i b2 = _mm_unpacklo_epi8(bbbb,zero);
b1 = mm mullo epi16(b1 G);b1 _mm_mullo_epi16(b1,G);b2 = _mm_mullo_epi16(b2,G);
__m128i c1 = _mm_unpackhi_epi8(cccc,zero);__m128i c2 = _mm_unpacklo_epi8(cccc,zero);
c1 = _mm_mullo_epi16(c1,R);2 ll i16( 2 R)c2 = _mm_mullo_epi16(c2,R);
色変換(11/11)cv::cvtColor(a,b,CV_BGR2Gray)
//半分づつに分けた値を加算//半分づつに分けた値を加算a1 = _mm_add_epi16(a1,b1);a1 = _mm_add_epi16(a1,c1);a2 = _mm_add_epi16(a2,b2);a2 = _mm_add_epi16(a2,c2);
//8bit シフトによる割り算固定小数点で8bitシフト//8bitsシフトによる割り算,固定小数点で8bitシフトa1 = _mm_srli_epi16(a1,8);a2 = mm srli epi16(a2 8);a2 _mm_srli_epi16(a2,8);
//shortのデータをパックしてストアa = _mm_packus_epi16(a1,a2);_mm_store_si128((__m128i*)(d),a);d+=16;
}}}
結果結果
O C 8 0• OpenCV 840.7 ms• C++ 519.4 ms (固定小数点)• SSE 306.9 ms (アライメント無視)
• SSE 172.7 msSSE 172.7 msコメント
16ビットに桁を落としたので 精度が悪くなっているが– 16ビットに桁を落としたので,精度が悪くなっているが,かなりの高速化に成功
(JPEGなどの符号化など色変換でビットが落ちて画像品質が落ちると困( な の符号化な 色変換で ット 落ちて画像品質 落ちる 困るものには向かないが,画像処理の工程で色変換の精度が高くは必要のないものも多数)
新しく使ったその他関数一覧新しく使ったその他関数 覧
• Mullo
• BlendBlend
• Insert
• extract
乗算 mm mullo epi乗算_mm_mullo_epi
• _mm_mullo_epi(__m128i a, __m128i b)
• A,bの16bit同士の積を8つ同時に行う.桁のあふれは無視あふれは無視
• _mm_mulhi_epi(__m128i a, __m128i b)
が上位ビ 演算• が上位ビットの演算
挿入 インサート挿入 インサート
指定• 指定して代入
• _mm_insert_epi8(__m128i a,int b,int n);
• Aのn番目のレジスタに値bを書きこむAのn番目のレジスタに値bを書きこむ
抽出 エクストラクト抽出 エクストラクト
• int r = _mm_extract_epi8(__m128i a, int n)
• レジスタaの15番目の値を抽出してrに代入レジスタaの15番目の値を抽出してrに代入
マスク付き代入 ブレンドマスク付き代入 ブレンド
部分 制御• マスクの部分で代入のオンオフ制御
• _mm_blendv_epi8 (__m128 a, __m128 b, __m128 mask)
• Maskの値で出力をでaにするかbにするか制御できる
まとめまとめ
組 数 を使 ば組み込み関数(intrinsic)を使えばOpenCVを超える速度の実装がp 装
結構簡単に出来てしまう!?
※ただし,16の倍数に限る
※ただし では今日紹介した関数はほとんど で( 命令※ただし,IPPでは今日紹介した関数はほとんどSSEで(AVX命令を含む)実装されている
※O CVは全てのIPP関数をラップしているわけではないので※OpenCVは全てのIPP関数をラップしているわけではないので
Appendix
SIMDの歴史(Intel)SIMDの歴史(Intel)64bitレジスタ m6464bitレジスタ__m64• MMXMMX PentiumとかPentiumII (1997)
b レジスタ128bitレジスタ__m128• SSE Pentium III(1999):浮動小数点に対応• SSE2 Pentium 4(2000):整数,倍精度浮動小数点• SSE3 Pentium 4(2004)の後期:• SSSE3 Pentium 4(2006)の後期:• SSE4.1 Core 2 の前期(2007)前期( )• SSE4.2 Core 2 の後期,Core i7(2008)
256bitレジスタ m256256bitレジスタ__m256• AVX Core i7 Sandy Bridge(2011.1 ):浮動小数点に対応• AVX2? 整数が256bitに対応予定
備考備考
対応する バ ジ が な• CPUによって対応するSSEのバージョンが異なるため,CPUによっては使えない関数がある
• 結構便利な関数ほど上位のSSEのバージョンで定義されているで定義されている.
今回の関数のSSEの対応今回の関数のSSEの対応
• SSE
• SSE2SSE2
• SSE3
• SSSE3
• SSE4 1SSE4.1
• __m128i _mm_cvtepi8_epi16 (__m128i a)
使っているCPUがどこまでSSEをサポートしているか
void checkCPU(void){
int CPUInfo[4];int InfoType = 1;int InfoType = 1;__cpuid(CPUInfo, InfoType);
if (CPUInfo[3] & 0x00800000) printf("support MMX¥n");else printf("DO NOT support MMX¥n");else printf( DO NOT support MMX¥n );if (CPUInfo[3] & 0x02000000) printf("support SSE¥n");else printf("DO NOT support SSE¥n");if (CPUInfo[3] & 0x04000000) printf("support SSE2¥n");else printf("DO NOT support SSE2¥n");else printf( DO NOT support SSE2¥n );if (CPUInfo[2] & 0x00000001) printf("support SSE3¥n");else printf("DO NOT support SSE3¥n");if (CPUInfo[2] & 0x00080000) printf("support SSE4.1¥n");else printf("DO NOTsupport SSE4 1¥n");else printf( DO NOTsupport SSE4.1¥n );if (CPUInfo[2] & 0x00100000) printf("support SSE4.2¥n");else printf("DO NOT support SSE4.2¥n");return ;
}
OpenCV.jpクックブックの「CPUがサポートする機能(SSEなど)をチェックする」よりhttp://opencv.jp/cookbook/opencv_utility.html#cpu‐sse
}
マニュアルhttp://software intel com/en us/avx/http://software.intel.com/en‐us/avx/
128 単精度• __m128 単精度• __m128d倍精度
整数• __m128i 整数
• _mm_op_suffix• S:シングル• D:ダブル• i# #={8,16,32,64,128}符号あり整数{ , , , , }符号あり整数• u# #={8,16,32,64,128}符号なし整数
キャッシュ制御 Streamキャッシュ制御 Stream
キ シ を使わずにメモリに読み書きする関数• キャッシュを使わずにメモリに読み書きする関数• _mm_stream_load_si128
i128• _mm_stream_si128
キ シ を汚したくな ときに使うキャッシュを汚したくないときに使う• 読みこんだデータはもう使わないとき
出力デ タをしばらく使わな とき• 出力データをしばらく使わないとき
• 画像データが大きくて書きこみをキャッシュに残さないほうがいいときとかうがいいときとか
– prefetch