当指针为零时理解 Clang 的优化

Understanding Clang's optimization when pointer is zero

简而言之: 尝试在此处将 foos 指针从 0 切换为 1: godbolt - compiler explorer link - 发生了什么?


当我编译以下 C 代码时,我很惊讶从 clang 中发出了多少指令。 - 我注意到它只发生在指针 foos 为零时。 (x86-64 clang 12.0.1 with -O2 or -O3).

#include <stdint.h>

typedef uint8_t     u8;
typedef uint32_t    u32;


typedef struct {
    u32 x;
    u32 y;
}Foo;

u32 count = 500;

int main()
{
     u8 *foos = (u8 *)0;

    u32 element_size = 8;
    u32 offset = 0;
    for(u32 i=0;i<count;i++)
    {
        u32 *p = (u32 *)(foos + element_size*i);
        *p = i;
    }

     return 0;
}

这是指针为零时的输出。

main:                                   # @main
        mov     r8d, dword ptr [rip + count]
        test    r8, r8
        je      .LBB0_6
        lea     rcx, [r8 - 1]
        mov     eax, r8d
        and     eax, 3
        cmp     rcx, 3
        jae     .LBB0_7
        xor     ecx, ecx
        jmp     .LBB0_3
.LBB0_7:
        and     r8d, -4
        mov     esi, 16
        xor     ecx, ecx
.LBB0_8:                                # =>This Inner Loop Header: Depth=1
        lea     edi, [rsi - 16]
        and     edi, -32
        mov     dword ptr [rdi], ecx
        lea     edi, [rsi - 8]
        and     edi, -24
        lea     edx, [rcx + 1]
        mov     dword ptr [rdi], edx
        mov     edx, esi
        and     edx, -16
        lea     edi, [rcx + 2]
        mov     dword ptr [rdx], edi
        lea     edx, [rsi + 8]
        and     edx, -8
        lea     edi, [rcx + 3]
        mov     dword ptr [rdx], edi
        add     rcx, 4
        add     rsi, 32
        cmp     r8, rcx
        jne     .LBB0_8
.LBB0_3:
        test    rax, rax
        je      .LBB0_6
        lea     rdx, [8*rcx]
.LBB0_5:                                # =>This Inner Loop Header: Depth=1
        mov     esi, edx
        and     esi, -8
        mov     dword ptr [rsi], ecx
        add     rdx, 8
        add     ecx, 1
        add     rax, -1
        jne     .LBB0_5
.LBB0_6:
        xor     eax, eax
        ret
count:
        .long   500                             # 0x1f4

你能帮我理解这里发生了什么吗?我不太了解汇编。 AND with 3 向我表明存在一些对齐分支。 LBB0_8 的顶部对我来说看起来很奇怪...

这是循环展开。

代码首先检查 count 是否大于 3,如果是,则分支到 LBB0_7,它设置循环变量并在 LBB0_8 处进入循环。这个循环每次迭代执行 4 个步骤,只要还有 4 个或更多步骤要做。之后它落入 LBB0_3/LBB0_5 处的“慢速路径”,每次迭代只执行一个步骤。
这条缓慢的路径也与您使用该指针的非零值编译代码时得到的路径非常相似。

至于为什么会出现这种情况,我不知道。最初我认为编译器证明 NULL deref 将在循环内发生并对其进行优化,但通常这类似于用 __builtin_unreachable(); 替换循环内容,这导致它完全抛出循环。仍然不能排除这种可能性,但我已经多次看到编译器抛出了很多代码,所以至少 UB 似乎不太可能导致这种情况。
然后我在想也许 0 不需要额外的计算,但它所需要改变的只是 mov esi, 16mov esi, 17,所以它有相同数量的指令。
同样有趣的是,在 x86_64 上,它生成一个每次迭代 4 步的循环,而在 arm64 上,它生成一个每次迭代 2 步的循环。