Как относиться к результату vaddv_u8 в arm64 как неоновый регистр

vaddv_u8 и некоторые другие похожие новые v-intrinsics от AArch64 (arm64) возвращаются uint8_t, Как я могу обработать результат этого встроенного как неоновый регистр вместо простого типа C?

Например:

void paddClz(uint8_t* x)
{
uint8x8_t ret = vdup_n_u8(0);
for (int i = 0; i < 8; ++i, x += 8)
{
uint8x8_t x8 = vld1_u8(x);
uint8_t sum = vaddv_u8(x8);
uint8x8_t r = vdup_n_u8(sum); //or: r = vset_lane_u8(sum, r, 0);
r = vclz_u8(r);
ret = vext_u8(ret, r, 1);
}
vst1_u8(x, ret);
}

что породил лязг:

paddClz(unsigned char*): // @paddClz(unsigned char*)
mov x8, xzr
movi d0, #0000000000000000
.LBB0_1: // =>This Inner Loop Header: Depth=1
ldr d1, [x0, x8]
add x8, x8, #8 // =8
cmp w8, #64 // =64
addv b1, v1.8b
dup v1.8b, v1.b[0]   <<== useless! I only need/use/care about v1.b[0]
clz v1.8b, v1.8b
ext v0.8b, v0.8b, v1.8b, #1
b.ne .LBB0_1
str d0, [x0, #64]
ret

Как видите, там бесполезно dup свойственный требуется, чтобы получить uint8_t vaddv_u8 Результат преобразован в тип, который будет работать в качестве аргумента для vclz_u8, Я беру только первый переулок из следующего vclz_u8 результат, поэтому на самом деле дублирование его на все дорожки будет напрасной работой.

Как я могу написать это по сути, чтобы получить это sum в неон типизированной переменной, не заставляя компилятор выдавать бесполезные коды операций? (И желательно без этого лишнего шума в исходном коде.) Чтобы было ясно и очевидно, если это не так: я не прошу оптимизировать или улучшить тот фрагмент кода, который я разместил; Я просто написал это, чтобы показать проблему.

2

Решение

Вы действительно должны получить тестовое устройство с SoC в порядке. Чипы Apple серии A вышли из строя, безусловно, самые мощные, если быть точными.

Ваша реализация может работать на вашем iPhone достаточно быстро, но едва ли будет быстрее, чем простейшие версии C на ядрах, которые нельзя использовать.

Подумайте дважды, прежде чем спешить писать циклы на NEON.
Вы можете вообще избегать так называемых «горизонтальных» операций, транспонируя матрицу, а затем выполнять «вертикальную» математику.


#define vuzp8(a, b, c) ({ \
c = vuzp_u8(a, b); \
a = c.val[0]; \
b = c.val[1]; \
})

void foo(uint8_t *pDst, uint8_t *pSrc)
{
uint8x8x4_t top, bottom;
uint8x8x2_t temp;

top = vld4_u8(pSrc);
pSrc += 32;
bottom = vld4_u8(pSrc);

vuzp8(top.val[0], bottom.val[0], temp);
vuzp8(top.val[1], bottom.val[1], temp);
vuzp8(top.val[2], bottom.val[2], temp);
vuzp8(top.val[3], bottom.val[3], temp);

top.val[0] += bottom.val[0];
top.val[1] += bottom.val[1];
top.val[2] += bottom.val[2];
top.val[3] += bottom.val[3];

top.val[0] += top.val[1];
top.val[2] += top.val[3];

top.val[0] += top.val[2];

top.val[0] = vclz_u8(top.val[0]);

vst1_u8(pDst, top.val[0]);
}

Еще один пример, где вы спрашиваете себя, если intrinsux имеет смысл вообще. Его неуклюжесть делает код намного более сложным, и он недостаточно выразителен, чтобы сделать три 128-битных плюс одно 64-битное добавление вместо шести 64-битных.

Кроме того, вы должны еще раз проверить, не компилятор ничего не испортил, опять же, особенно когда вы делаете перестановки (vzip, vuzp, vtrn)

Я думаю, что машинный код будет в порядке на aarch32но я не уверен в этом aarch64 где инструкции по перестановке сильно отличаются.

Я думаю, что вы уже поняли, почему я ненавижу intrinsux как вредитель. Это больше неприятностей, чем любая помощь.

PS: Teclast P10 Планшет Android — неплохой кандидат в качестве aarch64 Тестовое устройство: все восемь ядер одинаковы, установлен Android 7.12 64bit и стоит всего около 100 долларов.

2

Другие решения

Ваш обходной путь потенциально ухудшает производительность. Ваша проблема написана так, как будто вы хотите получить скалярный результат от вашего единственного вектора uint8_t. Нет ничего плохого в том, что инструкция vaddv_u8 возвращает скалярное значение. В ARMv8 «устройство NEON» теперь полностью интегрировано и не имеет большого штрафа за перемещение данных между регистрами NEON и ARM. Просто используйте встроенную C для подсчета лидирующих нулей результата, и вы получите то, что вам нужно:

int paddClz(const uint8_t* x)
{
uint8x8_t x8 = vld1_u8(x);
uint8_t sum = vaddv_u8(x8);
return __builtin_clz(sum) - 24;
}

Интрис будет скомпилирован в одну инструкцию ARM (CLZ).

Если вы работаете с большим набором данных, напишите код на C, чтобы должным образом отразить этот факт.

1

Кажется, что Я могу сделать это в Clang:

int paddClz(const uint8_t* x)
{
uint8x8_t x8 = vld1_u8(x);
uint8_t sum = vaddv_u8(x8);
uint8x8_t r;
r = vset_lane_u8(sum, r, 0);
r = vclz_u8(r);
return vget_lane_u8(r, 0);
}

Это производит именно то, что я хочу:

addv b0, v0.8b
clz v0.8b, v0.8b

Тем не мение, GCC производит беспорядок из этого кода. Другая проблема заключается в том, что он использует неинициализированный r и в зависимости от того, как вы настроили свою сборку, она может быть неприемлемой. Более того, это не работает в более сложных сценариях. Есть ли лучший / правильный способ сделать это?

0