【问题标题】:How to get data out of AVX registers?如何从 AVX 寄存器中获取数据?
【发布时间】:2016-10-03 09:45:28
【问题描述】:

使用 MSVC 2013 和 AVX 1,我在一个寄存器中有 8 个浮点数:

__m256 foo = mm256_fmadd_ps(a,b,c);

现在我想为所有 8 个浮点数调用 inline void print(float) {...}。看起来 Intel AVX intrisics 会使这变得相当复杂:

print(_castu32_f32(_mm256_extract_epi32(foo, 0)));
print(_castu32_f32(_mm256_extract_epi32(foo, 1)));
print(_castu32_f32(_mm256_extract_epi32(foo, 2)));
// ...

但是 MSVC 甚至没有这两个内在函数中的任何一个。当然,我可以将值写回内存并从那里加载,但我怀疑在汇编级别没有必要溢出寄存器。

Bonus Q:我当然想写

for(int i = 0; i !=8; ++i) 
    print(_castu32_f32(_mm256_extract_epi32(foo, i)))

但 MSVC 不理解许多内在函数 需要 循环展开。如何在__m256 foo 中的 8x32 浮点数上编写循环?

【问题讨论】:

  • 如果您要打印数据,那么将寄存器溢出到内存几乎无关紧要 - 只需使用合适的联合。
  • @PaulR:简化示例。
  • 重要的是print() 是代表一个真正可以完全内联的函数,还是编译器最终必须call 一个它看不到代码的函数。到底发生了什么?
  • 如果你只关心 MSVC,像 foo.m256_f32[i] 这样的东西可能会起作用(即 foo[i] 与 gcc)。
  • @PeterCordes:我有 Paul 想法的递归模板代码。无需展开;只是内联 7 级辅助函数。非常简单——一次调用print,一次递归调用。对于内联来说应该是微不足道的,这意味着我最终会连续进行 8 次调用,就像展开循环一样。不过还是要检查组装。

标签: c++ visual-c++ avx fma


【解决方案1】:

假设您只有 AVX(即没有 AVX2),那么您可以这样做:

float extract_float(const __m128 v, const int i)
{
    float x;
    _MM_EXTRACT_FLOAT(x, v, i);
    return x;
}

void print(const __m128 v)
{
    print(extract_float(v, 0));
    print(extract_float(v, 1));
    print(extract_float(v, 2));
    print(extract_float(v, 3));
}

void print(const __m256 v)
{
    print(_mm256_extractf128_ps(v, 0));
    print(_mm256_extractf128_ps(v, 1));
}

但我想我可能只使用联合:

union U256f {
    __m256 v;
    float a[8];
};

void print(const __m256 v)
{
    const U256f u = { v };

    for (int i = 0; i < 8; ++i)
        print(u.a[i]);
}

【讨论】:

  • 他的print() 函数接受一个浮点参数。 extract_ps 提取到内存或整数寄存器。 float shuffle(如 shufps)是一个更好的选择(然后_mm_cvtss_f32 将向量转换为其标量低元素)。如果要使用 SSE4.1 指令,请使用_mm_insert_ps,它可以选择任何源元素并将其放入任何目标元素中,也可以将目标中的指定元素归零。 (SysV ABI 允许在用于传递标量的 reg 的上部 xmm 元素中出现垃圾,因此您不需要归零。我假设 Windows 是相同的。)
  • 显然有一个wrapper macro called _MM_EXTRACT_FLOAT。您将它用作_MM_EXTRACT_FLOAT(dest_float, src_m128, element_index),所以这很奇怪(它不会计算为表达式,除非作为 GNU C 语句表达式)。当在寄存器中已经存在的向量上使用时,g++ 将其编译为 shufps,或者在对向量的引用具有偏移量的 movss 上编译它。 gcc 在/usr/lib/gcc/x86_64-linux-gnu/5.2.1/include/smmintrin.h 中根据__builtin_ia32_vec_ext_v4sf 定义它,而不是特定的英特尔内在函数。所以,是的,我猜这里是不错的选择。
  • 另外,它仅适用于-msse4.1 或更高版本!这太疯狂了,因为除了 SSE1 之外不需要任何东西来实现它。
  • @MSalters:没错,AVX 保证所有以前的英特尔 SSE 扩展(并提供所有这些扩展的 VEX 编码非破坏性目标版本)。
  • @PeterCordes:现在已修复答案 - _MM_EXTRACT_FLOAT 方法相当笨拙 - 我想我现在更喜欢联合方法。
【解决方案2】:

小心:_mm256_fmadd_ps 不是 AVX1 的一部分。 FMA3 有自己的功能位,并且仅在 Intel 和 Haswell 上引入。 AMD 推出了带 Piledriver 的 FMA3(AVX1+FMA4+FMA3,没有 AVX2)。


在 asm 级别,如果要将 8 个 32 位元素放入整数寄存器中,实际上存储到堆栈然后进行标量加载会更快。 pextrd 是 SnB 系列和 Bulldozer 系列的 2-uop 指令。 (以及不支持 AVX 的 Nehalem 和 Silvermont)。

vextractf128 + 2xmovd + 6xpextrd 不可怕的唯一 CPU 是 AMD Jaguar。 (便宜的pextrd,而且只有一个装载口。)(见Agner Fog's insn tables

宽对齐的存储可以转发到重叠的窄负载。 (当然,您可以使用movd 来获取低元素,因此您可以混合使用加载端口和ALU 端口)。


当然,您似乎是通过使用整数提取然后将其转换回浮点数来提取floats。这看起来很可怕。

您真正需要的是每个 float 在其自己的 xmm 寄存器的低元素中。 vextractf128 显然是开始的方式,将元素 4 带到新 xmm reg 的底部。那么6x AVXshufps就可以轻松得到每一半的其他三个元素。 (或者movshdupmovhlps 的编码更短:没有立即字节)。

7 shuffle uop 与 1 个 store 和 7 个 load uop 相比值得考虑,但如果您要为函数调用溢出向量,则不值得考虑。


ABI 注意事项:

您在 Windows 上,其中 xmm6-15 被调用保留(只有 low128;ymm6-15 的上半部分被调用破坏)。这也是以vextractf128 开头的另一个原因。

在 SysV ABI 中,所有 xmm / ymm / zmm 寄存器都被调用破坏,因此每个print() 函数都需要溢出/重新加载。唯一明智的做法是将存储到内存并使用原始向量调用print(即打印低元素,因为它将忽略寄存器的其余部分)。然后movss xmm0, [rsp+4] 并在第二个元素上调用print,等等。

将所有 8 个浮点数很好地解包到 8 个向量寄存器中对你没有好处,因为在第一次函数调用之前它们都必须单独溢出!

【讨论】:

  • 我想当你说“将元素 4 带到新 XMM 寄存器的底部”时,它还包括将元素 5-7 移动到同一个寄存器?因为 AVX 车道概念似乎暗示我需要让所有 4 个高元素都进入低车道。 (或者更现实地说,一个 YMM 寄存器只是一对 XMM 寄存器,我需要将一个高位寄存器重命名为一个低位寄存器)。您的程序集与 Paul R 的内在函数基本相同,我说得对吗? vextractf12_mm256_extractf128_ps(v, 1);六个shufps_mm_extract_ps(v, 1)3
  • @MSalters: vextractf128 (_mm256_extractf128_ps) 顾名思义。是的,您可以将其 128b 结果洗牌以获得元素 5-7。我只是指出元素 4 已经存在的额外功能,所以你不需要再洗牌了。使用 AVX2,您可以使用 7x vpermps 依次获取每个元素,或者在通道内 256b 随机播放,然后 vextractf128 上 128 的低元素,但这些选项都更糟。
  • 但是_mm_extract_ps 不是 shufps。这是extractps 的内在函数,结果是int。你可以search on instruction names in Intel's intrinsics guide.。如果幸运的话,编译器可能会将reinterpret_cast&lt;float&gt;(_mm_extract_ps(v, 1)) 优化为shufpsvpermilps,但它可能会发出extractps / movd。在保罗的回答中在 cmets 中讨论的 _MM_EXTRACT_FLOAT 宏可能很方便。
  • @MSalters:顺便说一句,我不喜欢英特尔的固有名称。它们的打字时间太长了,而且不如 asm 助记符那么令人难忘。尤其是必须一直输入_mm256__m256 是荒谬的,epi32epi8 本来可以更紧凑。但即使除了那种噪音,我也不喜欢他们选择的实际名字。它们与 asm 助记符不同,例如 vpermilps 的内在函数只是 _mm_permute_ps_mm_permutevar_ps。很难记住是shufpsvpermilps,还是AVX2跨车道vpermps
  • Agner Fog 的矢量类库非常好,gcc 将__m256 定义为 gcc 本机矢量类型,因此您可以对矢量类型执行简单的操作,例如 a + b(无论如何对于浮点数。对于整数,您需要 Agner 的 VCL 或其他东西来选择正确的元素大小)。
【解决方案3】:
    float valueAVX(__m256 a, int i){

        float ret = 0;
        switch (i){

            case 0:
//                 a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 0)      ( a3, a2, a1, a0 )
// cvtss_f32             a0 

                ret = _mm_cvtss_f32(_mm256_extractf128_ps(a, 0));
                break;
            case 1: {
//                     a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 0)     lo = ( a3, a2, a1, a0 )
// shuffle(lo, lo, 1)      ( - , a3, a2, a1 )
// cvtss_f32                 a1 
                __m128 lo = _mm256_extractf128_ps(a, 0);
                ret = _mm_cvtss_f32(_mm_shuffle_ps(lo, lo, 1));
            }
                break;
            case 2: {
//                   a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 0)   lo = ( a3, a2, a1, a0 )
// movehl(lo, lo)        ( - , - , a3, a2 )
// cvtss_f32               a2 
                __m128 lo = _mm256_extractf128_ps(a, 0);
                ret = _mm_cvtss_f32(_mm_movehl_ps(lo, lo));
            }
                break;
            case 3: {
//                   a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 0)   lo = ( a3, a2, a1, a0 )
// shuffle(lo, lo, 3)    ( - , - , - , a3 )
// cvtss_f32               a3 
                __m128 lo = _mm256_extractf128_ps(a, 0);                    
                ret = _mm_cvtss_f32(_mm_shuffle_ps(lo, lo, 3));
            }
                break;

            case 4:
//                 a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 1)      ( a7, a6, a5, a4 )
// cvtss_f32             a4 
                ret = _mm_cvtss_f32(_mm256_extractf128_ps(a, 1));
                break;
            case 5: {
//                     a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 1)     hi = ( a7, a6, a5, a4 )
// shuffle(hi, hi, 1)      ( - , a7, a6, a5 )
// cvtss_f32                 a5 
                __m128 hi = _mm256_extractf128_ps(a, 1);
                ret = _mm_cvtss_f32(_mm_shuffle_ps(hi, hi, 1));
            }
                break;
            case 6: {
//                   a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 1)   hi = ( a7, a6, a5, a4 )
// movehl(hi, hi)        ( - , - , a7, a6 )
// cvtss_f32               a6 
                __m128 hi = _mm256_extractf128_ps(a, 1);
                ret = _mm_cvtss_f32(_mm_movehl_ps(hi, hi));
            }
                break;
            case 7: {
//                   a = ( a7, a6, a5, a4, a3, a2, a1, a0 )
// extractf(a, 1)   hi = ( a7, a6, a5, a4 )
// shuffle(hi, hi, 3)    ( - , - , - , a7 )
// cvtss_f32               a7 
                __m128 hi = _mm256_extractf128_ps(a, 1);
                ret = _mm_cvtss_f32(_mm_shuffle_ps(hi, hi, 3));
            }
                break;
        }

        return ret;
    }

【讨论】:

  • 对于案例 3 和 7,您应该使用 _mm_shuffle_ps(tw, tw, 3) 而不是使用两次随机播放。 (vshufpsvmovhlps + vshufps 快)。 movehl_ps 对情况 2 和 6 有好处,但是:节省 1 字节的代码大小,因为它不需要立即数。如果您有 AVX2,则案例 6 可以通过 vpermpd ymm, ymm, imm8 立即随机播放来完成。如果您在寄存器中有一个随机播放控制向量,则可以使用vpermps lane-crossing shuffle 来完成案例 5 和 7。
  • 虽然此代码 sn-p 可能是解决方案,但 including an explanation 确实有助于提高您的帖子质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。
【解决方案4】:

(未完成的答案。无论如何发布以防万一它对任何人有帮助,或者如果我回到它。通常,如果您需要与无法矢量化的标量接口,那么只存储一个矢量也不错到本地数组,然后一次重新加载一个元素。)

有关 asm 详细信息,请参阅我的其他答案。这个答案是关于 C++ 方面的。


void foo(__m256 v) {
    alignas(32) float vecbuf[8];   // 32-byte aligned array allows aligned store
                                   // avoiding the risk of cache-line splits
    _mm256_store_ps(vecbuf, v);

    float v0 = _mm_cvtss_f32(_mm256_castps256_ps128(v));  // the bottom of the register
    float v1 = vecbuf[1];
    float v2 = vecbuf[2];
    ...
   // or loop over vecbuf[i]
   // if you do need all 8 elements one at a time, this is a good way
}

或循环遍历vecbuf[i]。向量存储可以转发到其元素之一的标量重新加载,因此这只引入了大约 6 个延迟周期,并且可以同时进行多个重新加载。 (因此它非常适合具有 2 个时钟负载吞吐量的现代 CPU 的吞吐量。)

请注意,我避免重新加载低元素;寄存器中向量的低元素已经一个标量float_mm_cvtss_f32( _mm256_castps256_ps128(v) ) 只是让编译器的类型系统满意的方法;它编译为零 asm 指令,因此它实际上是免费的(除非错过优化错误)。 (见Intel's intrinsics guide)。 XMM 寄存器是相应 YMM 寄存器的低 128 位,标量 float / double 是 XMM 寄存器的低 32 或 64 位。 (上半部分的垃圾无所谓。)

铸造第一次让 OoO exec 在等待其余的到达时有一些事情要做。您可能会考虑改组以获得第二个元素,vunpckhpsvmovhlps 在低 128 上,这样您就可以快速准备好 2 个元素,如果这有助于填补延迟气泡。

在 GNU C/C++ 中,您可以使用 v[1] 甚至像 v[i] 这样的变量索引来索引数组等向量类型。编译器将在 shuffle 或 store/reload 之间进行选择。

但这不能移植到 MSVC,它根据与一些命名成员的联合来定义 __m256

存储到数组并重新加载是可移植的,编译器有时甚至可以将其优化为随机播放。(如果您不希望这样做,请检查生成的 asm。)

例如clang 优化了一个只返回 vecbuf[1] 到一个简单的 vshufps 的函数。 https://godbolt.org/z/tHJH_V


如果你真的想将一个向量的所有元素加到一个标量总数中,可以使用 shuffle 和 SIMD 相加Fastest way to do horizontal float vector sum on x86

(对于单个向量的元素的乘法、最小值、最大值或其他关联归约也是如此。当然,如果您有多个向量,请对一个向量进行垂直操作,例如 _mm256_add_ps(v1,v2)


使用Agner Fog's Vector Class Library,他的包装类重载operator[] 以完全按照您期望的方式工作,即使对于非常量参数也是如此。这通常编译为存储/重新加载,但它可以很容易地用 C++ 编写代码。启用优化后,您可能会得到不错的结果。 (除了低元素可能会被存储/重新加载,而不是仅仅被使用。所以你可能想要将vec[0] 特殊情况转换为_mm_cvtss_f32(vec) 或其他东西。)

(VCL 曾经在 GPL 下获得许可,但当前版本现在是简单的 Apache 许可。)

另请参阅我的 github repo,其中对 Agner 的 VCL 进行了大部分未经测试的更改,以便为某些功能生成更好的代码。


有一个_MM_EXTRACT_FLOAT wrapper macro,但它很奇怪,只在 SSE4.1 中定义。我认为它打算与 SSE4.1 extractps 一起使用(它可以将浮点数的二进制表示提取到整数寄存器中,或存储到内存中)。不过,当目标是float 时,gcc 确实会将其编译为 FP shuffle。如果您希望结果为float,请注意其他编译器不会将其编译为实际的extractps 指令,因为that's not what extractps 可以。 (这就是insertps does,但更简单的 FP shuffle 会占用更少的指令字节。例如,shufps 与 AVX 非常棒。)

这很奇怪,因为它需要 3 个参数:_MM_EXTRACT_FLOAT(dest, src_m128, idx),所以你甚至不能将它用作 float 本地的初始化器。


循环一个向量

gcc 会为您展开这样的循环,但仅限于 -O1 或更高版本。在-O0,它会给你一个错误信息。

float bad_hsum(__m128 & fv) {
    float sum = 0;
    for (int i=0 ; i<4 ; i++) {
        float f;
        _MM_EXTRACT_FLOAT(f, fv, i);  // works only with -O1 or higher
        sum += f;
    }
    return sum;
}

【讨论】:

  • 您是否知道类似 VCL 但未获得 copyleft 许可的东西?
  • @BeeOnRope:我认为 Chuck Walbourn 的 DirectXMath 类似:github.com/Microsoft/DirectXMath。但更强调矩阵数学和无法映射到单个指令的函数。
  • 也许这个推理是愚蠢的,但是根据这个 (godbolt.org/z/h4M94z),提取值(即 vecbuf[i])或不使用(即 v[i])将其存储到数组中会产生相同的 ASM 代码。尽管如此,我很可能遗漏了一些东西,或者我没有考虑索引 AVX 寄存器的其他副作用。在那种情况下,这种方法有什么影响?
猜你喜欢
  • 1970-01-01
  • 2017-03-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-22
  • 1970-01-01
  • 2020-12-26
相关资源
最近更新 更多