了解SSE的内部函数如何使用内存

Understanding how the instrinsic functions for SSE use memory

本文关键字:何使用 内存 内部函数 SSE 了解      更新时间:2023-10-16

在我问问题之前,只需要一些背景信息。

在C语言中,当您分配给一个变量时,您可以从概念上假设您只修改了RAM中的一小块内存。

int a = rand(); //conceptually, you created and assigned variable A in ram

在汇编语言中,要做同样的事情,您基本上需要存储在寄存器中的rand()的结果,以及指向"a"的指针。然后,您将执行存储指令,将寄存器内容获取到ram中。

例如,当您用C++编程时,当您分配和操作值类型对象时,您通常甚至不必考虑它们的地址,也不必考虑如何或何时将它们存储在寄存器中。

使用SSE指令很奇怪,因为就概念内存模型而言,它们似乎介于C语言编码和汇编之间。

您可以调用加载/存储函数,它们返回对象。像_mm_add这样的数学运算会返回一个对象,但我不清楚结果是否会实际存储在对象中,除非您调用_mm_store。

考虑以下示例:

inline void block(float* y, const float* x) const {
// load 4 data elements at a time
__m128 X = _mm_loadu_ps(x);
__m128 Y = _mm_loadu_ps(y);
// do the computations
__m128 result = _mm_add_ps(Y, _mm_mul_ps(X, _mm_set1_ps(a)));
// store the results
_mm_storeu_ps(y, result);

}

这里有很多临时物品。临时对象实际上不存在吗?以类似C的方式调用汇编指令,这只是语法糖吗?如果不是在最后执行store命令,而是保留结果,那么结果会不仅仅是语法糖,而且实际上会保存数据吗?

TL:DR在使用SSE指令集时,我应该如何看待内存?

__m128变量可能在寄存器和/或内存中。它与简单的floatint变量非常相似——编译器将决定哪些变量属于寄存器,哪些必须存储在内存中。通常,编译器会尝试将"最热"的变量保存在寄存器中,其余的则保存在内存中。它还将分析变量的寿命,以便寄存器可以用于块中的多个变量。作为一名程序员,你不需要太担心这一点,但你应该知道你有多少寄存器,即在32位模式下有8个XMM寄存器,在64位模式下为16个。将变量使用量保持在这些数字以下将有助于尽可能地将所有内容保存在寄存器中。话虽如此,访问一级缓存中的操作数的惩罚并不是比访问寄存器操作数大得多,所以如果很难做到这一点,你不应该太在意将所有内容都保留在寄存器中

脚注:在使用内部函数时,这种关于SSE变量是在寄存器中还是在内存中的模糊性实际上很有帮助,并且使编写优化代码比使用原始汇编程序更容易——编译器完成了跟踪寄存器分配和其他优化的繁重工作,使您能够集中精力使代码正确工作。

向量变量并不特殊。如果编译器在优化循环时(或在对编译器无法"看到"的函数的函数调用中)用完寄存器,它们将溢出到内存中,并在以后需要时重新加载。

gcc -O0实际上倾向于在设置它们时存储到RAM中,而不是将__m128i变量仅保留在寄存器IIRC中。

可以在不使用任何加载或存储内部代码的情况下编写所有内部代码,但随后将由编译器决定如何以及何时移动数据。(事实上,在某种程度上,这些天你仍然是这样,这要归功于编译器善于优化内部函数,而不仅仅是在你使用内部函数的地方抛出负载。)

如果不需要将值作为其他值的输入,编译器将把负载折叠到内存操作数中,以执行以下指令。但是,只有当数据位于已知的对齐地址,或者使用了对齐的内部加载时,这才是安全的。

我目前认为加载内部函数的方式是将对齐保证(或缺少对齐保证)传达给编译器如果与未对齐的128b内存操作数一起使用,矢量指令的"常规"SSE(非AVX/非VEX编码)版本会出错。(即使在支持AVX、FWIW的CPU上。)例如,请注意,即使punpckl*将其内存操作数列为m128,因此也有对齐要求,即使它实际上只读取低位64b。pmovzx将其操作数列为m128

无论如何,使用load而不是loadu告诉编译器,它可以将加载折叠为另一条指令的内存操作数,即使它无法证明它来自对齐的地址。

为AVX目标机器编译将允许编译器将甚至未对齐的负载折叠到其他操作中,以利用uop微融合。

这出现在"如何使用_mm_mul_ps指定对齐"的评论中。

store内部函数显然有两个目的:

  1. 告诉编译器应该使用对齐的还是未对齐的asm指令
  2. 删除从__m128ddouble *的强制转换的需要(不适用于整数情况)

只是为了混淆,AVX2引入了_mm256_storeu2_m128i (__m128i* hiaddr, __m128i* loaddr, __m256i a)之类的东西,它将高/低半部分存储到不同的地址。它可能编译成vmovdqu / vextracti128 ..., 1序列。顺便说一句,我猜他们在制作vextracti128时考虑到了AVX512,因为使用0作为立即数与vmovdqu相同,但编码速度较慢,时间较长。