我对内在函数,simd和一般的底层编程真的很陌生。 我正在迈出第一步,但是对于我所看到的,我正在使用的所有内在函数(Intel ones现在)都是C ++通用代码,没有任何“特殊”或专用关键字。
这些“函数列表”与编译器之间似乎达成了某种协议,例如告诉编译器是否使用了类似的东西:
__m128d vA = _mm_load_pd(a);
应该将vA
变量视为XMM
寄存器,而不是将其分配到内存中。但这并不能保证(由于__m128d
,最后是一个C ++联合/结构对象,该对象可以驻留在内存中。)
我是对的吗?或者引擎盖下还有其他黑魔法?
以及编译器如何“以某种方式”而不是通用函数来处理这些函数?通过解析规则匹配代码?这样的东西?
这对于Web开发人员来说非常有趣:)
答案 0 :(得分:3)
您实际上是在问两个不同的问题:
(1)编译器如何确定将SIMD变量放在何处?在内存还是寄存器中?
(2)内在的“合同”有多具体?它总是发出特定的指令吗?
对于第一个问题,对于SIMD而言,答案实际上与对任何其他类型的变量没有什么不同。在C / C ++中,通常使用automatic variables,因为它们最有可能在寄存器中结束。编译器可以根据上下文自由安排实际的指令和寄存器使用情况,并且通常会根据代码中有多少“寄存器压力”将数据移入或移出寄存器到“堆栈存储器”。
与在汇编中进行编写相比,这种灵活性是“一件好事”,在汇编程序中,程序员可以决定何时确切地使用什么寄存器以及执行指令的确切顺序。通常,编译器可以混入其他附近的代码或执行其他难以保持直截了当的优化,并且可以利用架构差异。例如,在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 prefix和指令vaddps
。
__m128d _mm_fmadd_pd (__m128d a, __m128d b, __m128d c)
被定义为FMA3指令,但是编译器实际上可以根据确切的寄存器用途发出vfmadd132pd
,vfmadd213pd
或vfmadd231pd
。实际上,编译器甚至可以决定使用vmulpd
和后面的vaddpd
来更快,这取决于所使用的硬件指令成本函数的确切指令时序。 p>
请注意,尽管编译器实施者当然可以决定说他们可以优化
__m128 _mm_shuffle_ps (__m128 a, __m128 b, unsigned int imm8)
,其中寄存器a和b相同,然后选择发出vpermilps
而不是{{1 }}(如果您使用shufps
构建)。那将是与内在的“契约”。但是,实际上,内在函数往往会被视为特殊的事物,并且强烈希望将内在函数定义为指令,因为您经常基于硬件特征检测在特定上下文中使用它们。因此,通常您可以指望某个特定的内部指令最终成为您期望的指令或它的非常接近的变体。
因此,简而言之,从源代码描述您想要的确切计算的意义上来说,所有C / C ++ 都是编译器的“提示”,但是编译器可以自由地实际发出代码可以达到相同的结果,但顺序可能不同,或使用的指令可能与您假设的指令不同。
Intel Intrinsics Guide是探索内在函数的好资源。
您可能还会发现我的一些与内部函数有关的blog posts有用。
DirectXMath程序员指南中还散布了一些有用的内在用法技巧,因此值得一读,它只有6页,因此不会花那么长时间。参见Microsoft Docs