如何将 arm64 中的vaddv_u8结果视为氖寄存器

How to treat result of vaddv_u8 in arm64 as a neon register

本文关键字:结果 寄存器 u8 arm64 中的 vaddv      更新时间:2023-10-16

vaddv_u8和 AArch64 (arm64) 的其他一些类似的新 v 内联函数返回uint8_t。如何将此内在的结果视为 neon 寄存器而不是普通 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

如您所见,需要将uint8_t vaddv_u8结果转换为可用作vclz_u8参数的类型,需要无用的dup。 我只从随后的vclz_u8结果中选取第一条车道,因此实际上将其复制到所有车道将是浪费工作。

如何在内部函数中编写它以在 neon 类型变量中获得该sum,而不会使编译器发出无用的操作码? (最好在源代码中没有这种额外的噪音。如果不是,要清楚明了:我不是要求优化或改进我发布的那段代码;我只是写它来显示问题。

你真的应该得到一个带有有序SoC的测试设备。 苹果的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 P10Android平板电脑作为aarch64测试设备是一个很好的候选者:所有八个内核都是一样的,安装了Android 7.12 64位,只需100美元左右。

您的解决方法可能会使性能变差。你的问题写得好像你想要从你的单个向量中获得标量结果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 代码以正确反映这一事实。

似乎我可以在叮当声中做到这一点:

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并且根据您设置构建的方式,它可能是不可接受的。更重要的是,它似乎不适用于更复杂的场景。有没有更好/正确的方法可以做到这一点?