李哲源*_*李哲源 9 c x86 gcc hpc loop-unrolling
有没有一种方法来指示GCC(版本我用4.8.4),以展开在底层函数while循环完全,即剥离这个循环?循环的迭代次数在编译时是已知的:58.
我先解释一下我的尝试.
通过检查GAS输出:
gcc -fpic -O2 -S GEPDOT.c
Run Code Online (Sandbox Code Playgroud)
使用12个寄存器XMM0 - XMM11.如果我将标志-funroll-loops传递给gcc:
gcc -fpic -O2 -funroll-loops -S GEPDOT.c
Run Code Online (Sandbox Code Playgroud)
循环只展开两次.我检查了GCC优化选项.GCC表示-funroll-loops也会打开-frename-registers,所以当GCC展开一个循环时,它先前选择的寄存器分配是使用"遗留"寄存器.但是XMM12只剩下4个 - XMM15,所以GCC最多只能展开2次.如果有48个而不是16个XMM寄存器可供使用,GCC将毫无困难地展开while循环4次.
然而,我做了另一个实验.我首先手动两次展开while循环,获得一个函数GEPDOT_2.然后两者之间没有任何区别
gcc -fpic -O2 -S GEPDOT_2.c
Run Code Online (Sandbox Code Playgroud)
和
gcc -fpic -O2 -funroll-loops -S GEPDOT_2.c
Run Code Online (Sandbox Code Playgroud)
由于GEPDOT_2已用完所有寄存器,因此不执行展开.
GCC确实注册了重命名,以避免引入潜在的错误依赖.但我确信在我的GEPDOT中没有这样的潜力; 即使有,也不重要.我尝试自己展开循环,展开4次比展开2次更快,比没有展开更快.当然我可以手动展开更多次,但这很乏味.GCC可以帮我吗?谢谢.
// C file "GEPDOT.c"
#include <emmintrin.h>
void GEPDOT (double *A, double *B, double *C) {
__m128d A1_vec = _mm_load_pd(A); A += 2;
__m128d B_vec = _mm_load1_pd(B); B++;
__m128d C1_vec = A1_vec * B_vec;
__m128d A2_vec = _mm_load_pd(A); A += 2;
__m128d C2_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C3_vec = A1_vec * B_vec;
__m128d C4_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C5_vec = A1_vec * B_vec;
__m128d C6_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
__m128d C7_vec = A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
__m128d C8_vec = A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
int k = 58;
/* can compiler unroll the loop completely (i.e., peel this loop)? */
while (k--) {
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A); A += 2;
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C7_vec += A1_vec * B_vec;
A1_vec = _mm_load_pd(A); A += 2;
C8_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
}
C1_vec += A1_vec * B_vec;
A2_vec = _mm_load_pd(A);
C2_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C3_vec += A1_vec * B_vec;
C4_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B); B++;
C5_vec += A1_vec * B_vec;
C6_vec += A2_vec * B_vec;
B_vec = _mm_load1_pd(B);
C7_vec += A1_vec * B_vec;
C8_vec += A2_vec * B_vec;
/* [write-back] */
A1_vec = _mm_load_pd(C); C1_vec = A1_vec - C1_vec;
A2_vec = _mm_load_pd(C + 2); C2_vec = A2_vec - C2_vec;
A1_vec = _mm_load_pd(C + 4); C3_vec = A1_vec - C3_vec;
A2_vec = _mm_load_pd(C + 6); C4_vec = A2_vec - C4_vec;
A1_vec = _mm_load_pd(C + 8); C5_vec = A1_vec - C5_vec;
A2_vec = _mm_load_pd(C + 10); C6_vec = A2_vec - C6_vec;
A1_vec = _mm_load_pd(C + 12); C7_vec = A1_vec - C7_vec;
A2_vec = _mm_load_pd(C + 14); C8_vec = A2_vec - C8_vec;
_mm_store_pd(C,C1_vec); _mm_store_pd(C + 2,C2_vec);
_mm_store_pd(C + 4,C3_vec); _mm_store_pd(C + 6,C4_vec);
_mm_store_pd(C + 8,C5_vec); _mm_store_pd(C + 10,C6_vec);
_mm_store_pd(C + 12,C7_vec); _mm_store_pd(C + 14,C8_vec);
}
Run Code Online (Sandbox Code Playgroud)
更新1
感谢@ user3386109的评论,我想稍微扩展这个问题.@ user3386109提出了一个非常好的问题.实际上,当有很多并行指令需要调度时,我确实对编译器的最佳寄存器分配能力有所怀疑.
我个人认为一种可靠的方法是首先在asm内联汇编中编码循环体(这是HPC的关键),然后根据需要复制它多次.今年早些时候,我有一个不受欢迎的帖子:内联汇编.代码有点不同,因为循环迭代次数j是一个函数参数,因此在编译时是未知的.在这种情况下,我无法完全展开循环,因此我只复制了汇编代码两次,并将循环转换为标签并跳转.事实证明,我编写的程序集的最终性能比编译器生成的程序集高约5%,这可能表明编译器无法以我们预期的最佳方式分配寄存器.
我(并且仍然)是汇编编码的宝贝,所以这对我来说是一个很好的案例研究,可以学习x86汇编.但从长远来看,我并不倾向于对GEPDOT进行大规模的装配编码.主要有三个原因:
如果您还可以分享一些生产高性能,便携式库的经验,我将不胜感激.
尝试调整优化器参数:
gcc -O3 -funroll-loops --param max-completely-peeled-insns=1000 --param max-completely-peel-times=100
Run Code Online (Sandbox Code Playgroud)
这应该可以解决问题。