有没有更好的方法来检测 16 字节标志数组中设置的位?

the*_*ilz 1 c++ sse x86-64 simd micro-optimization

    ALIGNTO(16) uint8_t noise_frame_flags[16] = { 0 };

    // Code detects noise and sets noise_frame_flags omitted

    __m128i xmm0            = _mm_load_si128((__m128i*)noise_frame_flags);
    bool    isNoiseToCancel = _mm_extract_epi64(xmm0, 0) | _mm_extract_epi64(xmm0, 1);

    if (isNoiseToCancel)
        cancelNoises(audiobuffer, nAudioChannels, audio_samples, noise_frame_flags);
Run Code Online (Sandbox Code Playgroud)

这是我在 Linux 上的 AV Capture 工具的代码片段。这里的noise_frame_flags是16通道音频的标志数组。对于每个通道,相应的字节可以是 0 或 1。1 表示该通道有一些噪声需要消除。例如,如果noise_frame_flags[0] == 1,则意味着设置了第一个通道噪声标志(通过省略的代码)。

即使设置了一个“标志”,我也需要调用cancelNoises. 这段代码在这方面似乎工作得很好。正如您所看到的,我曾经_mm_load_si128加载正确对齐的整个标志数组,然后加载两个_mm_extract_epi64来提取“标志”。我的问题是有更好的方法来做到这一点(也许使用流行计数)?

注意:ALIGNTO(16)是一个宏扩展以纠正 GCC 等效项,但外观更好。

Pet*_*des 8

是的,您最终需要使用 64 位 OR 来查找任一半中的任何非零位,但从uint64_t128 位加载中获取这些值然后提取的效率并不高。

在 asm 中,您只需要一个mov负载和一个内存源oradd,这将设置 ZF 就像您现在所做的那样。来自同一缓存行的两次加载非常便宜;当前的 CPU 至少具有 2/时钟的负载吞吐量。从单个 128 位负载中提取额外的 ALU 工作是不值得的,即使您对por单个movq.

在 C++ 中,使用memcpy来执行 tmp 变量的严格别名安全加载uint64_t,然后if(a | b). 这仍然是 SIMD,只是SWAR(寄存器内的 SIMD)。

add甚至比 更好or:它可以jcc与Intel Sandybridge 系列(但不是 AMD)上的 大多数指令进行宏融合。or无法与任何 CPU 上的分支指令融合。由于您的值是0or 1,我们不能有两个非零值相加产生零的情况,这就是您通常or在一般情况下使用的原因。

(某些寻址模式可能会击败英特尔上的微融合或宏融合。或者它可能总是有效,因为没有立即涉及。add rax, [mem]/确实有可能jnz作为单个微指令通过前端和 ROB,并在后面执行-end 仅为 2(加载+添加/子分支)。假设它cmp与我的 Skylake 上的大致相同,除了它确实写入了目的地,因此 Haswell 和更高版本甚至可以在索引寻址模式下保持微融合。 )

    uint64_t a, b;
    memcpy(&a, noise_frame_flags+0, sizeof(a));   // strict-aliasing-safe loads
    memcpy(&b, noise_frame_flags+8, sizeof(b));   // which optimize to MOV qword
    bool  isNoiseToCancel = a + b;   // equivalent to a | b  for bool inputs
Run Code Online (Sandbox Code Playgroud)

这应该编译为 3 个 asm 指令,总共解码为 2 uops,或者在 AMD CPU 上解码为 3 条,其中 JCC 只能与cmp或融合test

union { alignas(16) uint8_t flags[16]; uint64_t chunks[2];};在 C99 中是安全的,但在 ISO C++ 中则不然。大多数(但不是全部)支持 Intel 内在函数的 C++ 编译器定义了联合类型双关的行为。(我认为 @jww 已经说过 SunCC 没有。)

在 C++11 中,您不需要 的自定义宏ALIGNTO(16),只需使用alignas(16). 如果您也支持 C11#include <stdalign.h>


备择方案:

movdqa16 字节负载 / SSE4.1 ptest xmm0, xmm0/ jnz- Intel CPU 上为 4 uops,AMD 上为 3 uops。
Intelptest以 2 uops 运行,并且它不能与jcc.
AMD CPUptest以 1 uop 运行,但仍然无法熔断。
如果寄存器中有一个全 1 或全 0 常量,ptest xmm0, [mem]则可以在 Intel 上保存一个 uop(取决于寻址模式),但这仍然是 3 个。

PTEST 仅适用于使用 AVX1 或 AVX2 检查 32 字节数组。(令人惊讶的是,vptest ymm只需要 AVX1)。然后是关于 AVX2的收支平衡vmovdqa// vpslld ymm0, 7/ 。请参阅 TrentP 的答案,了解可移植的 GNU C 本机向量源代码,该代码应该编译为具有可用 AVX 的 x86,并且可能在其他 ISA(例如 ARM)上编译为笨重的东西,具体取决于它们的水平 OR 支持有多好。vpmovmskb eax,ymm0test+jnzvptest


popcnt除非您想根据设置的位数来分解工作,否则不会有用。
在这种情况下,是的,当然,您可以将 bool 数组转换为可以轻松扫描的位图,这可能比_mm_sad_epu8将归零寄存器求和为两个 8 字节一半更有效。

   __m128i vflags = _mm_load_si128((__m128i*)noise_frame_flags);
   vflags = _mm_slli_epi32(vflags, 7);
   unsigned flagmask = _mm_movemask_epi8(vflags);
   if (flagmask) {
       unsigned flagcount = __builtin_popcount(flagmask);  // popcnt with -march=nehalem or higher
       unsigned first_setflag = __builtin_ctz(flagmask);   // tzcnt if available, else BSF
       vflags &= vflags - 1;   // clear lowest set bit.  blsr if compiled with -march=haswell or bdver2 or newer.
      ...
   }
Run Code Online (Sandbox Code Playgroud)

(实际上不要使用-march=bdver2-march=nehalem,除非您想设置 ISA 基线,但也想使用-mtune=haswell或 更现代的东西。有像 和-mpopcnt之类的单独选项-mbmi,但通常可以很好地启用某些 CPU 支持的所有 ISA 扩展,因此您不会错过编译器可以使用有用的东西。)