Добавление массива 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);
Другие вопросы по тегам