为什么clang和gcc在循环中重复代码和分支与无条件跳转

Why do clang and gcc duplicate code and branch vs unconditional jump inside loop?

本文关键字:分支 代码 无条件 gcc clang 循环 为什么      更新时间:2023-10-16

为什么编译器似乎更喜欢将预测试循环优化为检查、条件跳转,然后优化为do-while结构,而不是在do-whil循环内进行无条件跳转?

我写了一个函数,它是用我描述的第二种风格编写的,但g++和clang都撤消了它,并将它转换为方法一。https://godbolt.org/g/2Dvudi

我很困惑,因为编译器似乎为预测试复制了很多指令(在这个例子中可能没有那么多)。此外,它无论如何都可能进行跳跃(尽管可能静态预测不会进行,在一般情况下也没什么大不了的),那么为什么不总是进行无条件跳跃呢?

我对此有一个想法,但它并不强烈支持这两种方法:
循环想要对齐,所以可能有空间在不浪费空间的情况下提前复制一些指令,因为它们会被nop填充。然而,clang和gcc都会为预测试发出超过16字节的代码,然后插入一个大的nop。

编辑:这是来自godbolt链接的代码:

typedef unsigned char uchar;
unsigned my_atoi(const uchar *p)//sentinel at end
{
unsigned acm=0u;
unsigned d;
goto LEnter;
do{
acm =  acm*10u + d;
LEnter:
d = *p++ - '0';
}while (d<10u);
return acm;
}

clang 5.0 at-O2发射:

my_atoi(unsigned char const*):                          # @my_atoi(unsigned char const*)
movzx   ecx, byte ptr [rdi]
add     ecx, -48
xor     eax, eax
cmp     ecx, 9
ja      .LBB0_3
inc     rdi
xor     eax, eax
.LBB0_2:                                # =>This Inner Loop Header: Depth=1
lea     eax, [rax + 4*rax]
lea     eax, [rcx + 2*rax]
movzx   ecx, byte ptr [rdi]
add     ecx, -48
inc     rdi
cmp     ecx, 10
jb      .LBB0_2
.LBB0_3:
ret

引用相关优化过程的GCC来源的一些评论。

如果循环的标头足够小,则复制它们,以便语句在进入循环时,总是执行循环主体中的。这提高了代码运动优化的有效性,并减少了需求用于环路预处理。

也就是说,如果后面的过程找到了一些循环不变的代码,他们将有一个地方可以将代码移动到那里,而无需添加检查循环是否会迭代。

对于所有循环,复制前面循环体末尾的条件循环的。这是有益的,因为它提高了代码运动优化。它还可以在进入循环时保存一次跳转。

初始化d并删除goto使其不再奇怪。

typedef unsigned char uchar;
unsigned my_atoi(const uchar *p) {//sentinel at end
unsigned acm=0u;
unsigned d=0; // initialized
//  goto LEnter;
do{
acm =  acm*10u + d;
//  LEnter:
d = *p++ - '0';
}while (d<10u);
return acm;
}

xor    eax,eax // acm=0
xor    ecx,ecx // d=0
data16 data16 nop WORD PTR cs:[rax+rax*1+0x0] // nops, aligns to 16 bytes
lea    eax,[rax+rax*4] // *5
lea    eax,[rcx+rax*2] // *2+d
movzx  ecx,BYTE PTR [rdi] // d=*p
inc    rdi // p++
add    ecx,0xffffffd0 // d-'0'
cmp    ecx,0xa // d<10
jb     4004e0 <my_atoi(unsigned char const*)+0x10>
ret    
data16 nop WORD PTR cs:[rax+rax*1+0x0]
main:
xor    eax,eax
ret    
nop    WORD PTR cs:[rax+rax*1+0x0]
nop    DWORD PTR [rax]

因此,编译器加倍检查的原因是,您会跳转到一个不对齐的代码中。中央循环看起来足够小,可以放在CPU的循环缓冲区中,但跳到中间可能会污染循环缓冲区。