我需要用另一个SIMD寄存器的一个元素填充SIMD寄存器。即“广播”或“散布”单个元素到每个位置。

我当前执行此操作的代码是(简化后,我的真实函数声明为inline):

__m128
f4_broadcast_1(__m128 a, int i) {
    return _mm_set1_ps(a[i]);
}


这似乎可以在clang和gcc上生成有效的代码,但是msvc禁止索引访问。因此,我改为:

__m128
f4_broadcast_2(__m128 a, int i) {
    union { __m128 reg; float f[4]; } r = { .reg = a };
    return _mm_set1_ps(r.f[i]);
}


它在clang和gcc上生成相同的代码,但在msvc上生成错误的代码。 Godbolt链接:https://godbolt.org/z/IlOqZl

有更好的方法吗?我知道在SO上已经存在类似的问题,但是我的用例涉及从寄存器中提取float32并将其放回另一个寄存器中,这是一个稍有不同的问题。如果您完全不必接触主存储器就可以这样做,那就太酷了。

索引是变量还是常量?显然,是否对SIMD性能至关重要。就我而言,索引是一个循环变量:

for (int i = 0; i < M; i++) {
    ... broadcast element i of some reg
}


其中M是4、8或16。也许我应该手动展开循环以使其恒定? for循环中有很多代码,因此代码量将大大增加。

我也想知道如何针对现代cpu:s上的__m256__m512寄存器执行相同的操作。

最佳答案

Get an arbitrary float from a simd register at runtime?中的某些混洗可以调整为广播一个元素,而不是仅复制一个副本到低层元素。它更详细地讨论了改组与存储/重新加载策略之间的权衡。



在AVX vpermilps和AVX2车道交叉线vpermps / vpermd之前,x86没有32位元素的变量控制改组。例如

// for runtime-variable i.  Otherwise use something more efficient.
_mm_permutevar_ps(v, _mm_set1_epi32(i));


或用vbroadcastss播放低位元素(矢量源版本需要AVX2)

广播加载对于AVX1非常有效:_mm_broadcast_ss(float*)(或相同的_mm256/512)或恰好来自内存的浮点数的128/256/512 _mm_set1_ps(float),让编译器在编译时使用广播加载启用了AVX1。



使用编译时常数控件,您可以使用SSE1广播任何单个元素
_mm_shuffle_ps(same,same, _MM_SHUFFLE(i,i,i,i));

或对于整数,使用SSE2 pshufd_mm_shuffle_epi32(v, _MM_SHUFFLE(i,i,i,i))

根据您的编译器的不同,i可能必须是宏才能成为禁用优化的编译时常量。改组控制常数必须编译成嵌入在机器代码中的立即字节(具有4个2位字段),而不是作为数据或从寄存器加载。



循环遍历元素。

我在本节中使用AVX2。这很容易适应AVX512。如果没有AVX2,则存储/重载策略是256位向量或vpermilps 128位向量的唯一不错选择。

如果没有AVX,SSSE3 pshufb的计数器(可能在__m128i__m128之间进行强制转换)可能会增加4个计数器(如果没有有效的广播负载,则可能是个好主意)。


索引是一个循环变量


编译器通常会为您完全展开循环,将循环变量转换为每次迭代的编译时常量。但仅启用优化。在C ++中,您可以使用模板递归来迭代constexpr

MSVC不会优化内在函数,因此,如果您编写_mm_permutevar_ps(v, _mm_set1_epi32(i));,则实际上是在每次迭代中得到的,而不是4x vshufps。但是gcc尤其是clang确实优化了shuffle,因此它们应该在启用优化的情况下做得很好。


for循环中有很多代码


如果将需要大量寄存器/花费大量时间,则存储/重新加载可能是一个不错的选择,尤其是对于可用于广播重新加载的AVX。与当前Intel CPU上的负载吞吐率(2 /时钟)相比,随机吞吐率(1 /时钟)受到的限制更大。

使用AVX512编译代码甚至将允许广播内存源操作数,而不是单独的加载指令,因此,如果只需要一次,则编译器甚至可以将广播加载折叠到源操作数中。

/*********   Store/reload strategy ****************/
#include <stdalign.h>

void foo(__m256 v) {
   alignas(32)  float tmp[8];
   _mm256_store_ps(tmp, v);

   // with only AVX1, maybe don't peel first iteration, or broadcast manually in 2 steps
   __m256 bcast = _mm256_broadcastss_ps(_mm256_castps256_ps128(v));  // AVX2 vbroadcastss ymm, xmm
    ... do stuff with bcast ...

    for (int i=1; i<8 ; i++) {
        bcast = _mm256_broadcast_ss(tmp[i]);
        ... do stuff with bcast ...
    }
}


我手动剥离了第一个迭代,以通过ALU操作(较低的延迟)仅广播低端元素,因此可以立即开始使用。然后,随后的迭代将重新加载广播负载。

如果您有AVX2,另一种选择是将SIMD增量用于矢量随机播放控制(也称为掩码)。

// Also AVX2
void foo(__m256 v) {

   __m256i shufmask = _mm256_setzero_si256();

    for (int i=1; i<8 ; i++) {
        __m256 bcast = _mm256_permutevar8x32_ps(v, shufmask);    // AVX2 vpermps
        // prep for next iteration by incrementing the element selectors
        shufmask = _mm256_add_epi32(shufmask, _mm256_set1_epi32(1));

        ... do stuff with bcast ...

    }
}


这在shufmask上执行了一个冗余vpaddd(在最后一次迭代中),但是这可能比剥掉第一次或最后一次迭代更好并且更好。并且显然比以-1开头并在第一次迭代中的洗牌之前进行添加更好。

穿越车道的改组在Intel上具有3个周期的延迟,因此除非有其他不依赖bcast的迭代工作,否则将改组摆在改组之后可能是好的调度。无论如何,无序的exec都会使这成为一个小问题。在第一次迭代中,vpermps的掩码被异或为零的情况基本上与Intel上的vbroadcastss一样好,以便乱序的exec能够快速上手。

但是在AMD CPU(至少在Zen2之前)上,跨越道vpermps相当慢。粒度小于128位的行车道混洗更加昂贵,因为它必须解码为128位的指令。因此,这种策略在AMD上并不出色。如果存储/重新加载对您在Intel上的周围代码表现相同,那么使您的代码也对AMD友好可能是一个更好的选择。

vpermps还具有AVX512内部函数引入的新内部函数:_mm256_permutexvar_ps(__m256i idx, __m256 a)具有与asm匹配的操作数。如果您的编译器支持新的编译器,请使用任意一个。

10-06 06:35
查看更多