見出し画像

画像解析を高速化させるためのTipsわかりやすくまとめてみた(5)

前回(第4回)は、整数型での四捨五入を、AVX2の整数型で実装しました。

今回は、もっと単純なお題で、SSE2とAVX2の比較をしてみたいと思います。

お題

乱数の入った80000000個の符号なし8bit整数型の配列を2つ(X, Y)を用意し、XとYの要素同士の平均値Zを求めます(ただし、小数点以下は切り上げ)。つまり、Z[i] = ceil(X[i] + Y[i]) です。

実装

前回までは、32bit整数型でしたが、今回は8bit整数型です。SSE2の場合はレジスタが128bitなので16個、AVX2の場合はレジスタが256bitなので32個、同時に計算できることになります。

さらに、なんと、SSE2とAVX2では、平均値の計算が1命令でできちゃいます。(_mm_avg_si128, _mm256_avg_si256)

// 通常版
uint8_t average_normal(uint8_t x, uint8_t y)
{
   return (x + y + 1) >> 1;
}

// SSE2版
void average_sse2(const __m128i* px, const __m128i* py, __m128i* pans)
{
   __m128i mx = _mm_load_si128(px);
   __m128i my = _mm_load_si128(py);
   __m128i mans = _mm_avg_epu8(mx, my);
   _mm_store_si128(pans, mans);
}

// AVX2版
void average_avx2(const __m256i* px, const __m256i* py, __m256i* pans)
{
   __m256i mx = _mm256_load_si256(px);
   __m256i my = _mm256_load_si256(py);
   __m256i mans = _mm256_avg_epu8(mx, my);
   _mm256_store_si256(pans, mans);
}
// ベンチマーク
void benchmark(uint8_t (*func)(uint8_t, uint8_t), int n, const uint8_t* x, const uint8_t* y, uint8_t* ans)
{
   auto start = std::chrono::system_clock::now();

   for (int i = 0; i < n; ++i) {
       ans[0] = func(x[0], y[0]);
   }

   auto end = std::chrono::system_clock::now();
   auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
   std::cout << elapsed << std::endl;
}

void benchmark_sse2(void (*func)(const __m128i* x, const __m128i* y , __m128i* ans), int n, const uint8_t* x, const uint8_t* y, uint8_t* ans)
{
   auto start = std::chrono::system_clock::now();

   for (int i = 0; i < n; i += 16) {
       func(reinterpret_cast<const __m128i*>(&x[0]), reinterpret_cast<const __m128i*>(&y[0]), reinterpret_cast<__m128i*>(&ans[0]));
   }

   auto end = std::chrono::system_clock::now();
   auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
   std::cout << elapsed << std::endl;
}

void benchmark_avx2(void (*func)(const __m256i* x, const __m256i* y , __m256i* ans), int n, const uint8_t* x, const uint8_t* y, uint8_t* ans)
{
   auto start = std::chrono::system_clock::now();

   for (int i = 0; i < n; i += 32) {
       func(reinterpret_cast<const __m256i*>(&x[0]), reinterpret_cast<const __m256i*>(&y[0]), reinterpret_cast<__m256i*>(&ans[0]));
   }

   auto end = std::chrono::system_clock::now();
   auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
   std::cout << elapsed << std::endl;
}
int main()
{
   static const int N = 800000000;

   std::random_device rd;
   std::mt19937 mt(rd());
   std::uniform_int_distribution<> rand256(0, 255);

   std::vector<uint8_t, aligned_allocator<uint8_t, 32>> x(N);
   std::vector<uint8_t, aligned_allocator<uint8_t, 32>> y(N);
   std::vector<uint8_t, aligned_allocator<uint8_t, 32>> ans1(N);
   std::vector<uint8_t, aligned_allocator<uint8_t, 32>> ans2(N);
   std::vector<uint8_t, aligned_allocator<uint8_t, 32>> ans3(N);
   std::generate(x.begin(), x.end(), [&](){ return rand256(mt); });
   std::generate(y.begin(), y.end(), [&](){ return rand256(mt); });

   benchmark(average_normal, N, x.data(), y.data(), ans1.data());
   benchmark_sse2(average_sse2, N, x.data(), y.data(), ans2.data());
   benchmark_avx2(average_avx2, N, x.data(), y.data(), ans3.data());

   std::cout << "ans1 == ans2: " << std::equal(ans1.begin(), ans1.end(), ans2.begin()) << std::endl;
   std::cout << "ans1 == ans3: " << std::equal(ans1.begin(), ans1.end(), ans3.begin()) << std::endl;

   return 0;
}

ベンチマーク

実行環境:
・CPU: 8th Generation Core i7
・OS: Ubuntu (WSL2)
・Compiler: clang version 6.0.0-1ubuntu2
・Compile Options (通常版): -O3
・Compile Options (SSE2版): -O3 -msse2
・Compile Options (AVX2版): -O3 -mavx2

ベンチマーク結果(5試行平均):

画像1

ほとんどかわりませんね…😇

おそらく、メモリアクセスに時間がかかっているのでは…

そこで、メモリアクセスをなくすため、80000000個の配列すべてを計算するのではなく、配列の先頭部分だけを80000000回繰り返し計算してみましょう。

ベンチマーク結果(5試行平均):

画像2

やりました! SSE2→AVX2で、約1.8倍になりました!

<次回に続く>

メンバー募集中です
アダコテックは上記のような画像処理技術を使って、大手メーカーの検査ラインを自動化するソフトウェアを開発している会社です。
機械学習や画像処理の内部ロジックに興味がある方、ご連絡下さい!
我々と一緒にモノづくりに革新を起こしましょう!


いいなと思ったら応援しよう!