SIMD for float threshold operation

Sin*_*bag 3 c++ double sse simd vectorization

I would like to make some vector computation faster, and I believe that SIMD instructions for float comparison and manipulation could help, here is the operation:

void func(const double* left, const double* right, double* res, const size_t size, const double th, const double drop) {
        for (size_t i = 0; i < size; ++i) {
            res[i] = right[i] >= th ? left[i] : (left[i] - drop) ;
        }
    }
Run Code Online (Sandbox Code Playgroud)

Mainly, it drops the left value by drop in case right value is higher than threshold.

The size is around 128-256 (not that big), but computation is called heavily.

I tried to start with loop unrolling, but did not win a lot of performance, but may be some compile instructions are needed.

Could you please suggest some improvement into the code for faster computation?

Pet*_*des 7

Clang已经按照Soonts建议的手动方式进行了自动矢量化。__restrict在指针上 使用,因此不需要后备版本,该版本可用于某些数组之间的重叠。它仍然会自动矢量化,但会使功能肿。

不幸的是,gcc仅使用进行自动矢量化-ffast-math。事实证明,这仅-fno-trapping-math是必需的:通常这是安全的,特别是如果您不使用fenv访问权限来揭露任何FP异常(feenableexcept)或查看MXCSR粘性FP异常标志(fetestexcept)。

使用该选项,那么GCC也将(v)pblendvpd-march=nehalem或一起使用-march=znver1在Godbolt上看到

此外,您的C函数已损坏。 th并且drop是标量双精度,但您将其声明为const double *


AVX512F允许您进行!(right[i] >= thresh)比较,并将结果掩码用于合并掩码减法。

谓词为true的left[i] - drop元素将获得,其他元素将保留其left[i]值,因为您将info合并为left值的向量。

不幸的是,GCC -march=skylake-avx512使用法线vsubpd,然后使用单独的法线vmovapd zmm2{k1}, zmm5进行混合,这显然是错过的优化方法。混合目标已经是SUB的输入之一。

将AVX512VL用于256位向量(以防程序的其余部分无法有效使用512位,因此您的Turbo时钟速度不会降低):

__m256d left = ...;
__m256d right = ...;
__mmask8 cmp = _mm256_cmp_pd_mask(right, set1(th), _CMP_NGE_UQ);
__m256d res = _mm256_mask_sub_pd (left, cmp, left, set1(drop));
Run Code Online (Sandbox Code Playgroud)

因此(除加载和存储外)这是AVX512F / VL的2条指令。


如果您不需要版本的特定NaN行为,GCC也可以自动矢量化

而且它对所有编译器都更有效,因为您只需要AND,而不是变量混合。 因此,仅使用SSE2会明显更好,并且即使在大多数CPU支持SSE4.1的情况下也是如此blendvpd,因为该指令的效率不高。

您可以减去0.0dropleft[i]基于比较结果。

0.0根据比较结果产生或常量非常有效:只需一条andps指令即可。(的位模式为0.0全零,SIMD会比较全1或全0位的产生向量。因此,AND会将旧值保留或将其清零。)

我们也可以加-drop而不是减drop。这会在输入上造成额外的否定,但是使用AVX可以使用的存储源操作数vaddpd。GCC选择使用索引寻址模式,因此实际上并没有帮助减少Intel CPU的前端uop数量。它将“分层”。但是即使使用-ffast-math,gcc也不会自行进行此优化以允许折叠负载。(不过,除非我们展开循环,否则不应该单独进行指针增量操作。)

void func3(const double *__restrict left, const double *__restrict right, double *__restrict res,
  const size_t size, const double th, const double drop)
{
    for (size_t i = 0; i < size; ++i) {
        double add = right[i] >= th ? 0.0 : -drop;
        res[i] = left[i] + add;
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的Godbolt链接提供的GCC 9.1的内部循环(不带任何-march选项,不带-ffast-math):

# func3 main loop
# gcc -O3 -march=skylake       (without fast-math)
.L33:
    vcmplepd        ymm2, ymm4, YMMWORD PTR [rsi+rax]
    vandnpd ymm2, ymm2, ymm3
    vaddpd  ymm2, ymm2, YMMWORD PTR [rdi+rax]
    vmovupd YMMWORD PTR [rdx+rax], ymm2
    add     rax, 32
    cmp     r8, rax
    jne     .L33
Run Code Online (Sandbox Code Playgroud)

或普通的SSE2版本具有一个内部循环,基本上与with相同,left - zero_or_drop而不是left + zero_or_minus_drop,因此除非您可以保证编译器以16字节对齐或正在制作AVX版本,否则否定drop只是额外的开销。

否定drop需要不断从内存(异或符号位),这是唯一的静态常量这个功能需求,使代价是值得考虑你的情况下循环不运行的时候一个巨大的数字。(除非thdrop在内联之后还是编译时常量,并且无论如何都会被加载。或者特别是如果-drop可以在编译时进行计算。或者是否可以使程序与负数一起使用drop。)

加法和减法之间的另一个区别是减法不会破坏零的符号。 -0.0 - 0.0 = -0.0+0.0 - 0.0 = +0.0。以防万一。

# gcc9.1 -O3
.L26:
    movupd  xmm5, XMMWORD PTR [rsi+rax]
    movapd  xmm2, xmm4                    # duplicate  th
    movupd  xmm6, XMMWORD PTR [rdi+rax]
    cmplepd xmm2, xmm5                    # destroy the copy of th
    andnpd  xmm2, xmm3                    # _mm_andnot_pd
    addpd   xmm2, xmm6                    # _mm_add_pd
    movups  XMMWORD PTR [rdx+rax], xmm2
    add     rax, 16
    cmp     r8, rax
    jne     .L26
Run Code Online (Sandbox Code Playgroud)

GCC使用未对齐的负载,因此(没有AVX)它无法将内存源操作数折叠为cmppdsubpd

  • “ -ftrapping-math”和“ -frounding-math”已完全损坏,但它们从未起作用。期望他们做出一致,明智的选择是相当乐观的。 (4认同)
  • @Soonts:VEX编码的指令不需要对齐其内存源操作数,除非特别*具有*的对齐要求(例如“ vmovaps”或“ vmovntps”)。即使对于128位指令,这也是VEX编码的众多优势之一。如果使用-march = sandybridge编译版本,则会看到该编译器可以将_mm_loadu_pd折叠到一个内存操作数中进行比较。(如果它可以找到符合`right []`作为第二个来源的谓词;但AVX确实为`vcmppd`添加了一堆新的比较谓词。) (3认同)
  • `-fno-trapping-math'似乎足以使gcc向量化。 (3认同)
  • 默认情况下,四舍五入是关闭的,而对于陷阱它是PR54192。 (3认同)
  • @MarcGlisse:有没有人建议默认关闭它们?如果它们不起作用,那么它们只是在创建错过的优化,没有好处。 (2认同)
  • 加而不是减的另一种(在大多数情况下不相关)效果:-0.0 + 0.0 == +0.0,而-0.0-0.0 == -0.0,即,在left中的-0.0将被转换。到“ +0.0”。 (2认同)