2012-04-16 5 views
6

Я изучаю возможности SIMD, переписывая свою личную библиотеку обработки изображений с использованием векторных встроенных функций. Одной из основных функций является простой «массив +=», т.е.SIMD array add для произвольной длины массива

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]; } 

Но это возможно do все дополнение с инструкциями SIMD? Я думал про это:

__m128i mask = (0x100<<8*(n - n16))-1; 
_mm_maskmoveu_si128(xmm1, mask, (__m128i*) (B + i)); 

для дополнительных элементов, но приведет ли это к неопределенному поведению? mask должен гарантировать, что на самом деле доступ не проходит через границы массива (я думаю). Альтернативой является сначала выполнить дополнительные элементы, но затем массив должен быть выровнен по n-n16, что кажется неправильным.

Есть ли другой, более оптимальный узор, такой векторизованный цикл?

+0

вы могли убедиться, что в вашем коде длина массива всегда кратна 16 байт (хотя, возможно, меньше элементов фактически используются), так что это эпилог никогда не приходит. Но эпилог действительно не важен с точки зрения скорости. – Walter

ответ

4

Один из вариантов заключается в том, чтобы заполнить ваш массив кратным 16 байтам. Затем вы можете выполнить 128 бит load/add/store и просто проигнорировать результаты, следующие за интересующим вас вопросом.

Для больших массивов, хотя накладные расходы байта байтом «epilog» будут очень маленькими. Развернув цикл может улучшить производительность более, что-то вроде:

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); 
+0

На практике я помещал маски в таблицу поиска. Считаете ли вы, что все равно будет медленнее, чем цикл «epilog»? –

+0

@reve_etrange: Вероятно, не медленнее, но это трудно понять без измерения двух решений. Попробуйте. –

+0

Я сделаю снимок. Но это законный доступ к памяти? Поскольку * some * значение 'mask' может вызвать нарушение границ массива. –

Смежные вопросы