为什么C / C ++比Assembly和其他低级语言慢?

时间:2016-06-20 21:43:06

标签: c++ assembly

我编写代码,在C ++中什么都不做

void main(void){

}

和大会。

.global _start
.text

_start:
    mov $60, %rax
    xor %rdi, %rdi 
    syscall

我编译C代码并编译和链接汇编代码。我用时间命令对两个可执行文件进行了比较。

汇编

time ./Assembly

real    0m0.001s
user    0m0.000s
sys     0m0.000s

C

time ./C

real    0m0.002s
user    0m0.000s
sys     0m0.000s

汇编速度是C的两倍。我反汇编代码,在汇编代码中,只有四行代码(相同)。在C代码中,有大量不必要的代码用于将main链接到_start。主要有四行代码,其中三行是为了使不可能(你无法从函数博客外部访问一个函数变量)来访问&# 39; 本地' (如功能变量)变量来自&strong; 阻止' (比如功能块)。

push %rbp ; push base pointer.
mov  %rsp, %rbp ; copy value of stack pointer to base pointer, stack pointer is using for saving variables.
pop  %rbp ; 'local' variables are removed, because we pop the base pointer 
retq ; ?

这是为什么?

3 个答案:

答案 0 :(得分:13)

执行您编写的程序核心所需的时间非常小。图中它由三个或四个汇编代码组成,并且在几千兆赫兹的情况下运行只需要几纳秒。这是一个很短的时间,它远远低于time程序的检测阈值,其分辨率以毫秒为单位(请记住,毫秒是一纳秒的百万分之一秒) !)所以从这个意义上说,我会非常小心地判断一个程序的运行时间是"快两倍"作为另一个;你的计时器的分辨率不够高,可以说肯定。你可能只是看到噪音条款。

但是,您的问题是,如果没有任何事情发生,所有这些都是自动生成的代码。答案是"它取决于。"在没有打开优化的情况下,大多数编译器会生成汇编代码,这些代码可以忠实地模拟您编写的程序,可能需要做更多的工作。由于大多数C和C ++函数,你实际上会有代码执行某些操作,需要局部变量等,编译器在函数的开头和结尾发出代码以设置堆栈时不会出错和帧指针适当地支持这些变量。随着优化达到最大值,优化编译器可能足够聪明,可以注意到这不是必需的并且删除了该代码,但它不是必需的。

原则上,一个完美的编译器总能发出最快的代码,但事实证明,构建一个总能做到这一点的编译器是不可能的(这与诸如停止的不可判断性之类的事情有关问题)。因此,它有点假设生成的代码会很好 - 甚至很好 - 但不是最佳的。但是,这是一个权衡。是的,代码可能没有它可能的那么快,但是通过使用C和C ++这样的语言,可以以一种方式编写大型复杂程序(与汇编相比)阅读,易于编写,易于维护。我们对性能的轻微影响还不错,因为在实践中它并不太糟糕,而且大多数优化编译器足以使价格微不足道(如果优化编译器找到更好的方法来解决问题,那么甚至是负面的)问题比人类!)

总结:

  • 您的计时机制可能不足以得出您所做出的结论。你需要一个更高精度的计时器。

  • 为简单起见,编译器通常会生成不必要的代码。优化编译器通常会删除该代码,但不能总是这样。

  • 由于易于开发,我们可以在原始运行时方面支付使用更高级语言的费用。实际上,使用具有良好优化编译器的高级语言实际上可能是一个净胜利,因为它可以减轻优化的复杂性。

答案 1 :(得分:3)

C的所有额外时间都是动态链接器和CRT开销。 asm程序是静态链接的,只调用exit(2)(直接调用sycall,而不是glibc包装器)。当然它更快,但它只是启动开销,并没有告诉你任何关于实际执行任何操作的编译器发出的代码运行速度有多快。

即。如果你编写了一些代码来实际用C语言做某些事情,并用gcc -O3 -march=native编译它,你会发现它比没有CRT开销的静态链接二进制文件慢~0.002秒。 (如果您手写的asm和编译器输出都接近最佳。例如,如果您使用编译器输出作为手动优化版本的起点,但没有找到任何重要的东西。通常可以制作一些对编译器输出的改进,但通常只是代码大小,可能对速度影响不大。)

如果您要拨打mallocprintf,那么启动费用 无用;实际上有必要初始化glibc内部数据结构,这样库函数就没有任何开销来检查每次调用它们时都会初始化。

通过链接glibc的静态链接手写asm程序,您需要先致电__libc_init_first__dl_tls_setup__libc_csu_initin that order,然后才能安全使用所有libc函数。

无论如何,理想情况下,您可以预期与启动开销之间存在恒定的时间差异,而不是2倍的差异。

如果您擅长编写最佳asm,通常可以在本地范围内比编译器做得更好,但编译器非常擅长全局优化。而且,他们只需几秒的CPU时间(非常便宜),而不是几周的人力(非常宝贵)。

手工制作关键循环是有意义的,例如:作为视频编码器的一部分,但即使是视频编码器(如x264,x264和vpx)也有大部分用C或C ++编写的逻辑,只需调用asm函数。

额外的push / mov / pop指令是因为您编译了禁用优化,其中-fno-omit-frame-pointer is the default,并且即使对于叶函数也会生成堆栈帧。 gcc在-fomit-frame-pointer默认为-O1,在x86和x86-64上默认为更高(因为现代调试元数据格式意味着调试或异常处理堆栈展开不需要它。)

如果您告诉C编译器生成快速代码(-O3),而不是快速编译并生成在调试器(-O0)中运行良好的哑代码,那么您将获得main的代码(来自Godbolt compiler explorer):

// this is valid C++ and C99, but C89 doesn't have an implicit return 0 in main.  
int main(void) {}

    xor     eax, eax
    ret

要了解有关汇编以及一切如何工作的更多信息,请查看标记wiki中的一些链接。也许Programming From the Ground Up会是一个好的开始;它可能解释了编译器和动态链接。

更短的文章是A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux,它从您所做的开始,然后归结为_start与其他一些ELF标题重叠,因此文件可以更小。

答案 2 :(得分:0)

  1. 您是否在启用优化的情况下编译?如果没有,那么这是无效的。

  2. 您是否认为这是一个完全无关紧要的例子,即使是一张明信片,也没有真正的性能影响?

  3. 请写清楚可维护的代码,并且(在99%的情况下)将优化留给编译器。请。