为什么 vhaddps 指令会以如此复杂的方式添加?

Why does the vhaddps instruction add in such an involved way?

vhaddps指令以一种非常奇特的方式添加:

来源:https://www.felixcloutier.com/x86/haddps

这是什么原因?该指令适用于哪些用例?看起来设计有一些特定的想法。

在低和高 128 位通道中有 2 in-lane haddps 条指令。 大多数 AVX 指令并没有真正将操作扩展到 256 位,它们执行 2 个单独的 in-lane 操作。这使得 AVX 难以使用,尤其是在没有 AVX2 的情况下 lane-crossing 小于 128 位粒度的随机播放!

但它节省了晶体管,例如。制作 vpshufb 单个 32 字节随机播放而不是 2x 16 字节随机播放。 AVX2 甚至不提供:(必须等待 AVX512VBMI)。

(相关: Also, AVX512 adds a lot of flexible lane-crossing shuffles, but the AXV512 versions of SSE/AVX instructions like vhaddps zmm are still in-lane. See also

AVX2 vpack* 链通常需要 vpermq 在最后进行 lane-crossing 修复,除非您要再次解压 in-lane。 所以在大多数情况下,2x in-lane 洗牌比完整的 256 位宽操作更糟糕,但这不是我们从 AVX 获得的结果。 通常仍然有加速的空间从 128 到 256 位向量,即使它需要额外的洗牌来纠正 in-lane 行为,但这通常意味着它不是 2 倍加速,即使没有内存瓶颈。

vpalignr 可能是同一 shuffle 的 2x 128 位版本本身并不是有用的构建块的最令人震惊的例子;我不记得我是否见过 use-case 用于获取 2 个单独的 in-lane 字节 windows 数据。哦,实际上是的,如果你用 vperm2i128 喂它,但通常未对齐的负载在支持 AVX2 的 CPUs 上更好。


(v)haddps 的 use-case 非常有限

也许英特尔计划在将 haddps 引入 SSE3 后的某个时候将其变成 single-uop 指令,但那从未发生过。

use-cases 包括 transpose-and-add 类型的东西,无论如何你都需要在垂直 addps 中打乱两个输入。例如 包括 vhaddps。 (加上 AVX1 vperm2f128 以纠正 in-lane 行为。)

许多人错误地认为它适用于单个向量的水平求和,但 128 位和 256 位 (v)haddps 都解码为 2x 随机微指令,为垂直 (v)addps 微指令准备输入向量。对于水平总和,每次添加只需要 1 个洗牌 uop。 (Fastest way to do horizontal float vector sum on x86)

首先缩小到 128 位(使用 vextractf128 / vaddps)通常是更好的第一步,除非您希望将结果广播到每个元素,并且您没有使用 AMD CPU(其中 256 位向量运算解码为至少 2 微指令,或更多 lane-crossing 随机播放)。 (v)haddps xmm 或整数 vphaddd 如果您针对 code-size 而不是速度进行优化,则对水平求和很有用,例如my x86 machine-code answer 关于 code-golf 问题 "Calculate the Mean mean of two numbers"。

AVX non-destructive 目标操作数也消除了具有 multi-uop 指令的一些吸引力。如果没有 AVX,有时您无法避免 movaps 在销毁寄存器之前复制寄存器,因此烘焙 2x 洗牌 + 添加到 1 条指令实际上节省了 uops 与必须使用 movaps + 手动执行此操作相比shufps.

与许多 256 位宽的指令一样,高 128 位 vhaddps ymm ymm ymm 只是 128 位宽 vhaddps xmm xmm xmm 的复制粘贴 操作说明。下面的例子表明它是有意义的 以复杂的方式定义 vhaddps xmm xmm xmm:两次使用这条指令 为您提供 4 xmm 个寄存器的水平总和。

/* gcc -m64 -O3 hadd_ex.c -march=sandybridge           */
#include<immintrin.h>
#include<stdio.h>
int main(){
    float tmp[4];
    __m128 a = _mm_set_ps(1.0, 2.0, 3.0, 4.0);
    __m128 b = _mm_set_ps(10.0, 20.0, 30.0, 40.0);
    __m128 c = _mm_set_ps(100.0, 200.0, 300.0, 400.0);
    __m128 d = _mm_set_ps(1000.0, 2000.0, 3000.0, 4000.0);
    __m128 sum1 = _mm_hadd_ps(a, b);
    __m128 sum2 = _mm_hadd_ps(c, d);
    __m128 sum = _mm_hadd_ps(sum1, sum2);
    _mm_storeu_ps(tmp,sum);
    printf("sum = %f  %f  %f  %f\n", tmp[0], tmp[1], tmp[2], tmp[3]);
    return 0;
}

输出:

sum = 10.000000  100.000000  1000.000000  10000.000000