Я пытаюсь написать очень эффективный код Хэмминга. Вдохновленный Wojciech Muła чрезвычайно умный SSE3 popcount реализация, я закодировал эквивалентное решение AVX2, на этот раз используя 256-битные регистры. l ожидал улучшения на 30% -40% на основе удвоенного parallelism задействованных операций, однако, к моему удивлению, код AVX2 немного медленнее (около 2%)!
Может ли кто-нибудь просветить меня о возможных причинах, по которым я не ожидаю повышения производительности?
Unrolled, SSE3 Расстояние Хэмминга двух 64-байтовых блоков:
INT32 SSE_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m128i paccum = _mm_setzero_si128();
__m128i a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA));
__m128i b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB));
__m128i err = _mm_xor_si128 (a, b);
__m128i lo = _mm_and_si128 (err, low_mask);
__m128i hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
__m128i popcnt1 = _mm_shuffle_epi8(lookup, lo);
__m128i popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 4));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 4));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 8));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 8));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
a = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pA + 12));
b = _mm_loadu_si128 (reinterpret_cast<const __m128i*>(pB + 12));
err = _mm_xor_si128 (a, b);
lo = _mm_and_si128 (err, low_mask);
hi = _mm_srli_epi16 (err, 4);
hi = _mm_and_si128 (hi, low_mask);
popcnt1 = _mm_shuffle_epi8(lookup, lo);
popcnt2 = _mm_shuffle_epi8(lookup, hi);
paccum = _mm_add_epi8(paccum, popcnt1);
paccum = _mm_add_epi8(paccum, popcnt2);
paccum = _mm_sad_epu8(paccum, _mm_setzero_si128());
UINT64 result = paccum.m128i_u64[0] + paccum.m128i_u64[1];
return (INT32)result;
}
Ununrolled, эквивалентная версия с использованием 256-битных регистров AVX:
INT32 AVX_PopCount(const UINT32* __restrict pA, const UINT32* __restrict pB) {
__m256i paccum = _mm256_setzero_si256();
__m256i a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA));
__m256i b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB));
__m256i err = _mm256_xor_si256 (a, b);
__m256i lo = _mm256_and_si256 (err, low_mask256);
__m256i hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
__m256i popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
__m256i popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
a = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pA + 8));
b = _mm256_loadu_si256 (reinterpret_cast<const __m256i*>(pB + 8));
err = _mm256_xor_si256 (a, b);
lo = _mm256_and_si256 (err, low_mask256);
hi = _mm256_srli_epi16 (err, 4);
hi = _mm256_and_si256 (hi, low_mask256);
popcnt1 = _mm256_shuffle_epi8(lookup256, lo);
popcnt2 = _mm256_shuffle_epi8(lookup256, hi);
paccum = _mm256_add_epi8(paccum, popcnt1);
paccum = _mm256_add_epi8(paccum, popcnt2);
paccum = _mm256_sad_epu8(paccum, _mm256_setzero_si256());
UINT64 result = paccum.m256i_i64[0] + paccum.m256i_u64[1] + paccum.m256i_i64[2] + paccum.m256i_i64[3];
return (INT32)result;
}
Я уже проверил код выходной сборки, испускаемый компилятором, и он выглядит хорошо, с ожидаемым прямым переводом встроенной инструкции на машинную инструкцию. Единственное, что я заметил, это то, что в версии AVX2 последняя строка, в которой накапливается совокупность четырех четырехсловных слов, генерируется более сложный код, чем версия SSE3 (где нужно всего лишь накапливать только два квад-слова для получения подсчет населения), однако я бы все же ожидал более высокую пропускную способность.
Код AVX2, созданный для накопления четырехъядерных слов
vextractf128 xmm0, ymm2, 1
psrldq xmm0, 8
movd ecx, xmm2
movd eax, xmm0
vextractf128 xmm0, ymm2, 1
psrldq xmm2, 8
add eax, ecx
movd ecx, xmm0
add eax, ecx
movd ecx, xmm2
add eax, ecx
Код SSE3, созданный для накопления четырехъядерных слов
movd ecx, xmm2
psrldq xmm2, 8
movd eax, xmm2
add eax, ecx
Моя тестовая программа вызывается по 1 миллиону раз в каждой процедуре с разными входными значениями, но повторно использует два статических буфера для хранения данных параметров pA
и pB
. В моем ограниченном понимании архитектуры процессора эта локальность (повторное использование одних и тех же буферов памяти снова и снова) должна хорошо подогревать кэширование процессора и не зависеть от проблемы с пропускной способностью памяти, но, помимо возможности пропускной способности памяти, я не могу понять, почему нет улучшения производительности.
Процедура тестирования
int _tmain(int argc, _TCHAR* argv[]) {
lookup = _mm_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask = _mm_set1_epi8(0xf);
lookup256 = _mm256_setr_epi8(
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4,
/* 0 */ 0, /* 1 */ 1, /* 2 */ 1, /* 3 */ 2,
/* 4 */ 1, /* 5 */ 2, /* 6 */ 2, /* 7 */ 3,
/* 8 */ 1, /* 9 */ 2, /* a */ 2, /* b */ 3,
/* c */ 2, /* d */ 3, /* e */ 3, /* f */ 4
);
low_mask256 = _mm256_set1_epi8(0xf);
std::default_random_engine generator;
generator.seed(37);
std::uniform_int_distribution<UINT32> distribution(0, ULONG_MAX);
auto dice = std::bind( distribution, generator);
UINT32 a[16];
UINT32 b[16];
int count;
count = 0;
{
cout << "AVX PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice();
b[j] = dice();
}
count+= AVX_PopCount(a, b);
}
}
cout << count << "\r\n";
std::default_random_engine generator2;
generator2.seed(37);
std::uniform_int_distribution<UINT32> distribution2(0, ULONG_MAX);
auto dice2 = std::bind( distribution2, generator2);
count = 0;
{
cout << "SSE PopCount\r\n";
boost::timer::auto_cpu_timer t;
for( int i = 0; i < 1000000; i++ ) {
for( int j = 0; j < 16; j++ ) {
a[j] = dice2();
b[j] = dice2();
}
count+= SSE_PopCount(a, b);
}
}
cout << count << "\r\n";
getch();
return 0;
}
Тест-машина - это Intel Corei7 4790, и я использую Visual Studio 2012 Pro.