为了缩小我的问题,让我描述一下我的假设和我做过的实验......
我的假设: 用汇编语言编写的代码运行速度比C / C ++代码快得多,而且可执行文件的大小要小得多从C / C ++代码生成。
实验:我将以下程序写入bin2dec.c
#include <stdio.h>
int main()
{
long int binary, decimal, reminder, exp;
int i, j;
for(i=0; i<10000; i++)
{
for(j=0; j<1000; j++)
{
binary = 11000101;
exp = 1;
decimal = 0;
while(binary != 0)
{
reminder = binary % 10;
binary = binary / 10;
decimal = decimal + reminder * exp;
exp *= 2;
}
}
}
return 0;
}
然后为其生成了ASM代码gcc -S bin2dec.c -o bin2dec.s
之后我编译了两个文件
gcc bin2dec.c -o bin2dec_c
gcc bin2dec.s -o bin2dec_s
测试1:找到两个文件的一些内部细节
[guest@localhost ASM]$ size bin2dec_c bin2dec_s
text data bss dec hex filename
951 252 4 1207 4b7 bin2dec_c
951 252 4 1207 4b7 bin2dec_s
结果:两者完全相同......
测试2:执行文件并计算所花费的时间
[guest@localhost ASM]$ time ./bin2dec_c
real 0m1.724s
user 0m1.675s
sys 0m0.002s
[guest@localhost ASM]$ time ./bin2dec_s
real 0m1.721s
user 0m1.676s
sys 0m0.001s
结果:两者都相同。有些时候从ASM生成的可执行文件运行得慢: - (
所以问题是,我的假设是否错误? 如果没有,我做了什么错误,以便可执行文件bin2dec_c和bin2dec_s以相同的速度运行? 有没有更好的方法从C / C ++程序中获取ASM代码,或者我应该从ASM中从头开始重写所有逻辑以获得速度和程序大小的优势?
答案 0 :(得分:3)
这是一个古老的传统(在20世纪70年代早期的Unix系统中,机器非常小,生成一些汇编程序文件更简单),而且有些编译器可以直接生成目标文件或机器代码。可能是最新版本的Clang/LLVM或TinyCC(仅适用于C:快速编译时,但执行速度非常慢!)也许来自IBM的一些专有XLC编译器,以及GCC社区中的一些人正在考虑那(特别是GCCJIT)。
但是,生成汇编程序文件对于编译器开发人员来说通常更容易。并且由于大多数编译器工作都发生在optimization次传递(在编译器中转换某些内部表示),因此丢失几毫秒来启动汇编程序并不是很重要。
使用GCC,使用gcc -time
和gcc -ftime-report
进行编译(当然还有常用的优化标记,例如-O2
),以了解编译器花费时间的位置。它永远不会出现在汇编程序中......
有时您可能会发现查看生成的汇编程序文件很有用。使用foo.cc
编译g++ -O2 -Wall -S -fverbose-asm -std=c++11 foo.cc
C ++ 11文件,然后(使用某个编辑器或寻呼机)查看生成的foo.s
汇编程序文件。
您甚至可以使用g++ -fdump-tree-all -O2
进行编译,并从GCC获取数百个编译器转储文件,解释编译器对您的代码所做的转换。
(顺便说一句,允许优化编译器转换你的bin2dec.c
程序,它没有可观察到的副作用,例如没有输入和输出,进入空程序,GCC 5.2用gcc -O2
!!)
另请阅读halting problem和Rice's theorem。优化编译器或static program analyzers可以实现的内在限制。
答案 1 :(得分:2)
假设:用汇编语言编写的代码运行速度比它快得多 C / C ++对应物和可执行文件的大小要小得多 从C / C ++代码生成的。
汇编语言只是机器代码的文本表示。
有一些注意事项,您可以反汇编二进制文件,并将该源重新组合回同一个二进制文件中。显然这对于ARM来说确实是可能的,但x86 asm方言没有语法来表示同一指令的不同编码。例如强制在PLT(过程链接表)中的jmp
指令中使用4字节偏移量,跳转目标将在稍后进行修补。
您的实验制作了两个相同的二进制文件。 gcc直接从C转到可执行文件内部生成一个asm源文件并组装它。你只需要分解这个过程,这样就可以了解编译器生成的asm。
手写汇编代码始终至少与编译器输出一样好。您始终可以从编译器输出开始并寻找改进。在极少数情况下,不会有任何改进
但是,编译过程中编译器生成的asm只是观察并没有做任何事情来改进它!将代码插入http://gcc.godbolt.org/以查看各种不同编译器的输出(甚至是ARM或PPC,这对于std:atomic
代码很有用,看看在弱有序的arch上会发生什么)
由于您编译时没有进行优化,因此肯定会有很大的改进。我将从gcc -O3 -march=native -fverbose-asm -masm=intel -S
编译器输出真的是最佳的非常,即使对于短序列也是如此。编译器对人类具有优势的地方在于一次跟踪大量源代码,并根据他们可以跨功能证明的内容进行优化。 (这样的整个程序优化对于人类在源代码中维护来说太脆弱了。)因此编译器可以利用在这个构建中恰好是真实的东西,但不是正在编译的函数的设计的一部分。 / p>
编译器几乎总是执行良好的作业,但极少需要伟大的作业。重要的是,它是足够好的作业,并且代码运行速度很快,即使它使用的指令多于所需的指令。通常诸如分支错误预测,缓存未命中和依赖链之类的东西都是瓶颈,并且CPU足够宽以处理编译器倾向于使用的额外指令而没有显着的减速。使用超线程,使用更少的指令执行相同的工作是一个更大的优势。
有关具体示例,请参阅https://codereview.stackexchange.com/questions/6502/fastest-way-to-clamp-an-integer-to-the-range-0-255上的编译器输出,并将其与我的probably-optimal hand-written asm进行比较。我试图让gcc生成类似的最佳输出,但没有成功。它要么使用多个分支,要么使用两个cmov指令(这将使得无钳位快速路径变慢),而不是用于钳位的分支,然后是钳位为零或钳位到最大值的cmov。