我自己算出来的,没有找到任何答案avx1(没有avx2)。这就是未来人们寻找答案的答案。
8-float m256 max
,然后可用于标准化,因为_max
将用x
填充
__m256 _inv2_max;
// Normaliser x
__m256 _inv = _mm256_permute_ps(x, 0b00011011);
__m256 _max = _mm256_max_ps(x, _inv);
_inv2_max = _mm256_permute_ps(_max, 0b000000010);
_max = _mm256_max_ps(_inv2_max, _max);
vlow = _mm256_castps256_ps128(_max);
vhigh = _mm256_extractf128_ps(_max, 1);
__m128 a[1] = {_mm_permute_ps(_mm_max_ps(vlow, vhigh), 0b00000000)};
_max = _mm256_broadcast_ps(a);
_mm_max_ps(vlow, vhigh)
的max已经是[0]。在这里我实现了max被brodcast到_max的每个位置。
3条答案
按热度按时间holgip5t1#
当你只想要一个标量结果时,通常你想缩小一半,直到你减少到一个元素。首先从
_mm256_extractf128_ps
/_mm256_castps256_ps128
开始,因此您的其余操作是128位而不是256位,这使得它们在Zen 1和桤木Lake及更高版本中的E核心上更快。这在Fastest way to do horizontal SSE vector sum (or other reduction)和How to sum __m256 horizontally?中讨论了标量浮点数的有效hsum,以_mm_cvtss_f32
结尾。有关128位与在不同的CPU上进行256位操作,包括Zen 1。但是,它需要额外的指令来将标量广播回向量的每个元素。
vbroadcastss ymm, xmm
在AVX 2中是新的,因此AVX 2需要存储/重新加载内存源vbroadcastss
,或者使用两个shuffle(in-lane和vinsertf128
)。(问题中的代码为128位广播而不是32位广播执行shuffle * 和 * store/reload。这里有两个不错的选择:
*始终保持所有元素都是256位,并使用shuffle/max,使每个元素都获得max。一个容易验证的模式是 * 交换 *,而不仅仅是将高元素降低到低。例如交换浮点数对,然后交换64位块,然后交换128位通道。
(When逻辑是“明显”正确的,而不必遵循不同元素发生的不同事情来检查每个结果元素“看到”每个输入元素,这更容易阅读和维护代码。我认为,其他一些答案的尝试实际上是错误的。测试可以帮助解决这个问题,例如。让单元测试制作一个像
float x[] = {0, 1, 0, 0,...}
这样的数组,在不同的位置放置max。或者以相反的顺序执行,从
vperm2f128
(_mm256_permute2f128_ps
)开始,因为您正在执行min和max,所以您可以将其中一个 Shuffle 的结果用于min和max。由于在某些CPU上,特别是在Zen 1上的vperm2f128
上,跨车道 Shuffle 的成本更高,因此只进行一次 Shuffle 具有优势。(否则,我建议先做低延迟的通道内shuffle,这样更多的shuffle/max依赖链可以更快地从调度器和ROB中释放出来,给无序的exec一个更轻松的时间。*或者,减少到128位,然后shuffle/max,这样
__m128
的每个元素最终都是相同的,然后再次扩展到256位。(如果你有AVX 2用于标量到ymm广播,你可以做标量。)在你的情况下,你可以做一个128位宽度的sub
,但其他操作涉及或依赖于原始的x
,其中所有8个元素都是不同的。这种策略可能对Zen 1和Intel E-cores有好处,但通常对具有256位向量执行单元的CPU(如Zen 2和更高版本,以及Intel大核心)来说更糟,因为在这些CPU上,256位操作通常与128位操作相同。(使用更多的功率,所以最大涡轮增压可以减少。
如果你做了很多这样的事情(对于许多8元素阵列),
vdivps
吞吐量可能是一个因素,特别是在可以运行此代码的最旧的CPU上(使用AVX 1但不使用AVX 2,如Sandybridge,其中256位vdivps ymm
吞吐量是每14个周期一个,而不是每14个周期一个)。vdivps xmm
或vdivss
标量在div/sqrt单元上具有7个周期的吞吐量成本,并且与Skylake不同,256位div
的延迟更高。查看另一个关于各种微架构上的div吞吐量/延迟的Q&A,以及它如何比其他操作差得多。因此,您可以考虑使用128位的
1/(max-min)
,并将其与(x-min) * recip_scale
一起使用。如果你的数组更大(重复使用元素的多个向量的比例因子),这将是更值得的,尽管这样你就不会有相同的x
的最小值/最大值,你会有单独的mins和maxs的最小值和最大值来独立地减少。(在这种情况下,您可以先对一个执行通道交叉混洗,最后对另一个执行通道交叉混洗,以减少对min
/max
执行端口的争用。)您甚至可以重新排列为arr*(recip_scale) - (min*recip_scale)
,这样您就只需要对x
的每个向量执行一个FMA。可能只对一个向量不值得,因为计算(min*recip_scale)
需要额外的操作。但是有AVX 1 CPU没有FMA。(还有一个不起眼的Via CPU,它有AVX 2,但没有FMA,但如果你要制作这个函数的另一个版本,请使用-march=x86-64-v3
AVX 2 +FMA+ BMI 1/2。vrcpps
可用于快速近似倒数,但在没有a Newton-Raphson iteration的情况下仅具有约12位精度。在像Skylake这样的现代CPU上(256位vdivps
的5周期吞吐量),额外的uop可能比div
更成为瓶颈,因为它是许多其他操作的一部分。在像Sandybridge这样的老CPU上,如果你不需要完全的精度,这可能是值得的。(如果你没有制作一个单独的AVX 2 +FMA版本,更现代的CPU将使用,你可能应该调整这个版本与现代CPU的想法,因为他们也会运行它。否则,您应该只关心具有AVX 1但不具有AVX 2的CPU:桑迪/常春藤桥,美洲虎,和推土机家族的大部分。)缩窄到128位,然后重新加宽
这在128位(包括
max-min
)上做了尽可能多的工作。我把min
放在第一位是为了鼓励编译器把这些指令放在第一位,这样关键路径的一部分就可以领先一步,让x - min
减法在max-min
仍然广播的时候发生。编译器自己安排指令,所以它可能会做一些不同的事情。当然,OoO exec将交错执行大部分工作,但最旧的就绪优先调度将优先考虑最先可见的指令。来自
immintrin.h
的_MM_SHUFFLE
是一个宏,它可以轻松地用4x 2位索引字段写入8位混洗控制常量。最高的位置在左边,所以_MM_SHUFFLE(3,2,1,0) == 0b11'10'01'00
是身份 Shuffle 。全程256位
请注意,两个版本都使用相同的
_MM_SHUFFLE
常量在128位通道内进行混洗。这不是意外;这两种方法都希望所有元素的最小值或最大值。这两个版本都在Godbolt上编译为合理的asm。
vperm2f128
在Zen 1上相当慢(8 uops,3c吞吐量),vinsert/extractf 128在Zen 1上非常高效,所以normalise_128
肯定会更快,不会在__m256
的每一半做冗余工作。微优化:
vshufps
而不是vpermilps
vpermilps
对于您的用例来说是“显而易见的”shuffle,但它不是最有效的。在Ice Lake和桤木Lake P核上,它只能在执行端口5上运行(该端口有一个shuffle单元,可以处理每次shuffle)。旧SSE 1指令的AVX版本
vshufps
有2个输入操作数,但使用相同的输入两次,它可以执行相同的混洗。由于Ice Lake可以在端口1或5上运行它,因此使用它可以减少端口5中shuffle单元的瓶颈。(最小/最大可以在端口0或1上运行。)vpermilps
在KNL Xeon Phi上会更好,因为2输入 Shuffle 速度较慢,但在其他CPU上则不然。他们在Skylake和AMD Zen上是平等的。(https://uops.info/)不幸的是,clang没有将
_mm_permute_ps
优化为vshufps
,即使是-mtune=icelake-client
。**事实上,它和GCC 12以及更高版本做了相反的事情,将_mm_shuffle_ps(same,same, i8)
pessimize为vpermilps
。因此,对于GCC 11和更早版本以及MSVC等编译器,最好使用
_mm_shuffle_ps(min,min, _MM_SHUFFLE(2,3, 0,1))
编写源代码。(Godbolt显示asm差异)对于以后的GCC和clang,希望他们能整理出他们的调优规则,并了解到
vpermilps
的吞吐量更差,所以他们应该使用vshufps
,除非有某种原因避免让shuffles在端口1上调度,因为它们可以与FP数学/比较操作(如min/max)竞争。如果你只是在执行这些标准化操作中的一个与其他周围代码混合,那么端口5上的额外压力可能是相关的,也可能不是相关的,或者如果后面的指令有很多非端口5的工作(比如FP数学),这可能是一件好事,这些工作独立于这个工作,如果它不占用周期,可能会重叠。
cgh8pdjw2#
使用
[(e-min)/(max-min) for e in array]
规范化数组关于m128
只有m256
k3fezbri3#
对于AVX1
如果
x = {0,4,3,-1,7,8,-2,7}
,则__max
将是{-2,-2,-2,-2,-2,-2,-2,-2}
为了只得到max,提取最后一个vlow的第一个元素。
提取:
((float*)&vlow)[0]
或者使用m256唯一版本
在我的规模,它有相同的表现,也许m256一个更稳定(和一点点,但更快)。