Добавление массива SIMD для произвольной длины массива
Я учусь использовать возможности SIMD, переписывая свою личную библиотеку обработки изображений с использованием векторных встроенных функций. Одна базовая функция - это простой массив +=
то есть
void arrayAdd(unsigned char* A, unsigned char* B, size_t n) {
for(size_t i=0; i < n; i++) { B[i] += A[i] };
}
Для произвольной длины массива очевидный код SIMD (при условии, что он выровнен по 16) выглядит примерно так:
size_t i = 0;
__m128i xmm0, xmm1;
n16 = n - (n % 16);
for (; i < n16; i+=16) {
xmm0 = _mm_load_si128( (__m128i*) (A + i) );
xmm1 = _mm_load_si128( (__m128i*) (B + i) );
xmm1 = _mm_add_epi8( xmm0, xmm1 );
_mm_store_si128( (__m128i*) (B + i), xmm1 );
}
for (; i < n; i++) { B[i] += A[i]; }
Но возможно ли сделать все дополнения с помощью инструкции SIMD? Я думал попробовать это:
__m128i mask = (0x100<<8*(n - n16))-1;
_mm_maskmoveu_si128( xmm1, mask, (__m128i*) (B + i) );
для дополнительных элементов, но приведет ли это к неопределенному поведению? mask
должен гарантировать, что доступ фактически не сделан за пределами массива (я думаю). Альтернативой является сначала сделать дополнительные элементы, но затем массив должен быть выровнен n-n16
, который не кажется правильным.
Есть ли другой, более оптимальный образец таких векторизованных циклов?
1 ответ
Один из вариантов - заполнить ваш массив кратным 16 байтам. Затем вы можете выполнить 128-битную загрузку / добавление / сохранение и просто проигнорировать результаты, следуя пункту, который вас волнует.
Для больших массивов заголовок байта "эпилог" будет очень маленьким. Развертывание цикла может еще больше повысить производительность, например:
for (; i < n32; i+=32) {
xmm0 = _mm_load_si128( (__m128i*) (A + i) );
xmm1 = _mm_load_si128( (__m128i*) (B + i) );
xmm2 = _mm_load_si128( (__m128i*) (A + i + 16) );
xmm3 = _mm_load_si128( (__m128i*) (B + i + 16) );
xmm1 = _mm_add_epi8( xmm0, xmm1 );
xmm3 = _mm_add_epi8( xmm2, xmm3 );
_mm_store_si128( (__m128i*) (B + i), xmm1 );
_mm_store_si128( (__m128i*) (B + i + 16), xmm3 );
}
// Do another 128 bit load/add/store here if required
Но трудно сказать, не выполняя какое-либо профилирование.
Вы также можете выполнить невыровненную загрузку / сохранение в конце (при условии, что у вас более 16 байтов), хотя это, вероятно, не будет иметь большого значения. Например, если у вас есть 20 байтов, вы делаете одну загрузку / сохранение со смещением 0 и другую невыровненную загрузку / добавление / хранение (_mm_storeu_si128
, __mm_loadu_si128
) к смещению 4.
Вы могли бы использовать _mm_maskmoveu_si128
но вам нужно поместить маску в регистр xmm, и ваш пример кода не будет работать. Вы, вероятно, хотите установить регистр маски для всех FF, а затем использовать сдвиг, чтобы выровнять его. В конце концов, это, вероятно, будет медленнее, чем загрузка / добавление / хранение без выравнивания.
Это было бы что-то вроде:
mask = _mm_cmpeq_epi8(mask, mask); // Set to all FF's
mask = _mm_srli_si128(mask, 16-(n%16)); // Align mask
_mm_maskmoveu_si128(xmm, mask, A + i);