【发布时间】:2020-01-22 19:48:08
【问题描述】:
这是一个非常简单的 C 程序:
int main()
{
int n = 0;
while(n != 1000000000){
n += 1;
}
return n;
}
我用 Clang 编译并计时。它在4.711095243692398e-06 秒或0.000004711095243692398 秒内运行。
接下来,我使用 Godbolt 编译器资源管理器 (https://godbolt.org) 将 C 程序输出到 Intel 语法汇编语言,以删除 .cfi 指令:
.file "Svx.c"
.intel_syntax noprefix
.text
.globl main
.type main, @function
main:
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], 0
jmp .L2
.L3:
add DWORD PTR -4[rbp], 1
.L2:
cmp DWORD PTR -4[rbp], 1000000000
jne .L3
mov eax, DWORD PTR -4[rbp]
pop rbp
ret
.size main, .-main
.ident "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
.section .note.GNU-stack,"",@progbits
我用 GCC 编译并计时。结果是1.96 秒——比 Clang 版本慢得多。
最后,我创建了自己的程序集版本:
[BITS 64]
[default rel]
global main:function
section .data align=16
section .text
main:
xor rax,rax
l_01:
cmp rax,1000000000
je l_02
add rax,5
jmp l_01
l_02:
ret
我用nasm编译它并把它和ld链接起来:
sudo nasm -felf64 Svx.asm
sudo ld -shared Svx.o -o Svx.so
并计时。它在0.14707629615440965 秒内运行。
如果反向编译的版本运行速度非常慢(0.0000047 秒 vs 1.96 秒)并且我的 NASM 版本运行在0.147 秒,为什么 C 版本运行如此之快?我感觉 0.0000047 秒的 C 版本的结果是错误的;它似乎不可能快。这是汇编语言的 Clang 输出:
.text
.intel_syntax noprefix
.file "Svx.c"
.globl main # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
push rbp
.cfi_def_cfa_offset 16
.cfi_offset rbp, -16
mov rbp, rsp
.cfi_def_cfa_register rbp
mov dword ptr [rbp - 4], 0
.LBB0_1: # =>This Inner Loop Header: Depth=1
cmp dword ptr [rbp - 4], 1000000000
je .LBB0_3
# %bb.2: # in Loop: Header=BB0_1 Depth=1
mov eax, dword ptr [rbp - 4]
add eax, 1
mov dword ptr [rbp - 4], eax
jmp .LBB0_1
.LBB0_3:
mov eax, dword ptr [rbp - 4]
pop rbp
.cfi_def_cfa rsp, 8
ret
.Lfunc_end0:
.size main, .Lfunc_end0-main
.cfi_endproc
# -- End function
.ident "clang version 8.0.0-3~ubuntu18.04.1 (tags/RELEASE_800/final)"
.section ".note.GNU-stack","",@progbits
.addrsig
清单显示他们使用堆栈存储变量,而不是寄存器,这(通常)较慢。
0.0000047 秒的速度似乎快到不可能数到十亿。如果这个速度是正确的,它的秘诀是什么?逆向工程没有透露任何信息,实际上 Godbolt 版本要慢得多。
【问题讨论】:
-
很确定
clang正在优化那个无用的循环,只是在做return 1000000000 -
换句话说,Clang 看到它只是数到十亿并跳过计数步骤并循环报告最终的预期数字?
-
没错。这是一个很常见的优化。可以看证明here。
-
要破坏循环优化并强制循环,您可以将
n标记为 volatile,如volatile int n = 0; -
不,永远不要在没有优化的情况下进行基准测试。未优化的代码可以做疯狂的事情,计时结果不可用
标签: c assembly x86 reverse-engineering