了解 SSE 的内在函数如何使用内存
Understanding how the instrinsic functions for SSE use memory
在我提问之前,先了解一下背景信息。
在 C 语言中,当你赋值给一个变量时,你可以在概念上假设你只是修改了 RAM 中的一小块内存。
int a = rand(); //conceptually, you created and assigned variable A in ram
在汇编语言中,要完成同样的事情,您基本上需要将 rand() 的结果存储在寄存器中,以及指向 "a" 的指针。然后,您将执行存储指令以将寄存器内容放入 ram。
例如,当您使用 C++ 编程时,当您分配和操作值类型对象时,您通常甚至不必考虑它们的地址或它们将如何或何时存储在寄存器中。
使用 SSE 内参很奇怪,因为就概念内存模型而言,它们似乎介于 C 和汇编代码之间。
您可以调用 load/store 个函数,它们 return 个对象。像 _mm_add 这样的数学运算将 return 一个对象,但我不清楚结果是否会实际存储在对象中,除非你调用 _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 的方式调用汇编指令的语法糖?如果你不在最后执行存储命令,而只是保留结果,会发生什么情况,结果会不会不仅仅是语法糖,而且会实际保存数据?
TL:DR 在使用 SSE 内部函数时我应该如何考虑内存?
__m128
变量可能在寄存器 and/or 内存中。它与简单的 float
或 int
变量非常相似——编译器将决定哪些变量属于寄存器,哪些必须存储在内存中。通常,编译器会尝试将 "hottest" 变量保存在寄存器中,其余的保存在内存中。它还将分析变量的生命周期,以便一个寄存器可以用于一个块中的多个变量。作为一名程序员,您不必太担心这一点,但您应该知道您有多少个寄存器,即 32 位模式下有 8 个 XMM 寄存器,64 位模式下有 16 个。将您的变量使用量保持在这些数字以下将有助于尽可能将所有内容保存在寄存器中。话虽如此,在 L1 缓存中访问操作数的代价并不 比访问寄存器操作数大得多,所以如果它的话,你不应该太执着于将所有内容都保存在寄存器中事实证明很难做到。
脚注:在使用内在函数时,关于 SSE 变量是在寄存器中还是在内存中的这种含糊不清实际上非常有帮助,并且使编写优化代码比使用原始汇编程序编写代码更容易 - 编译器会做一些繁重的工作来保持跟踪寄存器分配和其他优化,让您专注于使代码正常工作。
向量变量并不特殊。如果编译器在优化循环时用完了寄存器(或跨函数调用函数编译器无法 "see" 知道它没有,它们将被溢出到内存并在以后需要时重新加载t touch the vector regs).
gcc -O0
实际上确实倾向于在您设置它们时存储到 RAM,而不是将 __m128i
变量仅保存在寄存器中,IIRC。
您 可以 编写所有使用内部函数的代码,而无需使用任何加载或存储内部函数,但是您将受编译器的支配来决定如何以及何时移动数据。 (现在在某种程度上,你实际上仍然是,这要归功于编译器擅长优化内在函数,而不仅仅是在你使用加载内在函数的地方从字面上吐出一个负载。)
编译器会将负载折叠到内存操作数中以供后续指令使用,如果该值也不需要作为其他内容的输入。但是,只有当数据位于已知对齐地址或使用了对齐加载内在函数时,这才是安全的。
我目前考虑加载内在函数的方式是将对齐保证(或缺少对齐保证)传达给编译器。 "regular" SSE(非如果与未对齐的 128b 内存操作数一起使用,则向量指令的 AVX/非 VEX 编码版本会出错。 (即使在支持 AVX、FWIW 的 CPU 上。)例如,请注意,即使 punpckl*
也将其内存操作数列为 m128
,因此具有对齐要求,即使它实际上只读取低位 64b。 pmovzx
将其操作数列为 m128
.
无论如何,使用 load
而不是 loadu
告诉编译器它可以将负载折叠成另一条指令的内存操作数,即使它不能证明它来自一个对齐的地址。
为 AVX 目标机器编译将允许编译器将未对齐的负载折叠到其他操作中,以利用 uop 微融合。
这出现在 How to specify alignment with _mm_mul_ps 的评论中。
store
内在函数显然有两个目的:
- 告诉编译器它应该使用对齐的还是未对齐的 asm 指令。
- 消除从
__m128d
到 double *
的转换的需要(不适用于整数情况)。
只是为了混淆,AVX2 引入了像 _mm256_storeu2_m128i (__m128i* hiaddr, __m128i* loaddr, __m256i a)
这样的东西,它将 high/low 的一半存储到不同的地址。它可能编译为 vmovdqu / vextracti128 ..., 1
序列。顺便说一下,我猜他们在制作 vextracti128
时考虑到了 AVX512,因为将它与 0 一起用作立即数与 vmovdqu
相同,但编码速度较慢且时间较长。
在我提问之前,先了解一下背景信息。
在 C 语言中,当你赋值给一个变量时,你可以在概念上假设你只是修改了 RAM 中的一小块内存。
int a = rand(); //conceptually, you created and assigned variable A in ram
在汇编语言中,要完成同样的事情,您基本上需要将 rand() 的结果存储在寄存器中,以及指向 "a" 的指针。然后,您将执行存储指令以将寄存器内容放入 ram。
例如,当您使用 C++ 编程时,当您分配和操作值类型对象时,您通常甚至不必考虑它们的地址或它们将如何或何时存储在寄存器中。
使用 SSE 内参很奇怪,因为就概念内存模型而言,它们似乎介于 C 和汇编代码之间。
您可以调用 load/store 个函数,它们 return 个对象。像 _mm_add 这样的数学运算将 return 一个对象,但我不清楚结果是否会实际存储在对象中,除非你调用 _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 的方式调用汇编指令的语法糖?如果你不在最后执行存储命令,而只是保留结果,会发生什么情况,结果会不会不仅仅是语法糖,而且会实际保存数据?
TL:DR 在使用 SSE 内部函数时我应该如何考虑内存?
__m128
变量可能在寄存器 and/or 内存中。它与简单的 float
或 int
变量非常相似——编译器将决定哪些变量属于寄存器,哪些必须存储在内存中。通常,编译器会尝试将 "hottest" 变量保存在寄存器中,其余的保存在内存中。它还将分析变量的生命周期,以便一个寄存器可以用于一个块中的多个变量。作为一名程序员,您不必太担心这一点,但您应该知道您有多少个寄存器,即 32 位模式下有 8 个 XMM 寄存器,64 位模式下有 16 个。将您的变量使用量保持在这些数字以下将有助于尽可能将所有内容保存在寄存器中。话虽如此,在 L1 缓存中访问操作数的代价并不 比访问寄存器操作数大得多,所以如果它的话,你不应该太执着于将所有内容都保存在寄存器中事实证明很难做到。
脚注:在使用内在函数时,关于 SSE 变量是在寄存器中还是在内存中的这种含糊不清实际上非常有帮助,并且使编写优化代码比使用原始汇编程序编写代码更容易 - 编译器会做一些繁重的工作来保持跟踪寄存器分配和其他优化,让您专注于使代码正常工作。
向量变量并不特殊。如果编译器在优化循环时用完了寄存器(或跨函数调用函数编译器无法 "see" 知道它没有,它们将被溢出到内存并在以后需要时重新加载t touch the vector regs).
gcc -O0
实际上确实倾向于在您设置它们时存储到 RAM,而不是将 __m128i
变量仅保存在寄存器中,IIRC。
您 可以 编写所有使用内部函数的代码,而无需使用任何加载或存储内部函数,但是您将受编译器的支配来决定如何以及何时移动数据。 (现在在某种程度上,你实际上仍然是,这要归功于编译器擅长优化内在函数,而不仅仅是在你使用加载内在函数的地方从字面上吐出一个负载。)
编译器会将负载折叠到内存操作数中以供后续指令使用,如果该值也不需要作为其他内容的输入。但是,只有当数据位于已知对齐地址或使用了对齐加载内在函数时,这才是安全的。
我目前考虑加载内在函数的方式是将对齐保证(或缺少对齐保证)传达给编译器。 "regular" SSE(非如果与未对齐的 128b 内存操作数一起使用,则向量指令的 AVX/非 VEX 编码版本会出错。 (即使在支持 AVX、FWIW 的 CPU 上。)例如,请注意,即使 punpckl*
也将其内存操作数列为 m128
,因此具有对齐要求,即使它实际上只读取低位 64b。 pmovzx
将其操作数列为 m128
.
无论如何,使用 load
而不是 loadu
告诉编译器它可以将负载折叠成另一条指令的内存操作数,即使它不能证明它来自一个对齐的地址。
为 AVX 目标机器编译将允许编译器将未对齐的负载折叠到其他操作中,以利用 uop 微融合。
这出现在 How to specify alignment with _mm_mul_ps 的评论中。
store
内在函数显然有两个目的:
- 告诉编译器它应该使用对齐的还是未对齐的 asm 指令。
- 消除从
__m128d
到double *
的转换的需要(不适用于整数情况)。
只是为了混淆,AVX2 引入了像 _mm256_storeu2_m128i (__m128i* hiaddr, __m128i* loaddr, __m256i a)
这样的东西,它将 high/low 的一半存储到不同的地址。它可能编译为 vmovdqu / vextracti128 ..., 1
序列。顺便说一下,我猜他们在制作 vextracti128
时考虑到了 AVX512,因为将它与 0 一起用作立即数与 vmovdqu
相同,但编码速度较慢且时间较长。