Быстрый popcount на Intel Xeon Phi
Я внедряю сверхбыстрый поп-счет на Intel Xeon® Phi®, так как он является точкой доступа к производительности различных программ для биоинформатики.
Я реализовал пять частей кода,
#if defined(__MIC__)
#include <zmmintrin.h>
__attribute__((align(64))) static const uint32_t POPCOUNT_4bit[16] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
__attribute__((align(64))) static const uint32_t MASK_4bit[16] = {0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF};
inline uint64_t vpu_popcount1(uint64_t* buf, size_t n) {
register size_t result = 0;
size_t i;
register const __m512i popcnt = _mm512_load_epi32((void*)POPCOUNT_4bit);
register const __m512i mask = _mm512_load_epi32((void*)MASK_4bit);
register __m512i total;
register __m512i shuf;
#pragma unroll(8)
for (i = 0; i < n; i+=8) {
shuf = _mm512_load_epi32(&buf[i]);
_mm_prefetch((const char *)&buf[i+256], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+64], _MM_HINT_T0); // vprefetch0
total = _mm512_setzero_epi32();
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(shuf, mask), popcnt), total);
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(_mm512_srli_epi32(shuf, 4), mask), popcnt), total);
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(_mm512_srli_epi32(shuf, 8), mask), popcnt), total);
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(_mm512_srli_epi32(shuf, 12), mask), popcnt), total);
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(_mm512_srli_epi32(shuf, 16), mask), popcnt), total);
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(_mm512_srli_epi32(shuf, 20), mask), popcnt), total);
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(_mm512_srli_epi32(shuf, 24), mask), popcnt), total);
total = _mm512_add_epi32(_mm512_permutevar_epi32(_mm512_and_epi32(_mm512_srli_epi32(shuf, 28), mask), popcnt), total);
/* Reduce add, which is analogous to SSSE3's PSADBW instruction,
is not implementated as a single instruction in VPUv1, thus
emulated by multiple instructions*/
result += _mm512_reduce_add_epi32(total);
}
return result;
}
__attribute__((align(64))) static const unsigned magic[] = {\
0x55555555, 0x55555555, 0x55555555, 0x55555555,\
0x55555555, 0x55555555, 0x55555555, 0x55555555,\
0x55555555, 0x55555555, 0x55555555, 0x55555555,\
0x55555555, 0x55555555, 0x55555555, 0x55555555,\
0x33333333, 0x33333333, 0x33333333, 0x33333333,\
0x33333333, 0x33333333, 0x33333333, 0x33333333,\
0x33333333, 0x33333333, 0x33333333, 0x33333333,\
0x33333333, 0x33333333, 0x33333333, 0x33333333,\
0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F,\
0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F,\
0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F,\
0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F, 0x0F0F0F0F,\
0x00FF00FF, 0x00FF00FF, 0x00FF00FF, 0x00FF00FF,\
0x00FF00FF, 0x00FF00FF, 0x00FF00FF, 0x00FF00FF,\
0x00FF00FF, 0x00FF00FF, 0x00FF00FF, 0x00FF00FF,\
0x00FF00FF, 0x00FF00FF, 0x00FF00FF, 0x00FF00FF,\
0x0000FFFF, 0x0000FFFF, 0x0000FFFF, 0x0000FFFF,\
0x0000FFFF, 0x0000FFFF, 0x0000FFFF, 0x0000FFFF,\
0x0000FFFF, 0x0000FFFF, 0x0000FFFF, 0x0000FFFF,\
0x0000FFFF, 0x0000FFFF, 0x0000FFFF, 0x0000FFFF,\
0x000000FF, 0x000000FF, 0x000000FF, 0x000000FF,\
0x000000FF, 0x000000FF, 0x000000FF, 0x000000FF,\
0x000000FF, 0x000000FF, 0x000000FF, 0x000000FF,\
0x000000FF, 0x000000FF, 0x000000FF, 0x000000FF
};
inline uint64_t vpu_popcount2(uint64_t* buf, size_t n) {
register size_t result = 0;
size_t i;
register const __m512i B0 = _mm512_load_epi32((void*)(magic+0));
register const __m512i B1 = _mm512_load_epi32((void*)(magic+16));
register const __m512i B2 = _mm512_load_epi32((void*)(magic+32));
register const __m512i B3 = _mm512_load_epi32((void*)(magic+48));
register const __m512i B4 = _mm512_load_epi32((void*)(magic+64));
register __m512i total;
register __m512i shuf;
#pragma unroll(8)
for (i = 0; i < n; i+=8) {
shuf = _mm512_load_epi32(&buf[i]);
_mm_prefetch((const char *)&buf[i+512], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+64], _MM_HINT_T0); // vprefetch0
total = _mm512_sub_epi32(shuf, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf,1)));
total = _mm512_add_epi32(_mm512_and_epi32(B1, total), _mm512_and_epi32(B1,_mm512_srli_epi32(total,2)));
total = _mm512_and_epi32(B2, _mm512_add_epi32(total, _mm512_srli_epi32(total,4)));
total = _mm512_and_epi32(B3, _mm512_add_epi32(total, _mm512_srli_epi32(total,8)));
total = _mm512_and_epi32(B4, _mm512_add_epi32(total, _mm512_srli_epi32(total,16)));
/* Reduce add, which is analogous to SSSE3's PSADBW instruction,
is not implementated as a single instruction in VPUv1, thus
emulated by multiple instructions*/
result += _mm512_reduce_add_epi32(total);
}
return result;
}
inline uint64_t vpu_popcount3(uint64_t* buf, size_t n) {
register size_t result = 0;
size_t i;
register const __m512i B0 = _mm512_load_epi32((void*)(magic+0));
register const __m512i B1 = _mm512_load_epi32((void*)(magic+16));
register const __m512i B2 = _mm512_load_epi32((void*)(magic+32));
register const __m512i B3 = _mm512_load_epi32((void*)(magic+48));
register const __m512i B4 = _mm512_load_epi32((void*)(magic+64));
register __m512i total;
register __m512i shuf;
#pragma unroll(4)
for (i = 0; i < n; i+=16) {
shuf = _mm512_load_epi32(&buf[i]);
result += _mm_countbits_64(buf[i+8]);
_mm_prefetch((const char *)&buf[i+512], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+576], _MM_HINT_T1); // vprefetch1
result += _mm_countbits_64(buf[i+9]);
_mm_prefetch((const char *)&buf[i+64], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+128], _MM_HINT_T0); // vprefetch0
total = _mm512_sub_epi32(shuf, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf,1)));
result += _mm_countbits_64(buf[i+10]);
total = _mm512_add_epi32(_mm512_and_epi32(B1, total), _mm512_and_epi32(B1,_mm512_srli_epi32(total,2)));
result += _mm_countbits_64(buf[i+11]);
total = _mm512_and_epi32(B2, _mm512_add_epi32(total, _mm512_srli_epi32(total,4)));
result += _mm_countbits_64(buf[i+12]);
total = _mm512_and_epi32(B3, _mm512_add_epi32(total, _mm512_srli_epi32(total,8)));
result += _mm_countbits_64(buf[i+13]);
total = _mm512_and_epi32(B4, _mm512_add_epi32(total, _mm512_srli_epi32(total,16)));
result += _mm_countbits_64(buf[i+14]);
/* Reduce add, which is analogous to SSSE3's PSADBW instruction,
is not implementated as a single instruction in VPUv1, thus
emulated by multiple instructions*/
result += _mm512_reduce_add_epi32(total);
result += _mm_countbits_64(buf[i+15]);
}
return result;
}
/* Using VPU or SSE's machine intrinsic, CPUs not supporting SIMD
* will use compiler's implementation, the speed of which depends */
static inline size_t scalar_popcountu(unsigned *buf, size_t n) {
register size_t cnt = 0;
size_t i;
#pragma vector always
#pragma unroll(8)
for (i = 0; i < n; i++) {
cnt += _mm_countbits_32(buf[i]);
_mm_prefetch((const char *)&buf[i+512], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+64], _MM_HINT_T0); // vprefetch0
}
return cnt;
}
static inline size_t scalar_popcountlu(uint64_t *buf, size_t n) {
register size_t cnt = 0;
size_t i;
#pragma vector always
#pragma unroll(8)
for (i = 0; i < n; i++) {
cnt += _mm_countbits_64(buf[i]);
_mm_prefetch((const char *)&buf[i+512], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+64], _MM_HINT_T0); // vprefetch0
}
return cnt;
}
#endif
Завершение кода с поддержкой OpenMP можно загрузить по https://www.dropbox.com/sh/b3sfqps19wa2oi4/iFQ9wQ1NTg
Код был скомпилирован с использованием компилятора Intel C/C++ XE 13 с помощью команды:
icc -debug inline-debug-info -O3 -mmic -fno-alias -ansi-alias -opt-streaming-stores always -ipo popcnt-mmic.cpp -o popcnt-mmic -vec-report=2 -openmp
Код изначально работает на сопроцессоре (61 ядро) с "122 потоками" и привязкой потоков как "сбалансированной" с использованием экспорта:
export OMP_NUM_THREADS=122;export KMP_AFFINITY=balanced
Я использую Xeon Phi SE10p, степпинг B1, CentOS6.4. Тестирование на 28 мегабайтах джонков (заполненных rand()) и повторение 10000 раз, производительность следующая:
Buffer allocated at: 0x7f456b000000
OpenMP scalar_popcountu 4310169 us; cnt = 28439328
OpenMP scalar_popcountlu 1421139 us; cnt = 28439328
OpenMP vpu_popcount 1489992 us; cnt = 28439328
OpenMP vpu_popcount2 1109530 us; cnt = 28439328
OpenMP vpu_popcount3 951122 us; cnt = 28439328
"Scalar_popcountu" и "scalar_popcountlu" используют "_mm_countbits_32" и "_mm_countbits_64" соответственно, которые используют скалярную инструкцию "popcnt". Установка "#pragma vector always" просит компилятор векторизовать загрузку и суммировать как 16 беззнаковых целых или 8 беззнаковых длинных, хотя сам по себе popcount все еще является скалярной инструкцией.
Реализация vpu_popcount1 аналогична реализации попкорна SSSE3 http://wm.ite.pl/articles/sse-popcount.html. Тем не менее, 1) Xeon Phi не поддерживает упакованные байтовые операции с целым числом (минимум - двойные слова, иначе 32-разрядные) и 2) он не реализует инструкцию "Упакованная сумма абсолютной разности" (например, _mm_sad_epu8 в SSSE3), таким образом добавление сокращения было выполнено комбинацией четырех групп "vpermf32x4", "vpaddd" и "movslq". Таким образом, реализация генерирует гораздо больше инструкций, чем оригинальная версия SSSE3.
Реализация vpu_popcount2 аналогична реализации popcount SSE2 (можно обратиться к "Восторгу хакера"). Реализация генерирует меньше инструкций, чем vpu_popcount1, и примерно на 30% быстрее. Тем не менее, утомительного "уменьшить добавление" по-прежнему нельзя избежать.
Реализация vpu_popcount3 очень специфична для Xeon Phi. Благодаря сочетанию векторных и скалярных операций, это примерно на 15% быстрее, чем vpu_popcount2 (в моей реализации свободное распространение скалярных операций на фоне векторных операций является досугом, можно переставить скалярные операции в соответствии с кодом сборки, сгенерированным компилятором, но ожидаемым улучшением ограничен, насколько я обеспокоен). Улучшение основано на наблюдении, что 1) Xeon Phi - это планирование по порядку, 2) две скалярные команды или команды "1 вектор + 1 скаляр" могут быть выданы за такт. Я уменьшил развертку с 8 до 4, чтобы избежать насыщения файла реестра.
Явная предварительная выборка из памяти в циклы L2 8 заранее и из циклов L2 в L1 1 заранее в каждой функции увеличила коэффициент попадания L1 с 0,38 до 0,994.
Развертывание увеличивает производительность примерно на 15%. Это противоречит интуиции, поскольку Xeon Phi находится в порядке планирования. Но развертывание позволяет компилятору icc выполнять как можно больше планирования времени компиляции.
Есть ли у нас еще больше техники для повышения производительности?
Два куска быстрых кодов от Брайана Никерсона,
OpenMP vpu_popcount2 1110737 us; cnt = 28439328
OpenMP vpu_popcount3 951459 us; cnt = 28439328
OpenMP vpu_popcount3_r 815126 us; cnt = 28439328
OpenMP vpu_popcount5 746852 us; cnt = 28439328
vpu_popcount3_revised:
inline uint64_t vpu_popcount3_revised(uint64_t* buf, size_t n) {
_mm_prefetch((const char *)&buf[0], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[8], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[16], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[24], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[32], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[40], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[48], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[56], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[64], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[72], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[80], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[88], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[96], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[104], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[112], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[120], _MM_HINT_T1); // vprefetch1
register size_t result;
size_t i;
register const __m512i B0 = _mm512_load_epi32((void*)(magic+0));
register const __m512i B1 = _mm512_load_epi32((void*)(magic+16));
register const __m512i B2 = _mm512_load_epi32((void*)(magic+32));
register const __m512i B3 = _mm512_load_epi32((void*)(magic+48));
register const __m512i B4 = _mm512_load_epi32((void*)(magic+64));
register __m512i total0;
register __m512i total1;
register __m512i shuf0;
register __m512i shuf1;
register __m512i result0;
register __m512i result1;
result0 = _mm512_setzero_epi32();
result1 = _mm512_setzero_epi32();
for (i = 0; i < n; i+=16) {
shuf0 = _mm512_load_epi32(&buf[i ]);
shuf1 = _mm512_load_epi32(&buf[i+8]);
_mm_prefetch((const char *)&buf[i+128], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+136], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+16], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+24], _MM_HINT_T0); // vprefetch0
total0 = _mm512_sub_epi32(shuf0, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf0,1)));
total1 = _mm512_sub_epi32(shuf1, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf1,1)));
total0 = _mm512_add_epi32(_mm512_and_epi32(B1, total0), _mm512_and_epi32(B1,_mm512_srli_epi32(total0,2)));
total1 = _mm512_add_epi32(_mm512_and_epi32(B1, total1), _mm512_and_epi32(B1,_mm512_srli_epi32(total1,2)));
total0 = _mm512_and_epi32(B2, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,4)));
total1 = _mm512_and_epi32(B2, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,4)));
total0 = _mm512_and_epi32(B3, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,8)));
total1 = _mm512_and_epi32(B3, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,8)));
total0 = _mm512_and_epi32(B4, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,16)));
total1 = _mm512_and_epi32(B4, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,16)));
result0 = _mm512_add_epi32(result0,total0);
result1 = _mm512_add_epi32(result1,total1);
}
result0 = _mm512_add_epi32(result0,result1);
result = _mm512_reduce_add_epi32(result0);
return result;
}
vpu_popcount5:
inline uint64_t vpu_popcount5(uint64_t* buf, size_t n) {
_mm_prefetch((const char *)&buf[0], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[8], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[16], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[24], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[32], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[40], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[48], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[56], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[64], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[72], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[80], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[88], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[96], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[104], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[112], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[120], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[128], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[136], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[144], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[152], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[160], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[168], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[176], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[184], _MM_HINT_T1); // vprefetch1
register size_t result;
size_t i;
register const __m512i B0 = _mm512_load_epi32((void*)(magic+0));
register const __m512i B1 = _mm512_load_epi32((void*)(magic+16));
register const __m512i B2 = _mm512_load_epi32((void*)(magic+32));
register const __m512i B3 = _mm512_load_epi32((void*)(magic+48));
register const __m512i B4 = _mm512_load_epi32((void*)(magic+64));
register const __m512i B6 = _mm512_load_epi32((void*)(magic+80));
register __m512i total0;
register __m512i total1;
register __m512i total2;
register __m512i total3;
register __m512i shuf0;
register __m512i shuf1;
register __m512i shuf2;
register __m512i shuf3;
register __m512i result0;
register __m512i result1;
result0 = _mm512_setzero_epi32();
result1 = _mm512_setzero_epi32();
for (i = 0; i < n; i+=32) {
shuf0 = _mm512_load_epi32(&buf[i ]);
shuf1 = _mm512_load_epi32(&buf[i+ 8]);
shuf2 = _mm512_load_epi32(&buf[i+16]);
shuf3 = _mm512_load_epi32(&buf[i+24]);
_mm_prefetch((const char *)&buf[i+192], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+200], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+208], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+216], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+32], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+40], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+48], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+56], _MM_HINT_T0); // vprefetch0
total0 = _mm512_sub_epi32(shuf0, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf0,1))); // max value in nn is 10
total1 = _mm512_sub_epi32(shuf1, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf1,1)));
total2 = _mm512_sub_epi32(shuf2, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf2,1)));
total3 = _mm512_sub_epi32(shuf3, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf3,1)));
total0 = _mm512_add_epi32(_mm512_and_epi32(B1, total0), _mm512_and_epi32(B1,_mm512_srli_epi32(total0,2))); // max value in nnnn is 0100
total1 = _mm512_add_epi32(_mm512_and_epi32(B1, total1), _mm512_and_epi32(B1,_mm512_srli_epi32(total1,2)));
total2 = _mm512_add_epi32(_mm512_and_epi32(B1, total2), _mm512_and_epi32(B1,_mm512_srli_epi32(total2,2)));
total3 = _mm512_add_epi32(_mm512_and_epi32(B1, total3), _mm512_and_epi32(B1,_mm512_srli_epi32(total3,2)));
total0 = _mm512_and_epi32(B2, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,4))); // max value in 0000nnnn is 00001000
total1 = _mm512_and_epi32(B2, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,4)));
total2 = _mm512_and_epi32(B2, _mm512_add_epi32(total2, _mm512_srli_epi32(total2,4)));
total3 = _mm512_and_epi32(B2, _mm512_add_epi32(total3, _mm512_srli_epi32(total3,4)));
total0 = _mm512_add_epi32(total0, total1); // max value in 000nnnnn is 00010000
total1 = _mm512_add_epi32(total2, total3);
total0 = _mm512_add_epi32(total0, _mm512_srli_epi32(total0,8)); // max value in xxxxxxxx00nnnnnn is 00100000
total1 = _mm512_add_epi32(total1, _mm512_srli_epi32(total1,8));
total0 = _mm512_and_epi32(B6, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,16))); // max value in each element is 01000000, i.e. 64
total1 = _mm512_and_epi32(B6, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,16)));
result0 = _mm512_add_epi32(result0,total0);
result1 = _mm512_add_epi32(result1,total1);
}
result0 = _mm512_add_epi32(result0,result1);
result = _mm512_reduce_add_epi32(result0);
return result;
}
2 ответа
Со вчерашнего дня я смог запустить ваш код и мое предложение на своей собственной карточке. Я не получаю точно такие же тайминги, как вы, вероятно, из-за отказа моего оборудования и, возможно, из-за версий моего компилятора. Но эта тенденция сохраняется, и мое предложение, похоже, достигло 15-процентного прироста производительности.
Я получил дополнительное небольшое повышение производительности, между пятью и десятью процентами, с небольшой дополнительной настройкой, как показано в коде ниже. Обратите внимание, что в следующем фрагменте кода каждый элемент B6 имеет значение 0x000000FF. На данный момент, я думаю, что алгоритм может быть достаточно близок к максимальной устойчивой пропускной способности, передаваемой из GDDR в кэш L2.
(ДОБАВЛЕННОЕ ПРИМЕЧАНИЕ. Свидетельством этого утверждения является то, что если я оберну тело функции popcount5 циклом for, который повторяет его десять раз, и заметим, что это десять быстрых повторений "chunk_size" входных данных, то есть девять из тех времен, когда в L2 будет очень жарко - общее время теста увеличивается только в пять раз, а не в 10. Я поднимаю это, потому что думаю, что ваша цель - настроить скорость логики подсчета битов, но, возможно, приложение, в котором вы надеетесь развернуть его, на самом деле имеет меньший и / или более горячий рабочий набор. Если это так, регулирование, введенное полосой пропускания DRAM->L2, затуманивает картину. Но обратите внимание, что увеличение размера обратно ваш тестовый ввод, так что он имеет тенденцию оставаться горячим в L2, по-видимому, приводит к тому, что другие издержки - вероятно, издержки openmp - становятся относительно более значительными.)
inline uint64_t vpu_popcount5(uint64_t* buf, size_t n) {
_mm_prefetch((const char *)&buf[0], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[8], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[16], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[24], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[32], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[40], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[48], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[56], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[64], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[72], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[80], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[88], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[96], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[104], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[112], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[120], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[128], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[136], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[144], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[152], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[160], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[168], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[176], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[184], _MM_HINT_T1); // vprefetch1
register size_t result;
size_t i;
register const __m512i B0 = _mm512_load_epi32((void*)(magic+0));
register const __m512i B1 = _mm512_load_epi32((void*)(magic+16));
register const __m512i B2 = _mm512_load_epi32((void*)(magic+32));
register const __m512i B6 = _mm512_load_epi32((void*)(magic+80));
register __m512i total0;
register __m512i total1;
register __m512i total2;
register __m512i total3;
register __m512i shuf0;
register __m512i shuf1;
register __m512i shuf2;
register __m512i shuf3;
register __m512i result0;
register __m512i result1;
result0 = _mm512_setzero_epi32();
result1 = _mm512_setzero_epi32();
for (i = 0; i < n; i+=32) {
shuf0 = _mm512_load_epi32(&buf[i ]);
shuf1 = _mm512_load_epi32(&buf[i+ 8]);
shuf2 = _mm512_load_epi32(&buf[i+16]);
shuf3 = _mm512_load_epi32(&buf[i+24]);
_mm_prefetch((const char *)&buf[i+192], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+200], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+208], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+216], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+32], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+40], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+48], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+56], _MM_HINT_T0); // vprefetch0
total0 = _mm512_sub_epi32(shuf0, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf0,1))); // max value in nn is 10
total1 = _mm512_sub_epi32(shuf1, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf1,1)));
total2 = _mm512_sub_epi32(shuf2, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf2,1)));
total3 = _mm512_sub_epi32(shuf3, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf3,1)));
total0 = _mm512_add_epi32(_mm512_and_epi32(B1, total0), _mm512_and_epi32(B1,_mm512_srli_epi32(total0,2))); // max value in nnnn is 0100
total1 = _mm512_add_epi32(_mm512_and_epi32(B1, total1), _mm512_and_epi32(B1,_mm512_srli_epi32(total1,2)));
total2 = _mm512_add_epi32(_mm512_and_epi32(B1, total2), _mm512_and_epi32(B1,_mm512_srli_epi32(total2,2)));
total3 = _mm512_add_epi32(_mm512_and_epi32(B1, total3), _mm512_and_epi32(B1,_mm512_srli_epi32(total3,2)));
total0 = _mm512_and_epi32(B2, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,4))); // max value in 0000nnnn is 00001000
total1 = _mm512_and_epi32(B2, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,4)));
total2 = _mm512_and_epi32(B2, _mm512_add_epi32(total2, _mm512_srli_epi32(total2,4)));
total3 = _mm512_and_epi32(B2, _mm512_add_epi32(total3, _mm512_srli_epi32(total3,4)));
total0 = _mm512_add_epi32(total0, total1); // max value in 000nnnnn is 00010000
total1 = _mm512_add_epi32(total2, total3);
total0 = _mm512_add_epi32(total0, _mm512_srli_epi32(total0,8)); // max value in xxxxxxxx00nnnnnn is 00100000
total1 = _mm512_add_epi32(total1, _mm512_srli_epi32(total1,8));
total0 = _mm512_and_epi32(B6, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,16))); // max value in each element is 01000000, i.e. 64
total1 = _mm512_and_epi32(B6, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,16)));
result0 = _mm512_add_epi32(result0,total0);
result1 = _mm512_add_epi32(result1,total1);
/* Reduce add, which is analogous to SSSE3's PSADBW instruction,
is not implementated as a single instruction in VPUv1, thus
emulated by multiple instructions*/
}
result0 = _mm512_add_epi32(result0,result1);
result = _mm512_reduce_add_epi32(result0);
return result;
}
Не могли бы вы попробовать следующий вариант и сообщить, улучшает ли это производительность для вас? Я обращаюсь к нескольким моментам, которые, на мой взгляд, не совсем оптимальны в вашем кодировании:
- Я не думаю, что ваши предвыборные расстояния были совершенно правильными. Мне показалось, что вы, возможно, думали о смещении байтов, когда индексирование действительно было в терминах uint64.
- Я не вижу причин делать операцию сокращения на каждой итерации цикла. Вы можете выполнить частичное накопление количества битов в 16 элементах SIMD, а затем выполнить одно сокращение вне цикла.
- Я не думаю, что выполнение инструкций по подсчету на скалярной стороне так же выгодно, как и получение наилучших результатов от планирования VPU. Фокусировка на отличном графике VPU имеет первостепенное значение. Я также не думаю, что скалярная команда popcount фактически соединяется с векторной операцией; т.е. я думаю, что поддерживается только в U-трубе.
inline uint64_t vpu_popcount3_revised(uint64_t* buf, size_t n) {
_mm_prefetch((const char *)&buf[0], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[8], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[16], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[24], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[32], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[40], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[48], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[56], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[64], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[72], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[80], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[88], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[96], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[104], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[112], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[120], _MM_HINT_T1); // vprefetch1
register size_t result;
size_t i;
register const __m512i B0 = _mm512_load_epi32((void*)(magic+0));
register const __m512i B1 = _mm512_load_epi32((void*)(magic+16));
register const __m512i B2 = _mm512_load_epi32((void*)(magic+32));
register const __m512i B3 = _mm512_load_epi32((void*)(magic+48));
register const __m512i B4 = _mm512_load_epi32((void*)(magic+64));
register __m512i total0;
register __m512i total1;
register __m512i shuf0;
register __m512i shuf1;
register __m512i result0;
register __m512i result1;
result0 = _mm512_setzero_epi32();
result1 = _mm512_setzero_epi32();
for (i = 0; i < n; i+=16) {
shuf0 = _mm512_load_epi32(&buf[i ]);
shuf1 = _mm512_load_epi32(&buf[i+8]);
_mm_prefetch((const char *)&buf[i+128], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+136], _MM_HINT_T1); // vprefetch1
_mm_prefetch((const char *)&buf[i+16], _MM_HINT_T0); // vprefetch0
_mm_prefetch((const char *)&buf[i+24], _MM_HINT_T0); // vprefetch0
total0 = _mm512_sub_epi32(shuf0, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf0,1)));
total1 = _mm512_sub_epi32(shuf1, _mm512_and_epi32(B0, _mm512_srli_epi32(shuf1,1)));
total0 = _mm512_add_epi32(_mm512_and_epi32(B1, total0), _mm512_and_epi32(B1,_mm512_srli_epi32(total0,2)));
total1 = _mm512_add_epi32(_mm512_and_epi32(B1, total1), _mm512_and_epi32(B1,_mm512_srli_epi32(total1,2)));
total0 = _mm512_and_epi32(B2, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,4)));
total1 = _mm512_and_epi32(B2, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,4)));
total0 = _mm512_and_epi32(B3, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,8)));
total1 = _mm512_and_epi32(B3, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,8)));
total0 = _mm512_and_epi32(B4, _mm512_add_epi32(total0, _mm512_srli_epi32(total0,16)));
total1 = _mm512_and_epi32(B4, _mm512_add_epi32(total1, _mm512_srli_epi32(total1,16)));
result0 = _mm512_add_epi32(result0,total0);
result1 = _mm512_add_epi32(result1,total1);
}
/* Reduce add, which is analogous to SSSE3's PSADBW instruction,
is not implementated as a single instruction in VPUv1, thus
emulated by multiple instructions*/
result0 = _mm512_add_epi32(result0,result1);
result = _mm512_reduce_add_epi32(result0);
return result;
}