Assembly 了解vpermd(或vpermps)的优化顺序

Assembly 了解vpermd(或vpermps)的优化顺序,assembly,x86-64,avx,micro-optimization,Assembly,X86 64,Avx,Micro Optimization,基本上,假设您在编译时有一个排列索引列表,我试图了解x86_64指令选择的最佳顺序 我了解大多数优化选择,但有一个案例我很难理解 给定的排列顺序可以实现为: _mm256_permutevar8x32_epi32(r, _mm256_set_epi32(/* indicies */)); 或 我不明白为什么第一种选择会更好 以排列列表7,6,5,4,3,2,1,0为例(反向epi32) 这两个选项都需要2个uop,鉴于这两个指令之间存在依赖关系,我认为端口并不重要。我看到的唯一区别是第

基本上,假设您在编译时有一个排列索引列表,我试图了解x86_64指令选择的最佳顺序

我了解大多数优化选择,但有一个案例我很难理解

给定的排列顺序可以实现为:

    _mm256_permutevar8x32_epi32(r, _mm256_set_epi32(/* indicies */));

我不明白为什么第一种选择会更好

以排列列表
7,6,5,4,3,2,1,0
为例(反向epi32)

这两个选项都需要2个uop,鉴于这两个指令之间存在依赖关系,我认为端口并不重要。我看到的唯一区别是第一个选项额外添加了32字节的.rodata

有人能帮我理解为什么Clang(我猜是Agner Fog)更喜欢第一个选项而不是第二个选项吗


是skylake编译输出的一个godbolt链接

对于
load\u perm
,clang似乎喜欢把事情变成
ps
形式。这为传统SSE编码节省了代码大小(其中SSE1指令的前缀更少)。但不使用VEX编码,因此没有好处。只是clang的洗牌优化器显然不知道或不关心保持整数与FP域的区别。我认为这对于当前CPU上的洗牌很好

对于
perm_shuf
,这绝对是clang的洗牌优化器在做它的工作。其他编译器不太擅长像对待
+
*
运算符那样对待随机洗牌内部函数:作为指定所需结果的方法,而不必指定如何实现。e、 g.
x*y
对于x86不必编译为
imul
,选择可以取决于周围的代码

大多数SIMD代码都是在循环中运行的,所以假设一个shuffle常量在缓存中保持热状态并被多次使用也不错。特别是如果内联线和随机向量可以被提升。但即使不是,也值得加载一个常量对于从
m
输入到
return
value
的关键路径的延迟,以及Intel CPU上的端口5 UOP(从Haswell开始,到Ice Lake,通常每个时钟限制1次洗牌),一次洗牌优于2次


顺便说一句,
m
是一个非常糟糕的变量名选择:它到达一个寄存器,您在注释中使用
m
来谈论内存常量。

很好的调用,将
m
更改为
r
。但是,负载是否也在关键路径上?根据Agner Fog的指令表
vmovaps
的延迟与
vpshufd
vpermq
相同,因此不了解.rodata的32字节(以及可执行的膨胀)是如何构成的。你说的“洗牌向量可以被提升”是什么意思?你的意思是重复使用它多次加载到的同一个寄存器吗?@Noah:不,加载地址实际上是一个立即常数,可以作为解码指令的一部分。(RIP相对或32位绝对,取决于模式。)load uop可以在
v
(正在洗牌的数据)准备就绪之前执行,因此只有
vpermd
延迟是从
v
到结果的关键路径的一部分。当然,如果调用此函数时I-cache未命中或发生了什么情况,则加载不可能提前启动,和/或向量数据加载可能在缓存中丢失。@Noah:显然,如果我们讨论的是一个可以提升恒定负载的循环,那么效果会更好
vmovaps
来自
.rodata
在循环外部,而
vpermps
在循环内部。然后,洗牌的总uop只有1个,任何缓存未命中的风险都会在循环迭代次数上摊销。顺便说一句,如果你只加载一次,你可以通过
vpmovzxbd
Ah我明白了,加载它,将shuffle常量压缩到8字节。作为旁注,我想知道为什么Agner Fog选择在他的向量库中执行
\u mm256\u shuffle\u epi16
\u mm256\u shufflehi\u epi16
之前执行
\u mm256\u shuffle\u epi8
。有什么想法吗?e、 g他优先考虑在一个洗牌epi8上同时击打两个球的情况。@Noah:是的,这很合理。尽管您可能会将循环计算为“热”,即使它实际上并不经常运行。特别是如果迭代次数是输入的次数,那么您将在大量使用中摊销负载。如果代码真的很冷,那么通常不应该首先对其进行矢量化,或者以更紧凑的方式(如128位矢量)进行矢量化,如果这样做只会使运行时间的一小部分(如果它使二进制文件变小的话)慢几%。
    __m256i tmp = _mm256_permute4x64_epi64(r, /* some mask */);
    return _mm256_shuffle_epi32(tmp, /* another mask */);
__m256i
load_perm(__m256i r) {
    // clang
    // 1 uop vmovaps (y, m) p23
    // 1 uop vpermps (y, y, y) p5

    // gcc
    // 1 uop vmovdqa (y, m) p23
    // 1 uop vpermd (y, y, y) p5
    return _mm256_permutevar8x32_epi32(r, _mm256_set_epi32(0, 1, 2, 3, 4, 5, 6, 7));
}

__m256i
perm_shuf(__m256i r) {
    // clang
    // 1 uop vmovaps (y, m) p23
    // 1 uop vpermps (y, y, y) p5

    // gcc
    // 1 uop vpermq (y, y, i) p5
    // 1 uop vpshufd (y, y, i) p5
    __m256i tmp = _mm256_permute4x64_epi64(r, 0x4e);
    return _mm256_shuffle_epi32(tmp, 0x1b);
}