C simd AVX1 m256水平最大最小归一化

hkmswyz6  于 2023-10-16  发布在  其他
关注(0)|答案(3)|浏览(106)

我自己算出来的,没有找到任何答案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的每个位置。

holgip5t

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 xmmvdivss标量在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 。

__m256 normalise_128(__m256 x)
{
    __m128 xlow  = _mm256_castps256_ps128(x);
    __m128 xhigh = _mm256_extractf128_ps(x, 1);   // reused by both min and max

    __m128 min128 = _mm_min_ps(xlow, xhigh);
    __m128 shuf = _mm_permute_ps(min128, _MM_SHUFFLE(2,3, 0,1)); // swap pairs
    min128 = _mm_min_ps(min128, shuf);
    shuf = _mm_permute_ps(min128, _MM_SHUFFLE(1,0, 3,2));        // swap 64-bit halves
    min128 = _mm_min_ps(min128, shuf);

    __m128 max128 = _mm_max_ps(xlow, xhigh);
    shuf   = _mm_permute_ps(max128, _MM_SHUFFLE(2,3, 0,1));  // swap pairs
    max128 = _mm_max_ps(max128, shuf);
    shuf   = _mm_permute_ps(max128, _MM_SHUFFLE(1,0, 3,2));  // swap 64-bit halves
    max128 = _mm_max_ps(max128, shuf);                      // all 4 elements hold the max

    __m256 min = _mm256_set_m128(min128, min128);   // vinsertf128
    __m128 range128 = _mm_sub_ps(max128, min128);   // This subtraction can be done before widening
    __m256 range = _mm256_set_m128(range128, range128);

    return _mm256_div_ps(_mm256_sub_ps(x, min), range);
}

全程256位

请注意,两个版本都使用相同的_MM_SHUFFLE常量在128位通道内进行混洗。这不是意外;这两种方法都希望所有元素的最小值或最大值。

__m256 normalisation_all256(__m256 x)
{
    __m256 xswapped = _mm256_permute2f128_ps(x,x,1);  // swap 128-bit halves

    __m256 min = _mm256_min_ps(x, xswapped);                  // low and high lanes are now the same
    __m256 shuf = _mm256_permute_ps(min, _MM_SHUFFLE(2,3, 0,1));  // swap pairs
    min  = _mm256_min_ps(min, shuf);
    shuf = _mm256_permute_ps(min, _MM_SHUFFLE(1,0, 3,2));         // swap 64-bit halves within lanes
    min  = _mm256_min_ps(min, shuf);     // all 8 elements have seen every other

    __m256 max = _mm256_max_ps(x, xswapped);
    shuf   = _mm256_permute_ps(max, _MM_SHUFFLE(2,3, 0,1));  // swap pairs
    max    = _mm256_max_ps(max, shuf);
    shuf   = _mm256_permute_ps(max, _MM_SHUFFLE(1,0, 3,2)); // swap 64-bit halves
    max    = _mm256_max_ps(max, shuf);                      // all 4 elements hold the max

    __m256 range = _mm256_sub_ps(max, min);
    return _mm256_div_ps(_mm256_sub_ps(x, min), range);
}

这两个版本都在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数学),这可能是一件好事,这些工作独立于这个工作,如果它不占用周期,可能会重叠。

cgh8pdjw

cgh8pdjw2#

使用[(e-min)/(max-min) for e in array]规范化数组
关于m128

__m256 normalisation(__m256 x) {
    //  Normaliser x

    __m128 vlow  = _mm256_castps256_ps128(x);   // low 128
    __m128 vhigh = _mm256_extractf128_ps(x, 1); // high 128

    __m128 tmp0 = _mm_max_ps(vlow, vhigh);          // 4 elements to test
    __m128 tmp1 = _mm_permute_ps(tmp0, 0b00001011); // = {d,c,a,a}
    tmp0 = _mm_max_ps(tmp0, tmp1);              //comparing a/d and b/c
    tmp1 = _mm_permute_ps(tmp0, 0b00000001);    // = {b,a,a,a}
    tmp0 = _mm_max_ps(tmp0, tmp1);              //comparing a/b
    tmp0 = _mm_permute_ps(tmp0, 0b00000000);
    __m256 _max = _mm256_set_m128(tmp0, tmp0);

    tmp0 = _mm_min_ps(vlow, vhigh);             // 4 elements to test
    tmp1 = _mm_permute_ps(tmp0, 0b11100000);    // = {d,c,a,a}
    tmp0 = _mm_min_ps(tmp0, tmp1);              //comparing a/d and b/c
    tmp1 = _mm_permute_ps(tmp0, 0b01000000);    // = {b,a,a,a}
    tmp0 = _mm_min_ps(tmp0, tmp1);              //comparing a/b
    tmp0 = _mm_permute_ps(tmp0, 0b00000000);
    __m256 _min = _mm256_set_m128(tmp0, tmp0);

    return _mm256_div_ps(_mm256_sub_ps(x, _min), _mm256_sub_ps(_max, _min));
}

只有m256

__m256 normalisation(__m256 x) {
    //  Normaliser x

    __m256 permute = _mm256_permute2f128_ps(x,x,1);
    
    __m256 _max = _mm256_max_ps(x, permute);
    _max = _mm256_max_ps(_max, _mm256_permute_ps(_max, 0b00001011));
    _max = _mm256_max_ps(_max, _mm256_permute_ps(_max, 0b00000001));
    _max = _mm256_permute2f128_ps(_max, _max, 0b00000000);
    
    __m256 _min = _mm256_min_ps(x, permute);
    _min = _mm256_min_ps(_min, _mm256_permute_ps(_min, 0b00001011));
    _min = _mm256_min_ps(_min, _mm256_permute_ps(_min, 0b00000001));
    _min = _mm256_permute2f128_ps(_min, _min, 0b00000000);
    
    return _mm256_div_ps(_mm256_sub_ps(x, _min), _mm256_sub_ps(_max, _min));
}
k3fezbri

k3fezbri3#

对于AVX1

__m128 vlow  = _mm256_castps256_ps128(x);
__m128 vhigh = _mm256_extractf128_ps(x, 1); // high 128
vlow  = _mm_max_ps(vlow, vhigh);            // 4 elements to test
vhigh = _mm_permute_ps(vlow, 0b00001011);   // = {d,c,a,a}
vlow = _mm_max_ps(vlow, vhigh);             //comparing a/d and b/c
vhigh = _mm_permute_ps(vlow, 0b000000001);  // = {b,a,a,a}
vlow = _mm_max_ps(vlow, vhigh);             //comparing a/b
vlow = _mm_permute_ps(vlow, 0b00000000);
__m256 _max = _mm256_set_m128(vlow, vlow);

如果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 _max = _mm256_max_ps(x, permute);
_max = _mm256_max_ps(_max, _mm256_permute_ps(_max, 0b00001011));
_max = _mm256_max_ps(_max, _mm256_permute_ps(_max, 0b00000001));
_max = _mm256_permute2f128_ps(_max, _max, 0b00000000);

在我的规模,它有相同的表现,也许m256一个更稳定(和一点点,但更快)。

相关问题