[英]if statement in assembly ouput of c code
我在c中有这段简单的代码:
#include <stdio.h>
void test() {}
int main()
{
if (2 < 3) {
int zz = 10;
}
return 0;
}
当我看到此代码的汇编输出时:
test():
pushq %rbp
movq %rsp, %rbp
nop
popq %rbp
ret
main:
pushq %rbp
movq %rsp, %rbp
movl $10, -4(%rbp) // space is created for zz on stack
movl $0, %eax
popq %rbp
ret
我从这里得到了程序集(默认选项),我看不到条件检查的指令在哪里?
您看不到它,因为它不存在。 编译器能够执行分析,并且很容易看到将始终输入该分支。
它不会发出只会浪费CPU周期的检查,而是发出易于优化的代码版本。
AC程序不是 CPU要执行的指令序列。 这就是发出的机器代码。 AC程序是对已编译程序应具有的行为的描述。 编译器可以自由地以其所需的几乎任何方式进行翻译,只要您获得该行为即可 。
它被称为“假设规则”。
有趣的是,与其他一些编译器(ICC和MSVC)不同,gcc和clang 即使在-O0
处也优化了if()
)。
gcc -O0
不代表没有优化,这意味着没有额外的优化 Beyond的需要在所有的编译什么。 但是在发出asm之前,gcc确实必须通过功能逻辑的几个内部表示进行转换。 (GIMPLE和寄存器传输语言)。 gcc没有特殊的“哑模式”,它会强制将每个C表达式的每个部分音译为asm。
即使是像TCC这样的超简单单程编译器,也可以在表达式(甚至语句)中进行较小的优化,例如意识到始终为真的条件不需要分支。
gcc -O0
是默认值,您显然使用了它,因为未优化存储到zz
的无效存储。
gcc -O0
旨在快速编译,并给出一致的调试结果 。
C语句的寄存器中没有任何内容,因此您可以在调试过程中单步修改任何C变量。 即溢出/重新加载单独的C语句之间的所有内容。 (这就是为什么对-O0
基准测试是无意义的: 用更少的大表达式编写相同的代码仅在-O0
才更快 ,而不是像-O3
这样的真实设置才更快 )。
其他有趣的结果:常数传播无效,请参阅为什么将-1除以整数(负数)会导致FPE? 对于gcc使用div
将变量设置为常量的情况,而对于文字常量,则更简单一些。
jump
到不同的源代码行(在同一函数内)并获得一致的结果。 (与经过优化的代码不同,后者可能会崩溃或产生废话,并且肯定与C抽象机不匹配)。 考虑到gcc -O0
行为的所有这些要求, if (2 < 3)
仍然可以优化为0 asm指令。 该行为不依赖于任何变量的值,而只是一个语句。 绝不可能不采用它,因此编译它的最简单方法是没有指令:进入if
的{ body }
。
请注意, gcc -O0
的规则/限制远远超出了C-if规则,即函数的机器代码仅必须实现C源代码的所有外部可见行为。 gcc -O3
将整个功能优化到最小
main: # with optimization
xor eax, eax
ret
因为它并不关心为每个C语句保留asm。
clang与gcc相似,但是在堆栈中的另一个位置的死存储为0
,而zz
为10
。 clang -O0
通常更接近C到asm的音译,例如,它将对x / 2
而不是shift使用div
,而gcc即使在-O0
处也使用乘数逆除以常数 。 但是在这种情况下,clang还决定没有任何指令足以满足始终为真的条件。
ICC和MSVC都为分支发出asm,但是您可能期望的不是mov $2, %ecx
/ cmp $3, %ecx
,它们实际上都是0 != 1
,没有明显的原因:
# ICC18
pushq %rbp #6.1
movq %rsp, %rbp #6.1
subq $16, %rsp #6.1
movl $0, %eax #7.5
cmpl $1, %eax #7.5
je ..B1.3 # Prob 100% #7.5
movl $10, -16(%rbp) #9.16
..B1.3: # Preds ..B1.2 ..B1.1
movl $0, %eax #11.12
leave #11.12
ret #11.12
即使未启用优化,MSVC也会使用“零归零”窥视孔优化。
看看哪一个本地/猫眼优化编译器甚至在-O0
有点有趣,但是它并没有告诉您有关C语言规则或代码的任何基本知识,它只是告诉您有关编译器内部以及编译器开发人员在两者之间进行权衡的选择花时间寻找简单的优化,而不是在无优化模式下更快地进行编译。
汇编绝不打算以任何能让反编译器对其进行重构的方式忠实地表示C源代码。 只是为了实现等效逻辑。
这很简单。 它不在那里。 编译器对其进行了优化。
这是在没有优化的情况下使用gcc进行编译时的程序集:
.file "k.c"
.text
.globl test
.type test, @function
test:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size test, .-test
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $10, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Debian 6.3.0-18) 6.3.0 20170516"
.section .note.GNU-stack,"",@progbits
这就是优化:
.file "k.c"
.text
.p2align 4,,15
.globl test
.type test, @function
test:
.LFB11:
.cfi_startproc
rep ret
.cfi_endproc
.LFE11:
.size test, .-test
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB12:
.cfi_startproc
xorl %eax, %eax
ret
.cfi_endproc
.LFE12:
.size main, .-main
.ident "GCC: (Debian 6.3.0-18) 6.3.0 20170516"
.section .note.GNU-stack,"",@progbits
如您所见,不仅优化已被优化。 几乎整个主体都被优化了,因为它不会产生任何可见的东西。 从未使用变量zz。 您的代码唯一可观察的事情是返回0。
2总是比tan 3小,因此,由于编译器知道2 <3的结果始终为true,因此不需要在汇编器中进行if决定。
优化意味着生成更少的时间/更少的代码。
if (2<3)
始终为true,因此编译器不为此操作任何操作码。
if (2<3)
的条件始终为真。 因此,像样的编译器会检测到此生成代码,就好像条件不存在一样。 实际上,如果使用-O3
进行优化,则godbolt.org只会生成:
test():
rep ret
main:
xor eax, eax
ret
这同样是有效的,因为只要保留了可观察的行为,就允许编译器优化和转换代码。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.