SIMD/SSE初心者:シンプルな画像フィルタリング
-
01-10-2019 - |
質問
私はSIMD/SSEを非常に初めて使用していて、単純な画像フィルタリング(ぼやけ)を実行しようとしています。以下のコードは、8ビットグレービットマップの各ピクセルを、水平方向に単純な[1 2 1]重み付けでろ過します。一度に16ピクセルの合計を作成しています。
少なくとも私にとっては、このコードの非常に悪いと思われるのは、その中に多くの挿入/抽出物があることです。これはあまりエレガントではなく、おそらくすべてを遅くしています。シフトするときに、あるREGから別のREGにデータをラップするより良い方法はありますか?
BUFは画像データであり、16バイトアライメントされています。 w/hは幅と高さ、16の倍数です。
__m128i *p = (__m128i *) buf;
__m128i cur1, cur2, sum1, sum2, zeros, tmp1, tmp2, saved;
zeros = _mm_setzero_si128();
short shifted, last = 0, next;
// preload first row
cur1 = _mm_load_si128(p);
for (x = 1; x < (w * h) / 16; x++) {
// unpack
sum1 = sum2 = saved = cur1;
sum1 = _mm_unpacklo_epi8(sum1, zeros);
sum2 = _mm_unpackhi_epi8(sum2, zeros);
cur1 = tmp1 = sum1;
cur2 = tmp2 = sum2;
// "middle" pixel
sum1 = _mm_add_epi16(sum1, sum1);
sum2 = _mm_add_epi16(sum2, sum2);
// left pixel
cur2 = _mm_slli_si128(cur2, 2);
shifted = _mm_extract_epi16(cur1, 7);
cur2 = _mm_insert_epi16(cur2, shifted, 0);
cur1 = _mm_slli_si128(cur1, 2);
cur1 = _mm_insert_epi16(cur1, last, 0);
sum1 = _mm_add_epi16(sum1, cur1);
sum2 = _mm_add_epi16(sum2, cur2);
// right pixel
tmp1 = _mm_srli_si128(tmp1, 2);
shifted = _mm_extract_epi16(tmp2, 0);
tmp1 = _mm_insert_epi16(tmp1, shifted, 7);
tmp2 = _mm_srli_si128(tmp2, 2);
// preload next row
cur1 = _mm_load_si128(p + x);
// we need the first pixel of the next row for the "right" pixel
next = _mm_extract_epi16(cur1, 0) & 0xff;
tmp2 = _mm_insert_epi16(tmp2, next, 7);
// and the last pixel of last row for the next "left" pixel
last = ((uint16_t) _mm_extract_epi16(saved, 7)) >> 8;
sum1 = _mm_add_epi16(sum1, tmp1);
sum2 = _mm_add_epi16(sum2, tmp2);
// divide
sum1 = _mm_srli_epi16(sum1, 2);
sum2 = _mm_srli_epi16(sum2, 2);
sum1 = _mm_packus_epi16(sum1, sum2);
mm_store_si128(p + x - 1, sum1);
}
解決
SSEレジスタに隣接するピクセルを保持することをお勧めします。つまり、_MM_SLLI_SI128 / _MM_SRLI_SI128の結果をSSE変数に保持し、すべての挿入と抽出物を排除します。私の理由は、古いCPUでは、挿入/抽出命令には、SSEユニットと汎用ユニット間の通信が必要であり、L1キャッシュに波及したとしても、SSE内で計算を維持するよりもはるかに遅いということです。
それが完了したら、4つの16ビットシフト(_MM_SLLI_SI128、_MM_SRLI_SI128、_MM_SRLI_SI128のみが必要なはずです。 ディビソンシフトをカウントしません )。私の提案は、あなたのコードがすでにメモリ帯域幅の制限にヒットしている可能性があるため、コードでベンチマークを行うことです。つまり、もう最適化できないことを意味します。
画像が大きく(L2サイズよりも大きく)、出力をすぐに読み戻すことができない場合は、書き戻すためにMOVNTDQ(_MM_STREAM_SI128)を使用してみてください。いくつかのWebサイトによると、SSE2にありますが、ダブルチェックしたい場合があります。
SIMDチュートリアル:
- http://www.tommesani.com/docs.html
- http://en.wikipedia.org/wiki/x86_instruction_listings#sse2_instructions
いくつかのSIMDの第一人者のウェブサイト:
他のヒント
この種の近隣操作は、SSE3.5(別名SSSE3)が登場し、Palignr(_MM_ALIGNR_EPI8)が導入されるまで、常にSSEの痛みでした。
ただし、SSE2/SSE3との後方互換性が必要な場合は、SSE2/SSE3の_MM_AlignR_EPI8をエミュレートし、SSE3.5/SSE4をターゲットにするときに_MM_AlignR_EPI8にドロップする同等のマクロまたはインライン関数を記述できます。
もう1つのアプローチは、誤った荷重を使用してシフトしたデータを取得することです。これは、古いCPUで比較的高価です(潜在性の約2倍、整列した負荷のスループットの半分)が、これは負荷ごとに行う多くの計算に応じて許容される場合があります。また、現在のIntel CPU(Core i7)の誤った負荷が整列した負荷と比較してペナルティがないため、コードはCore i7で非常に効率的になるという利点があります。 et al.