为什么 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
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 甚至不提供:
(相关: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
(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