当指针为零时理解 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, 16
到 mov esi, 17
,所以它有相同数量的指令。
同样有趣的是,在 x86_64 上,它生成一个每次迭代 4 步的循环,而在 arm64 上,它生成一个每次迭代 2 步的循环。
简而言之: 尝试在此处将 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, 16
到 mov esi, 17
,所以它有相同数量的指令。
同样有趣的是,在 x86_64 上,它生成一个每次迭代 4 步的循环,而在 arm64 上,它生成一个每次迭代 2 步的循环。