有没有办法指示 GCC(我使用的是 4.8.4)完全展开底部函数中的循环while
,即剥离这个循环?循环的迭代次数在编译时已知:58。
让我先解释一下我尝试过的方法。
通过检查 GAS 输出:
gcc -fpic -O2 -S GEPDOT.c
使用了 12 个寄存器 XMM0 - XMM11。如果我将标志传递-funroll-loops
给 gcc:
gcc -fpic -O2 -funroll-loops -S GEPDOT.c
循环仅展开两次。我检查了 GCC 优化选项。GCC 表示它-funroll-loops
也会打开-frename-registers
,因此当 GCC 展开循环时,它对寄存器分配的优先选择是使用“剩余”寄存器。但是 XMM12 - XMM15 只剩下 4 个,所以 GCC 最多只能展开 2 次。如果有 48 个而不是 16 个 XMM 寄存器可用,GCC 将毫无问题地展开 while 循环 4 次。
然而我又做了一个实验。我首先手动展开了两次 while 循环,获得了一个函数GEPDOT_2
。那么两者之间完全没有区别
gcc -fpic -O2 -S GEPDOT_2.c
和
gcc -fpic -O2 -funroll-loops -S GEPDOT_2.c
由于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);
}
更新 1
感谢@user3386109 的评论,我想稍微扩展一下这个问题。@user3386109 提出了一个非常好的问题。实际上,当有这么多并行指令要调度时,我确实对编译器优化寄存器分配的能力有些怀疑。
我个人认为一种可靠的方法是首先在asm内联汇编中对循环体(HPC 的关键)进行编码,然后根据需要多次复制它。今年早些时候我有一个不受欢迎的帖子:内联汇编。代码有点不同,因为循环迭代的次数 j 是一个函数参数,因此在编译时是未知的。在那种情况下,我无法完全展开循环,所以我只复制了两次汇编代码,并将循环转换为标签并跳转。事实证明,我编写的程序集的结果性能比编译器生成的程序集高约 5%,这可能表明编译器未能以我们预期的最佳方式分配寄存器。
我曾经(现在仍然)是汇编编码方面的婴儿,因此这是一个很好的案例研究,可以让我学习一点 x86 汇编。但从长远来看,我并不倾向于编写GEPDOT
大量用于汇编的代码。主要有以下三个原因:
- asm内联汇编因不可移植而受到批评。虽然我不明白为什么。也许是因为不同的机器有不同的寄存器被破坏?
- 编译器也越来越好。所以我还是更喜欢算法优化和更好的 C 编码习惯来帮助编译器生成好的输出;
- 最后一个原因更重要。迭代次数可能并不总是 58。我正在开发一个高性能的矩阵分解子程序。对于缓存阻塞因子
nb
,迭代次数为nb-2
。我不会nb
像在之前的帖子中那样将其作为函数参数。这是一个机器特定的参数,将被定义为一个宏。因此迭代次数在编译时是已知的,但可能因机器而异。猜猜我在为各种nb
. 因此,如果有一种方法可以简单地指示编译器剥离循环,那就太好了。
如果您还可以分享一些生产高性能但可移植的库的经验,我将不胜感激。