Векторное сложение с расширением

kiracher

Помогите с проблемой - есть несколько массивов, надо их сложить поэлементно и записать в другой массив. Подточив примеры, легко разобрался с однобайтовыми и двухбайтовыми массивами, в случае когда тип исходных и результирующего массива одинаковый.
Но мне надо другое - сложить байтовые массивы в массив двухбайтовых результатов. Работающий вариант (маскирование и сложение младших и старших бит по отдельности есть но это криво. Архитектура - arm neon, сложение с расширением типа поддерживается. Векторизация требуется, иначе тупо скорости проц и шины памяти не хватит.

void sum16(unsigned int size, void *dst1, const void *src1, const void *src2, const void *src3, const void *src4)
{
typedef unsigned char v8byte __attribute__ vector_size(8;
typedef unsigned short v8int __attribute__ vector_size(16;

v8int *d1 = dst1;

const v8byte *s1 = src1;
const v8byte *s2 = src2;
const v8byte *s3 = src3;
const v8byte *s4 = src4;

unsigned int i;

i = size / 8;

while (i--) {

*d1++ = *s1++ + *s2++ + *s3++ + *s4++;
};

return;
}



По моему разумению, код считывает по 8 байтов из каждого массива и складывает в 8 же 16-битных ячеек в другой. Пробую разные cast и typedef, но получаю всякий раз разные ошибки. :confused:
Подскажите правильную комбинацию :o
PS Cache prefetch и небольшой unroll подбираю вручную, в примере не приводится чтобы не загромождать

salamander

По-моему, это тот случай, когда лучше одну функцию написать на ассемблере. У тебя архитектура фиксирована и ты точно знаешь, какой ассемблер ты хочешь получить от компилятора. И ты сейчас занимаешься тем, что подгоняешь C-шный код так, чтобы получить такой ассемблер.
Ну или используй интринсики. Примерно так. Ассемблер вроде похожий на правду получается, запускать не запускал.
#include <arm_neon.h>

void sum16(int size, void *dst, void *src1, void *src2,
void *src3, void *src4)
{
int i = size / 8;
uint16x8_t *d = (uint16x8_t *)dst;
uint8x8_t *s1 = (uint8x8_t *) src1;
uint8x8_t *s2 = (uint8x8_t *) src2;
uint8x8_t *s3 = (uint8x8_t *) src3;
uint8x8_t *s4 = (uint8x8_t *) src4;
while (i--) {
*d++ = vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++;
}
}

arm-none-linux-gnueabi-gcc -mfloat-abi=softfp -mfpu=neon -O3 -fno-unroll-loops vaddl.c -S -o vaddl.s

Получилось:
 sum16:
.fnstart
.LFB1870:
@ args = 8, pretend = 0, frame = 0
@ frame_needed = 0, uses_anonymous_args = 0
@ link register save eliminated.
add ip, r0, cmp r0, movlt r0, ip
stmfd sp!, {r4, r5, r6, r7, r8, sl}
.save {r4, r5, r6, r7, r8, sl}
.LCFI0:
movs r0, r0, asr ldr r7, [sp, #24]
ldr r8, [sp, #28]
beq .L4
mov ip, L3:
add sl, r2, ip
add r6, r3, ip
add r5, r7, ip
add r4, r8, ip
fldd d18, [sl, #0]
fldd d19, [r6, #0]
fldd d17, [r5, #0]
fldd d16, [r4, #0]
subs r0, r0, vaddl.u8 q9, d18, d19
add ip, ip, vaddl.u8 q8, d17, d16
vadd.i16 q8, q9, q8
vstmia r1!, {d16-d17}
bne .L3
.L4:
ldmfd sp!, {r4, r5, r6, r7, r8, sl}
bx lr

warningmax

Примеров пачка вот тут: http://gcc.gnu.org/projects/tree-ssa/vectorization.html
Возможно сложение с расширением не поддерживается в GCC, тогда интринсики зарешают.
Также не рекомендую пытаться как-то помогать с unroll и т.д. он сам сделает как ему надо, и в большинстве случаев это будет быстрее.
Чем проще написано на си, тем лучше в итоге всё будет векторизовано.
Всякие атрибуты типа vector_size тоже ни к чему.
Unroll вообще бывает сильно мешает - если в ffmpeg заменить пару развёрнутых вручную циклов, то он сразу на 5% шустрее становится, причём в случае с -O3 для этого ещё надо писать и -fno-unroll-loops.
Какие ошибки пишутся при -fdump-tree-vect-details?

kiracher

Спасибо, то что надо. Подточил, протестил (работает получилось вот такое
 void sum16(unsigned int size, void *dst, const void *src1, const void *src2, const void *src3, const void *src4, const void *src5, const void *src6, const void *src7, const void *src8)
{

unsigned int i = size / 8; // vectorization 8 byte

i = i / 8 ; // unroll 8x

uint16x8_t *d = (uint16x8_t *) dst;
uint8x8_t *s1 = (uint8x8_t *) src1;
uint8x8_t *s2 = (uint8x8_t *) src2;
uint8x8_t *s3 = (uint8x8_t *) src3;
uint8x8_t *s4 = (uint8x8_t *) src4;
uint8x8_t *s5 = (uint8x8_t *) src5;
uint8x8_t *s6 = (uint8x8_t *) src6;
uint8x8_t *s7 = (uint8x8_t *) src7;
uint8x8_t *s8 = (uint8x8_t *) src8;
while (i--) {
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;
*d++ = vaddq_u16( vaddq_u16(vaddl_u8(*s1++, *s2++ vaddl_u8(*s3++, *s4++
vaddq_u16(vaddl_u8(*s5++, *s6++ vaddl_u8(*s7++, *s8++;

#define PREFETCH 16
__builtin_prefetch(s1+PREFETCH,0);
__builtin_prefetch(s2+PREFETCH,0);
__builtin_prefetch(s3+PREFETCH,0);
__builtin_prefetch(s4+PREFETCH,0);
__builtin_prefetch(s5+PREFETCH,0);
__builtin_prefetch(s6+PREFETCH,0);
__builtin_prefetch(s7+PREFETCH,0);
__builtin_prefetch(s8+PREFETCH,0);

}
}

К вопросу ниже про unroll - его имеет смысл делать вручную, как минимум в моем случае, потому что за раз обрабатывается 8 байт, а линия кеша - 64 байт. Поэтому для максимальной скорости загрузки данных из памяти в кэш второго уровня на 8 сложений надо делать одну предзагрузку кеша. Причем насколько далеко стоит делать предзагрузку тоже надо подбирать.
Небольшой бенчмарк - 10 прогонов по 100 раз на массивах по 5 мбайт, время отработки в мс.
original - 171.3
cache-prefetch - 125.7
unroll 4x - 148.1
unroll 8x - 148.8
unroll 4x + cache prefetch - 81.5
unroll 8x + cache prefetch - 76.1
Оптимизировать дальше имеет мало смысла - это уже упирается в шину памяти. (8+2)*5мбайт / 0.076 с = 650 мб/с. Некий спец писал в инете что на этой железке (beagleboard) максимально оптимизированный memcpy у него давал где-то 300-400 мб/с, memset порядка 700-800 мб/с.

xronik111

-fprefetch-loop-arrays не пробовал? В теории должен помочь делать "Причем насколько далеко стоит делать предзагрузку тоже надо подбирать", но этим пассом вроде как давно не занимались, плюс размер кэша там в параметрах может быть не тот, тогда надо руками ставить.
Оставить комментарий
Имя или ник:
Комментарий: