在单臂霓虹灯寄存器中有效地将 8 位数字扩展到 12 位
Efficiently extend 8-bit numbers to 12-bits in a single arm neon register
我在霓虹灯寄存器中加载了 4 个字节。我怎样才能有效地将它转换为 12 位,例如我需要在第一个字节后插入 4 个零位,在第二个字节后插入 8 个零位,依此类推。例如,如果我有这 4 个十六进制字节:
01 02 03 04
It would end up with this in hex:
01 20 00 03 40
相同的操作表示为一个简单的 c 函数,该函数对表示 4 个输入字节的 32 位变量进行操作:
uint64_t expand12(uint32_t i)
{
uint64_t r = (i & 0xFF);
r |= ((i & 0x0000ff00) << 4); // shift second byte by 4 bits
r |= ((i & 0x00ff0000) << 8); // shift third byte by 8 bits
r |= (((uint64_t)(i & 0xff000000)) << 12); // 4th by 12
return r;
}
所以,如果我在 uint8x8_t
neon 寄存器中有这些字节,那么在 neon 中实现相同操作的好方法是什么,以便相同的寄存器将以这些移位值结束?
请注意,如果有任何帮助,所有四个字节的前 4 位都为零。
更新:
在我的例子中,我有 4 个 uint16x8_t 寄存器,对于每个寄存器,我需要计算所有通道的总和 (vaddv_u16
),然后对该总和执行 vclz_u16
,然后将这四个总和组合成一个霓虹灯寄存器将它们分开 12 位:
uint64_t compute(uint16x8_t a, uint16x8_t b, uint16x8_t c, uint16x8_t d)
{
u16 a0 = clz(vaddv(a));
u16 b0 = clz(vaddv(b));
u16 c0 = clz(vaddv(c));
u16 d0 = clz(vaddv(d));
return (a0 << 36) | (b0 << 24) | (c0 << 12) | (d0);
}
注意,这是伪代码,我需要在 neon 寄存器中得到结果。
以防万一,在我的代码中,我有一个函数可以在 4 个 uint16x8_t 寄存器中查找最大元素的索引。在该函数中,这四个寄存器被 vand
ed 与最大元素复制到所有通道,然后结果被 vorr
ed 与位掩码 {1<<15, 1<<14, ... 1<<0}
;然后,我将所有通道成对相加,其中的 clz 给出了每个寄存器的最大元素的索引。所有这些我都需要在元素之间插入额外的 4 个零位并存储到霓虹灯寄存器中。 C 中的示例:
void compute(uint16_t *src, uint64_t* dst)
{
uint64_t x[4];
for (int i = 0; i < 4; ++i, src+=16)
{
int max = 0;
for (int j = 0; j < 16; ++j)
{
if (src[j] > src[max])
max = j;
}
x[i] = max;
}
*dst = (x[0] << 36) | (x[1] << 24) | (x[2] << 12) | (x[3]);
}
此函数是大型函数的一部分,该函数在一个循环中执行此计算数百万次,并且使用此函数的结果并且必须在霓虹灯寄存器中。将其视为描述算法的伪代码,如果不清楚这意味着什么:这意味着只有算法很重要,没有需要优化的加载或存储
你必须跳出框框思考。不要拘泥于数据类型和位宽。
uint32_t
只不过是一个包含 4 个 uint8_t
的数组,您可以在加载时轻松地通过 vld4
即时传播。
问题因此变得更易于管理。
void foo(uint32_t *pDst, uint32_t *pSrc, uint32_t length)
{
length >>= 4;
int i;
uint8x16x4_t in, out;
uint8x16_t temp0, temp1, temp2;
for (i = 0; i < length; ++i)
{
in = vld4q_u8(pSrc);
pSrc += 16;
temp0 = in.val[1] << 4;
temp1 = in.val[3] << 4;
temp1 += in.val[1] >> 4;
out.val[0] = in.val[0] | temp0;
out.val[1] = in.val[2] | temp1;
out.val[2] = in.val[3] >> 4;
out.val[3] = vdupq_n_u8(0);
vst4q_u8(pDst, out);
pDst += 16;
}
}
请注意,我省略了剩余交易,如果展开得更深,它会 运行 快得多。
更重要的是,我会毫不犹豫地用汇编语言编写这个函数,因为我认为编译器不会如此巧妙地管理寄存器,以至于 out.val[3]
仅在循环外一次被零初始化.
而且我也怀疑 temp1 += in.val[1] >> 4;
会转换为 vsra
,因为指令的非单独目标操作数的性质。谁知道?
编译器糟透了。
更新:好的,这里有满足您需求的代码,用汇编语言编写,适用于两种架构。
aarch32
vtrn.16 q0, q1
vtrn.16 q2, q3
vtrn.32 q0, q2
vtrn.32 q1, q3
vadd.u16 q0, q1, q0
vadd.u16 q2, q3, q2
adr r12, shift_table
vadd.u16 q0, q2, q0
vld1.64 {q3}, [r12]
vadd.u16 d0, d1, d0
vclz.u16 d0, d0 // d0 contains the leading zeros
vmovl.u16 q0, d0
vshl.u32 q1, q0, q3
vpadal.u32 d3, d2 // d3 contains the final result
.balign 8
shift_table:
.dc.b 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4
aarch64
trn1 v16.8h, v0.8h, v1.8h
trn1 v18.8h, v2.8h, v3.8h
trn2 v17.8h, v0.8h, v1.8h
trn2 v19.8h, v2.8h, v3.8h
trn2 v0.4s, v18.4s, v16.4s
trn1 v1.4s, v18.4s, v16.4s
trn2 v2.4s, v19.4s, v17.4s
trn1 v3.4s, v19.4s, v17.4s
add v0.8h, v1.8h, v0.8h
add v2.8h, v3.8h, v2.8h
adr x16, shift_table
add v0.8h, v2.8h, v0.8h
ld1 {v3.2d}, [x16]
mov v1.d[0], v0.d[1]
add v0.4h, v1.4h, v0.4h
clz v0.4h, v0.4h // v0 contains the leading zeros
uxtl v0.4s, v0.4h
ushl v0.4s, v0.4s, v3.4s
mov v1.d[0], v0.d[1]
uadalp v1.1d, v0.2s // v1 contains the final result
.balign 8
shift_table:
.dc.b 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4
** 您可能需要在 Clang
中将 .dc.b
更改为 .byte
我在霓虹灯寄存器中加载了 4 个字节。我怎样才能有效地将它转换为 12 位,例如我需要在第一个字节后插入 4 个零位,在第二个字节后插入 8 个零位,依此类推。例如,如果我有这 4 个十六进制字节:
01 02 03 04
It would end up with this in hex:
01 20 00 03 40
相同的操作表示为一个简单的 c 函数,该函数对表示 4 个输入字节的 32 位变量进行操作:
uint64_t expand12(uint32_t i)
{
uint64_t r = (i & 0xFF);
r |= ((i & 0x0000ff00) << 4); // shift second byte by 4 bits
r |= ((i & 0x00ff0000) << 8); // shift third byte by 8 bits
r |= (((uint64_t)(i & 0xff000000)) << 12); // 4th by 12
return r;
}
所以,如果我在 uint8x8_t
neon 寄存器中有这些字节,那么在 neon 中实现相同操作的好方法是什么,以便相同的寄存器将以这些移位值结束?
请注意,如果有任何帮助,所有四个字节的前 4 位都为零。
更新:
在我的例子中,我有 4 个 uint16x8_t 寄存器,对于每个寄存器,我需要计算所有通道的总和 (vaddv_u16
),然后对该总和执行 vclz_u16
,然后将这四个总和组合成一个霓虹灯寄存器将它们分开 12 位:
uint64_t compute(uint16x8_t a, uint16x8_t b, uint16x8_t c, uint16x8_t d)
{
u16 a0 = clz(vaddv(a));
u16 b0 = clz(vaddv(b));
u16 c0 = clz(vaddv(c));
u16 d0 = clz(vaddv(d));
return (a0 << 36) | (b0 << 24) | (c0 << 12) | (d0);
}
注意,这是伪代码,我需要在 neon 寄存器中得到结果。
以防万一,在我的代码中,我有一个函数可以在 4 个 uint16x8_t 寄存器中查找最大元素的索引。在该函数中,这四个寄存器被 vand
ed 与最大元素复制到所有通道,然后结果被 vorr
ed 与位掩码 {1<<15, 1<<14, ... 1<<0}
;然后,我将所有通道成对相加,其中的 clz 给出了每个寄存器的最大元素的索引。所有这些我都需要在元素之间插入额外的 4 个零位并存储到霓虹灯寄存器中。 C 中的示例:
void compute(uint16_t *src, uint64_t* dst)
{
uint64_t x[4];
for (int i = 0; i < 4; ++i, src+=16)
{
int max = 0;
for (int j = 0; j < 16; ++j)
{
if (src[j] > src[max])
max = j;
}
x[i] = max;
}
*dst = (x[0] << 36) | (x[1] << 24) | (x[2] << 12) | (x[3]);
}
此函数是大型函数的一部分,该函数在一个循环中执行此计算数百万次,并且使用此函数的结果并且必须在霓虹灯寄存器中。将其视为描述算法的伪代码,如果不清楚这意味着什么:这意味着只有算法很重要,没有需要优化的加载或存储
你必须跳出框框思考。不要拘泥于数据类型和位宽。
uint32_t
只不过是一个包含 4 个 uint8_t
的数组,您可以在加载时轻松地通过 vld4
即时传播。
问题因此变得更易于管理。
void foo(uint32_t *pDst, uint32_t *pSrc, uint32_t length)
{
length >>= 4;
int i;
uint8x16x4_t in, out;
uint8x16_t temp0, temp1, temp2;
for (i = 0; i < length; ++i)
{
in = vld4q_u8(pSrc);
pSrc += 16;
temp0 = in.val[1] << 4;
temp1 = in.val[3] << 4;
temp1 += in.val[1] >> 4;
out.val[0] = in.val[0] | temp0;
out.val[1] = in.val[2] | temp1;
out.val[2] = in.val[3] >> 4;
out.val[3] = vdupq_n_u8(0);
vst4q_u8(pDst, out);
pDst += 16;
}
}
请注意,我省略了剩余交易,如果展开得更深,它会 运行 快得多。
更重要的是,我会毫不犹豫地用汇编语言编写这个函数,因为我认为编译器不会如此巧妙地管理寄存器,以至于 out.val[3]
仅在循环外一次被零初始化.
而且我也怀疑 temp1 += in.val[1] >> 4;
会转换为 vsra
,因为指令的非单独目标操作数的性质。谁知道?
编译器糟透了。
更新:好的,这里有满足您需求的代码,用汇编语言编写,适用于两种架构。
aarch32
vtrn.16 q0, q1
vtrn.16 q2, q3
vtrn.32 q0, q2
vtrn.32 q1, q3
vadd.u16 q0, q1, q0
vadd.u16 q2, q3, q2
adr r12, shift_table
vadd.u16 q0, q2, q0
vld1.64 {q3}, [r12]
vadd.u16 d0, d1, d0
vclz.u16 d0, d0 // d0 contains the leading zeros
vmovl.u16 q0, d0
vshl.u32 q1, q0, q3
vpadal.u32 d3, d2 // d3 contains the final result
.balign 8
shift_table:
.dc.b 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4
aarch64
trn1 v16.8h, v0.8h, v1.8h
trn1 v18.8h, v2.8h, v3.8h
trn2 v17.8h, v0.8h, v1.8h
trn2 v19.8h, v2.8h, v3.8h
trn2 v0.4s, v18.4s, v16.4s
trn1 v1.4s, v18.4s, v16.4s
trn2 v2.4s, v19.4s, v17.4s
trn1 v3.4s, v19.4s, v17.4s
add v0.8h, v1.8h, v0.8h
add v2.8h, v3.8h, v2.8h
adr x16, shift_table
add v0.8h, v2.8h, v0.8h
ld1 {v3.2d}, [x16]
mov v1.d[0], v0.d[1]
add v0.4h, v1.4h, v0.4h
clz v0.4h, v0.4h // v0 contains the leading zeros
uxtl v0.4s, v0.4h
ushl v0.4s, v0.4s, v3.4s
mov v1.d[0], v0.d[1]
uadalp v1.1d, v0.2s // v1 contains the final result
.balign 8
shift_table:
.dc.b 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00 // 0, 12, 24, 4
** 您可能需要在 Clang
中将.dc.b
更改为 .byte