SIMD 零向量测试

Posted

技术标签:

【中文标题】SIMD 零向量测试【英文标题】:SIMD zero vector test 【发布时间】:2015-03-14 17:18:09 【问题描述】:

是否存在一种快速的方法来检查 SIMD 向量是否为零向量(所有分量都等于 +-0)。我目前正在使用一种算法,使用移位,在 log2(N) 时间内运行,其中 N 是向量的维度。有没有更快的东西?请注意,我的问题比建议的答案更广泛(标签),它涉及所有类型的向量(整数、浮点数、双精度......)。

【问题讨论】:

Most efficient way to check if all __m128i components are 0 [using SSE intrinsics]的可能重复 取决于可用的说明,将neon和avx标记在一起我不知道你在做什么。 好吧,在最一般的情况下,您可能会遇到 log(n),如果您在 OR 树(或对一起直到你有 1 位为 0 当且仅当所有输入位都为 0 时才会是 log(n) 层深。所以,除非我们能稍微改变一下球门柱,否则这实际上是不可能的。 如果N 很大,那么显而易见的解决方案是处理W 组中的元素,其中W 是通过ORing 将它们组合在一起可以处理的最大块。到达终点后,您可以选择:1) ptest 在 SSE4.1+ 上 2) 与零比较,然后在 SSE2+ 和整数类型上比较 pmovmskb,或 movmskps/movmskpd 对于单/双精度.如果 N 特别大,最后的减少不会是你的瓶颈;这将是数据流,将被 ORed 到您的寄存器中。对于 ARMv7 上的 NEON,可以使用成对无符号 maxes 完成最终缩减,直到达到 32 位字长。 @user1095108 事实上,我做到了,@harold 也做到了。如果向量中的唯一值是+0,则归约结果将相同为+0,如果只有+0 和-0,则OR-reduction 将生成-0。当然,您必须知道 IEEE 要求 -0 比较等于 0,因此当您在 movmskps 之前对 == 0 进行比较时,您将正确地忽略零的符号。 【参考方案1】:

这个简单的 avx 代码怎么样?我认为它是 O(N) 并且不知道如何在不对输入数据做出假设的情况下做得更好 - 你必须实际读取每个值才能知道它是否为 0 所以它是关于每个周期尽可能多地做.

您应该能够根据需要调整代码。应将 +0 和 -0 都视为零。将适用于未对齐的内存地址,但对齐到 32 字节地址将使加载更快。如果 size 不是 8 的倍数,您可能需要添加一些东西来处理剩余字节。

uint64_t num_non_zero_floats(float *mem_address, int size) 
    uint64_t num_non_zero = 0;
    __m256 zeros _mm256_setzero_ps ();
    for(i = 0; i != size; i+=8) 
        __m256 vec _mm256_loadu_ps (mem_addr + i);
        __m256 comparison_out _mm256_cmp_ps (zeros, vec, _CMP_EQ_OQ); //3 cycles latency, throughput 1
        uint64_t bits_non_zero = _mm256_movemask_ps(comparison_out); //2-3 cycles latency
        num_non_zero += __builtin_popcountll(bits_non_zero);
    
    return num_non_zero;

【讨论】:

我认为 __builtin_popcountll 与确定零无关。 是的,只需测试 bits_non_zero ,当然,如果您想要优化,如果它不为零,则退出循环停止不必要的处理。该代码实际上计算了向量中非零的浮点数,正如其名称所暗示的那样 - 因此“您应该能够根据您的需要按摩代码” @Hal 这不太可能比将数据加载为__m256i 更快,ORing 到两个单独的累加器,然后在最后,ORing 累加器一起,转换为__m256,做@ 987654324@ 和执行_mm256_movemask_ps()vptest。话虽如此,您可以使用一个巧妙的技巧来避免在内部循环中进行掩码移动和弹出计数,就是将比较结果转换为整数,然后从累加器中减去。如果比较结果全为 1 (== -1),则从累加器中减去它就是向其添加 +1。如果为0,则减法无效。 @Hal 简而言之,__m256i acc = _mm256_setzero_si256();/* In the loop... */ acc = _mm256_sub_epi32(acc, _mm256_castps_si256(comparison_out));。然后在循环结束时将 8 个累加器相加,而您永远不必通过 popcount。这适用于具有 SSE 的系统和没有 popcount 的系统。【参考方案2】:

如果您想测试 +/- 0.0 的浮点数,那么您可以检查除符号位之外的所有位是否为零。除符号位之外的任何位置的任何设置位都意味着浮点数非零。 (http://www.h-schmidt.net/FloatConverter/IEEE754.html)


Agner Fog 的asm optimization guide 指出,您可以使用整数指令测试浮点数或双精度数为零:

; Example 17.4b
mov  eax, [rsi]
add  eax, eax   ; shift out the sign bit
jz   IsZero

不过,对于向量,使用带有符号位掩码的 ptest 比使用 paddd 去除符号位要好。实际上,test [rsi], $0x7fffffff 可能比 Agner Fog 的加载/添加序列更有效,但 32 位立即数可能会阻止加载在 Intel 上的微融合,并且可能具有更大的代码大小。


x86 PTEST (SSE4.1) 执行按位与并根据结果设置标志。

movdqa xmm0, [mask]
.loop:
ptest  xmm0, [rsi+rcx]
jnz    nonzero
add    rcx, 16  # count up towards zero
jl     .loop    # with rsi pointing to past the end of the array
...
nonzero:

或者cmov 可能有助于使用ptest 设置的标志。

IDK 如果可以使用没有设置零标志的循环计数器指令,那么您可以使用一条跳转指令或其他东西进行这两个测试。可能不是。合并标志的额外 uop(或早期 CPU 上的部分标志停顿)将抵消好处。

@Iwillnotexist Idonotexist:重新成为 OP 上的 cmets 之一:如果不先执行 pcmpeqcmpps,就不能只移动掩码。非零位可能不在高位!您可能知道这一点,但您的一位 cmets 似乎忽略了它。

我确实喜欢在实际测试之前将多个值组合在一起的想法。您是对的,符号位会与其他符号位进行 OR,然后您会像一次测试一个符号位一样忽略它们。 PORs 在每个 PTEST 之前有 4 或 8 个向量的循环可能会更快。 (PTEST 是 2 微秒,不能与 jcc 进行宏融合。)

【讨论】:

以上是关于SIMD 零向量测试的主要内容,如果未能解决你的问题,请参考以下文章

Swift 中的向量 SIMD 类型

动态分配 SIMD 向量数组是不是安全?

AAch64 高级 SIMD 向量加法

为啥向量长度 SIMD 代码比普通 C 慢

向量与 SIMD 的点积

使用 SIMD 根据另一个向量位值计算值的乘积