首页 > 解决方案 > 说“内在”只是对编译器的建议是否正确?

问题描述

我对内在函数、simd 和一般的低级编程真的很陌生。我正在迈出第一步,但就我所见,我正在使用的所有内在函数(现在是英特尔的)只是 C++ 通用代码,没有“特殊”或专用关键字。

这些“函数列表”和编译器之间似乎达成了某种协议,例如告诉编译器如果我使用这样的东西:

__m128d vA = _mm_load_pd(a);

应该vA变量视为XMM寄存器,而不是将其分配到内存中。但它不能保证(因为__m128d它最终是一个 C++ 联合/结构对象,它可以驻留在内存中)。

我对吗?或者引擎盖下还有更多的黑魔法?

Compiler 如何“以某种方式”处理这些函数而不是泛型函数?解析代码匹配的规则?像这样的东西?

对于 Web 开发人员来说,它非常迷人 :)

标签: c++simdintrinsics

解决方案


你真的在问两个不同的问题:

(1) 编译器如何决定将我的 SIMD 变量放在哪里?在内存中还是在寄存器中?

(2) 内在的“契约”有多具体?它总是发出特定的指令吗?

对于 SIMD 而言,第一个问题的答案实际上与任何其他类型的变量没有什么不同。在 C/C++ 中,您通常使用自动变量,因为它们最有可能最终出现在寄存器中。编译器可以根据上下文自由地安排实际指令和寄存器使用,并且通常会根据代码中的“寄存器压力”多少将数据移入和移出寄存器到“堆栈内存”。

与在汇编中编写它相比,这种灵活性是一件“好事”,程序员可以准确地决定何时使用哪些寄存器以及执行指令的确切顺序。通常编译器可以混合其他附近的代码或进行其他优化很难保持直线,并且可以利用架构差异。例如,在DirectXMath中,我为 x86(32 位)和 x64(64 位)编写了相同的内部代码,编译器可以利用 x64 中可用的 8 个额外寄存器。如果我使用的是内联汇编,我将不得不以两种不同的方式来编写它,而且可能不止于此,我很快就会谈到一些额外的差异。

在编写 SIMD 代码时,您确实希望最大限度地利用已经在寄存器中的数据,因为内存的加载/存储开销通常会花费与执行一些 SIMD 指令相比标量所获得的性能一样多的性能。因此,您通常会编写 SIMD 内在函数来显式加载到一堆“自动变量”中,但请记住,一次可能只有 8 个左右真正在寄存器中。您确实想做足够多的工作以使编译器可以填补空白。然后将结果存储到内存中。因此,你真的不做类似auto a = new __m128d;. 还有隐含对齐的额外复杂性(__m128d必须是 16 字节对齐的,而 x64new做到了 x86new没有)。

第二个答案有点复杂。给定的内在函数通常被定义为给定的指令,并且一些内在函数实际上是指令的组合,但是编译器在选择确切的指令时可能会选择使用目标平台的一些知识。这里有一些例子:

  • __m128 _mm_add_ps (__m128 a, __m128 b)被定义为 SSE 指令addps,并且经常这样发出。但是,如果您正在构建,/arch:AVX或者/arch:AVX2编译器将使用VEX 前缀和指令vaddps

  • __m128d _mm_fmadd_pd (__m128d a, __m128d b, __m128d c)被定义为 FMA3 指令,但编译器实际上可以发出vfmadd132pd, vfmadd213pd, 或vfmadd231pd取决于确切的寄存器使用。事实上,编译器甚至可以决定使用 avmulpd后跟 a更快,vaddpd这取决于它使用的硬件指令成本函数的确切指令时序。

请注意,虽然编译器实现者当然可以决定说他们可以优化__m128 _mm_shuffle_ps (__m128 a, __m128 b, unsigned int imm8)寄存器 a 和 b 相同的位置,并且如果您vpermilps使用. 那将与内在“契约”。然而,在实践中,内在函数往往被视为有点特殊,并且非常喜欢它们定义的指令,因为您经常在基于硬件特征检测的特定上下文中使用它们。因此,您通常可以依靠特定的内在函数最终成为您期望的指令或它的非常接近的变体。shufps/arch:AVX

所以简而言之,所有 C/C++都是对编译器的“提示”,因为源代码描述了您想要的确切计算,但编译器可以自由地实际发出实现相同结果但可以不同的代码订购或使用与您可能假设的不同的说明。

英特尔内部函数指南是探索内部函数的好资源。

您可能还会发现我的一些与内在函数相关的博客文章很有用。

DirectXMath Programmer's Guide也有一些有用的技巧和提示,用于内部函数的使用,所以值得一读,它只有 6 页,所以不会花那么长时间。请参阅Microsoft 文档


推荐阅读