在 arm neon 中有效地累加符号位
Efficiently accumulate sign bits in arm neon
我有一个循环进行一些计算,然后将符号位存储到向量中:
uint16x8_t rotate(const uint16_t* x);
void compute(const uint16_t* src, uint16_t* dst)
{
uint16x8_t sign0 = vmovq_n_u16(0);
uint16x8_t sign1 = vmovq_n_u16(0);
for (int i=0; i<16; ++i)
{
uint16x8_t r0 = rotate(src++);
uint16x8_t r1 = rotate(src++);
// pseudo code:
sign0 |= (r0 >> 15) << i;
sign1 |= (r1 >> 15) << i;
}
vst1q_u16(dst+1, sign0);
vst1q_u16(dst+8, sign1);
}
在该伪代码后面的 neon 中累加符号位的最佳方法是什么?
r0 = vshrq_n_u16(r0, 15);
r1 = vshrq_n_u16(r1, 15);
sign0 = vsraq_n_u16(vshlq_n_u16(r0, 15), sign0, 1);
sign1 = vsraq_n_u16(vshlq_n_u16(r1, 15), sign1, 1);
另外,请注意 "pseudo code" 实际上可以正常工作并生成几乎相同的代码。这里有什么可以改进的?请注意,在实际代码中,循环中没有函数调用,我精简了实际代码以使其易于理解。
另一点:在 neon 中你不能使用变量来进行向量移位(例如 i
不能使用 used 来指定移位数)。
ARM 可以在一条 vsri
指令中完成此操作(感谢@Jake'Alquimista'LEE)。
给定一个新向量,您希望从中获取符号位,将每个元素的低 15 位替换为右移 1 位的累加器。
你应该按 2 展开,这样编译器就不需要 mov
指令将结果复制回同一个寄存器,因为 vsri
是一个 2 操作数指令,并且方式我们需要在这里使用它,使我们在与旧 sign0
累加器不同的寄存器中得到结果。
sign0 = vsriq_n_u16(r0, sign0, 1);
// insert already-accumulated bits below the new bit we want
在 15 次插入后(或者 16 次,如果你从 sign0 = 0
开始,而不是剥离第一次迭代并使用 sign0=r0),sign0
的所有 16 位(每个元素)将是符号来自 r0
个值的位。
之前的建议:AND 用一个向量常数来隔离符号位。比两班更高效
您使用 VSRA 累加以移位累加器并添加新位的想法很好,因此我们可以保留它并减少总共 2 条指令。
tmp = r0 & 0x8000; // VAND
sign0 = (sign0 >> 1) + tmp; // VSRA
或使用 neon 内在函数:
uint16x8_t mask80 = vmovq_n_u16(0x8000);
r0 = vandq_u16(r0, mask80); // VAND
sign0 = vsraq_n_u16(r0, sign0, 1); // VSRA
随心所欲地使用内在函数或 asm 实现,并以相同的方式编写标量版本,以使编译器有更好的机会进行自动矢量化。
这确实需要寄存器中的向量常量。如果您对寄存器非常紧张,那么 2 个班次可能会更好,但总共 3 个班次似乎可能成为移位器吞吐量的瓶颈,除非 ARM 芯片通常在 SIMD 桶形移位器上花费大量空间。
在那种情况下,也许可以使用这个 没有 ARM shift+accumulate 或 shift+insert 的通用 SIMD 想法
tmp = r0 >> 15; // logical right shift
sign0 += sign0; // add instead of left shifting
sign0 |= tmp; // or add or xor or whatever.
这会以相反的顺序为您提供位。如果你能按相反的顺序制作它们,那就太好了。
否则,ARM 是否具有 SIMD 位反转或仅用于标量? (以相反的顺序生成并在最后翻转它们,对每个矢量位图进行一些额外的工作,希望只有一条指令。)
更新:是的,AArch64 有 rbit
,因此您可以反转一个字节中的位,然后字节洗牌以正确的顺序排列它们。 x86 可以使用 pshufb
LUT 在两个 4 位块中的字节内进行位反转。不过,当您在 x86 上积累位时,这可能不会在做更多工作之前出现。
我有一个循环进行一些计算,然后将符号位存储到向量中:
uint16x8_t rotate(const uint16_t* x);
void compute(const uint16_t* src, uint16_t* dst)
{
uint16x8_t sign0 = vmovq_n_u16(0);
uint16x8_t sign1 = vmovq_n_u16(0);
for (int i=0; i<16; ++i)
{
uint16x8_t r0 = rotate(src++);
uint16x8_t r1 = rotate(src++);
// pseudo code:
sign0 |= (r0 >> 15) << i;
sign1 |= (r1 >> 15) << i;
}
vst1q_u16(dst+1, sign0);
vst1q_u16(dst+8, sign1);
}
在该伪代码后面的 neon 中累加符号位的最佳方法是什么?
r0 = vshrq_n_u16(r0, 15);
r1 = vshrq_n_u16(r1, 15);
sign0 = vsraq_n_u16(vshlq_n_u16(r0, 15), sign0, 1);
sign1 = vsraq_n_u16(vshlq_n_u16(r1, 15), sign1, 1);
另外,请注意 "pseudo code" 实际上可以正常工作并生成几乎相同的代码。这里有什么可以改进的?请注意,在实际代码中,循环中没有函数调用,我精简了实际代码以使其易于理解。
另一点:在 neon 中你不能使用变量来进行向量移位(例如 i
不能使用 used 来指定移位数)。
ARM 可以在一条 vsri
指令中完成此操作(感谢@Jake'Alquimista'LEE)。
给定一个新向量,您希望从中获取符号位,将每个元素的低 15 位替换为右移 1 位的累加器。
你应该按 2 展开,这样编译器就不需要 mov
指令将结果复制回同一个寄存器,因为 vsri
是一个 2 操作数指令,并且方式我们需要在这里使用它,使我们在与旧 sign0
累加器不同的寄存器中得到结果。
sign0 = vsriq_n_u16(r0, sign0, 1);
// insert already-accumulated bits below the new bit we want
在 15 次插入后(或者 16 次,如果你从 sign0 = 0
开始,而不是剥离第一次迭代并使用 sign0=r0),sign0
的所有 16 位(每个元素)将是符号来自 r0
个值的位。
之前的建议:AND 用一个向量常数来隔离符号位。比两班更高效
您使用 VSRA 累加以移位累加器并添加新位的想法很好,因此我们可以保留它并减少总共 2 条指令。
tmp = r0 & 0x8000; // VAND
sign0 = (sign0 >> 1) + tmp; // VSRA
或使用 neon 内在函数:
uint16x8_t mask80 = vmovq_n_u16(0x8000);
r0 = vandq_u16(r0, mask80); // VAND
sign0 = vsraq_n_u16(r0, sign0, 1); // VSRA
随心所欲地使用内在函数或 asm 实现,并以相同的方式编写标量版本,以使编译器有更好的机会进行自动矢量化。
这确实需要寄存器中的向量常量。如果您对寄存器非常紧张,那么 2 个班次可能会更好,但总共 3 个班次似乎可能成为移位器吞吐量的瓶颈,除非 ARM 芯片通常在 SIMD 桶形移位器上花费大量空间。
在那种情况下,也许可以使用这个 没有 ARM shift+accumulate 或 shift+insert 的通用 SIMD 想法
tmp = r0 >> 15; // logical right shift
sign0 += sign0; // add instead of left shifting
sign0 |= tmp; // or add or xor or whatever.
这会以相反的顺序为您提供位。如果你能按相反的顺序制作它们,那就太好了。
否则,ARM 是否具有 SIMD 位反转或仅用于标量? (以相反的顺序生成并在最后翻转它们,对每个矢量位图进行一些额外的工作,希望只有一条指令。)
更新:是的,AArch64 有 rbit
,因此您可以反转一个字节中的位,然后字节洗牌以正确的顺序排列它们。 x86 可以使用 pshufb
LUT 在两个 4 位块中的字节内进行位反转。不过,当您在 x86 上积累位时,这可能不会在做更多工作之前出现。