我尝试使用跨平台的SIMD库ala ecmascript_simd aka SIMD.js,其中一部分是提供了一些水平的&#34;水平&#34; SIMD操作。特别是,库提供的API包括any(<boolN x M>) -> bool
和all(<boolN x M>) -> bool
个函数,其中<T x K>
是K
类型T
和boolN
元素的向量}是一个N
- 位布尔值,即全部为1或全部为零,因为SSE和NEON返回进行比较操作。
例如,让v
为<bool32 x 4>
(128位向量),它可能是VCLT.S32
或其他内容的结果。我想计算all(v) = v[0] && v[1] && v[2] && v[3]
和any(v) = v[0] || v[1] || v[2] || v[3]
。
使用SSE很容易,例如movmskps
将提取每个元素的高位,因此上面类型的all
变为(使用C内在函数):
#include<xmmintrin.h>
int all(__m128 x) {
return _mm_movemask_ps(x) == 8 + 4 + 2 + 1;
}
,同样适用于any
。
我很难找到明显/好的/有效的方法来实现NEON,它不支持像movmskps
这样的指令。有简单地提取每个元素并使用标量计算的方法。例如。这是天真的方法,但也有使用&#34;水平&#34; NEON支持的操作,如VPMAX
and VPMIN
。
#include<arm_neon.h>
int all_naive(uint32x4_t v) {
return v[0] && v[1] && v[2] && v[3];
}
int all_horiz(uint32x4_t v) {
uint32x2_t x = vpmin_u32(vget_low_u32(v),
vget_high_u32(v));
uint32x2_t y = vpmin_u32(x, x);
return x[0] != 0;
}
(人们可以用VPADD
为后者做类似的事情,这可能会更快,但它基本上是相同的想法。)
是否还有其他技巧可用于实现此目的?
是的,我知道SIMD矢量单元的水平操作不是很好。但有时它很有用,例如mandlebrot的许多SIMD实现将同时在4个点上运行,并且当所有这些都超出范围时保释出内循环...这需要进行比较然后进行水平和。
答案 0 :(得分:2)
这是我目前在 eve library 中实施的解决方案。
如果您的后端支持 C++20,您可以使用该库:它具有 arm-v7、arm-v8(目前只有小端)和从 sse2 到 avx-512 的所有 x86 的实现。它是开源的,并获得 MIT 许可。目前处于测试阶段。如果您正在试用该库,请随时联系(例如遇到问题)。
持保留态度 - 我还没有设置手臂基准。
注意:在基本的 all 和 any 之上,我们还有一个 movemask
等效于执行更复杂的操作,例如 first_true
。这不是问题的一部分,这并不奇怪,但可以找到代码 here
ARM-V7,8 字节寄存器
现在,arm-v7 是 32 位架构,所以我们尝试尽可能使用 32 位元素。
最多使用成对 32 位。如果任何元素为真,则最大值为真。
// cast to dwords
dwords = vpmax_u32(dwords, dwords);
return vget_lane_u32(dwords, 0);
成对的最小值而不是最大值。还有你针对变化测试的内容。 如果您有 4 字节元素 - 只需测试是否为真。如果是短裤或字符 - 你需要测试 -1;
// cast to dwords
dwords = vpmin_u32(dwords, dwords);
std::uint32_t combined = vget_lane_u32(dwords, 0);
// Assuming T is your scalar type
if constexpr ( sizeof(T) >= 4 ) return combined;
// I decided that !~ is better than -1, compiler will figure it out.
return !~combined;
ARM-V7,16 字节寄存器
对于大于字符的任何内容,只需转换为 64 位。这是 vector narrow integer 次转化的列表。
对于字符,我发现最好的方法是重新解释为 uint32 并进行额外检查。 所以比较所有的 == -1 和 > 0 的任何。 拆分成两个 8 字节寄存器似乎更好。
然后在那个双字寄存器上做 all/any。
ARM-v8,8 字节
ARM-v8 支持 64 位,因此您可以获得 64 位通道。那个是可以简单测试的。
ARM-v8,16 字节
我们使用 vmaxvq_u32
因为 any
和 vminvq_u32
、vminvq_u16
或 vminvq_u8
没有 64 位的 all
,具体取决于元素大小。
(类似于glibc strlen)
结论
缺乏基准肯定让我担心,有些指令有时会出现问题,我对此一无所知。 无论如何,这是我所拥有的最好的,至少到目前为止是这样。
答案 1 :(得分:1)
注意:今天第一次看胳膊,我可能是错的。
UPD:删除了 ARM-V7,并将在单独的答案中写下我们最终做了什么
ARM-V8。
对于 ARM-V8,请查看 glibc 中的 strlen 实现: https://code.woboq.org/userspace/glibc/sysdeps/aarch64/multiarch/strlen_asimd.S.html
ARM-V8 引入了跨寄存器的缩减。这里他们使用 min 与 0 进行比较
uminv datab2, datav.16b
mov tmp1, datav2.d[0]
cbnz tmp1, L(main_loop)
找到最小的字符,与 0 比较 - 取接下来的 16 个字节。
ARM-V8 中还有其他一些减少,例如 vaddvq_u8
。
我敢肯定,您可以使用 movemask
完成大部分您想做的事情。
这里另一个有趣的事情是他们如何找到 first_true
/* Set te NULL byte as 0xff and the rest as 0x00, move the data into a
pair of scalars and then compute the length from the earliest NULL
byte. */
cmeq datav.16b, datav.16b, #0
mov data1, datav.d[0]
mov data2, datav.d[1]
cmp data1, 0
csel data1, data1, data2, ne
sub len, src, srcin
rev data1, data1
add tmp2, len, 8
clz tmp1, data1
csel len, len, tmp2, ne
add len, len, tmp1, lsr 3
看起来有点吓人,但我的理解是:
所以 - 如果您只需要 V8 - 有一个解决方案。