为何Clang、GCC等编译器生成低效的循环展开代码?
为何编译器会生成低效的循环展开代码?
编译环境
- GCC 版本 14.2
- Clang 版本 19.1.0
- 编译参数:
-Ofast -lm
示例代码(变体1)
#define MIN(a,b) ((a) < (b)? (a) : (b)) void to_test(size_t num_iters) { const size_t exec_iters = MIN(num_iters, 10); #if __GNUC__ #pragma GCC unroll 10 #elif __clang__ #pragma unroll 10 #endif for (size_t i = 0; i < exec_iters; ++i) { printf("%d\n", i); } }
两款编译器都会展开循环,但每次执行后都插入条件跳转判断是否结束,生成的汇编代码效率极低,大致如下:
to_test: test rdi, rdi je .L33 push rbx xor esi, esi mov rbx, rdi xor eax, eax mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 1 je .L1 xor eax, eax mov esi, 1 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 2 je .L1 xor eax, eax mov esi, 2 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 3 je .L1 xor eax, eax mov esi, 3 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 4 je .L1 xor eax, eax mov esi, 4 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 5 je .L1 xor eax, eax mov esi, 5 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 6 je .L1 xor eax, eax mov esi, 6 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 7 je .L1 xor eax, eax mov esi, 7 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 8 je .L1 xor eax, eax mov esi, 8 mov edi, OFFSET FLAT:.LC0 call printf cmp rbx, 9 je .L1 mov esi, 9 mov edi, OFFSET FLAT:.LC0 xor eax, eax pop rbx jmp printf
示例代码(变体2)
void to_test(size_t num_iters) { const size_t exec_iters = fmin(num_iters, 10); #if __GNUC__ #pragma GCC unroll 10 #elif __clang__ #pragma unroll 10 #endif for (size_t i = 0; i < exec_iters; ++i) { printf("%d\n", i); } }
两款编译器的表现出现差异:
- GCC 生成包含大量比较的线性搜索代码
- Clang 会先判断迭代次数是否为10,若是则生成完全展开的无检查循环;否则不展开循环,仅生成普通循环
这些实现的效率都不理想,尤其是GCC的线性搜索和Clang在迭代次数小于10时不展开的行为。
期望的优化方向
理想情况下,编译器可以生成更高效的代码,比如通过计算起始索引直接跳转到对应位置执行:
// 假设 exec_iters = 4 // 则 start_idx = 4 - 10 = -6 const start_idx = -6; jump label_fabs({start_idx}) :label_0 printf("%d\n", start_idx + 0); // i = 0 :label_1 printf("%d\n", start_idx + 1); // i = 1 :label_2 printf("%d\n", start_idx + 2); // i = 2 :label_3 printf("%d\n", start_idx + 3); // i = 3 :label_4 printf("%d\n", start_idx + 4); // i = 4 :label_5 printf("%d\n", start_idx + 5); // i = 5 :label_6 printf("%d\n", start_idx + 6); // i = 6 :label_7 printf("%d\n", start_idx + 7); // i = 7 :label_8 printf("%d\n", start_idx + 8); // i = 8 :label_9 printf("%d\n", start_idx + 9); // i = 9
或者生成对应不同迭代次数的10个函数,直接跳转执行。
尝试调整代码为固定循环结束点、变量起始点的写法,仍未得到更优结果:
#define MIN(a,b) ((a) < (b)? (a) : (b)) void to_test(size_t num_iters) { const size_t exec_iters = MIN(num_iters, 10); int loop_start = 10 - exec_iters; int base_offset = exec_iters - 10; int running_offset = loop_start; #if __GNUC__ #pragma GCC unroll 10 #elif __clang__ #pragma unroll 10 #endif for (size_t i = loop_start; i < 10; ++i) { printf("%d\n", base_offset + running_offset); ++running_offset; } }
内容的提问来源于stack exchange,提问作者user4833973
相关产品推荐
相关产品推荐

