我们能比我们的C编译器做得更好吗?

2020-08-13 13:22:01

今天,我想成为一名C编译器。我从前面的编码练习中添加了一个手工编译的ECHO汇编版本,并添加了一个新的make目标make asm,它将对其进行汇编。让我们看看我们手工编译的程序集,并将其与我们的C编译器进行比较,然后问问它是否值得。

.text.p2ign 2.globl main.type main,@functionmain:movq%rdi,%r15#从%rdi获取argc,放入%r15 movq%rsi,%r14#从%rsi获取argv,放入%r14loop:decl%r15d#47:for(i=0;i<;argc;I++){JZ Done#重写为:While(--argc){addq$8,%r14#++argv movl$4,%eax#set up write(2)movl$1,%edi#第一个参数为1 movq(%r14),%rsi#第二个参数为*argv leaq-1(%rsi),%rdx#get*argv[0]strlen:#注意:strlen已内联CMPen)leaq 1(%rdx),%rdx#37:T++;Jne strlen subq%rsi,%rdx#39:返回t-s;syscall#48:write(1,*argv,%rdx);cmpl$1,%r15d#49:if(i+1!=argc)JE Done#重写为:if(argc!=1)movl$4,%eax#set up write(2)movl$1,%edi#first参数为1movq%r14,Movl$1,%edX#第三个参数是1 syscall#50:write(1,";";";,1);JMP loopone:movl$4,%eax#设置write(2)movl$1,%edi#第一个参数是1movq%r14,%rsi#将%rsi设置回*argv movb$10,(%rsi)#第二个参数是";\n";,1);Xorl%eax,%eax#返回值为0 retq#54:返回0;.size main,.-main。

整个源代码都有注释。以数字开头的注释是为了便于与我们的C版本的ECHO交叉引用。

主要技巧是手动内联strlen函数,并且不需要write和_syscall函数,而是能够直接使用syscall。如果需要,我们可以在C代码中内联strlen,但无论如何编译器都很可能会为我们做这件事。我们可能无法避免C中的write和_syscall函数,正如我们前面讨论的那样。

还有一些循环重写以避免使用变量:for(i=0;i<;argc;i++)重写为WHILE(--argc),if(i+1!=argc)变成if(argc!=1),这意味着我们可以去掉inti变量,并通过argv和++argv递增。当然,这些更改也可以应用于C代码。

其他一些技巧包括使用*argv作为空格和换行符的临时空间。一旦我们把*argv写出来,我们实际上并不关心它的价值--我们只用它一次。因此,它变成了空闲的内存空间,我们可以随心所欲地覆盖它(只要合适,我们的空格和换行符都是一个字节,所以我们是安全的)。如果我们愿意,我们也可以用C语言做这个。

如果我们进行这些更改,我们在C中的新Main函数可能如下所示。

Intmain(int argc,char*argv[]){While(--argc){++argv;write(1,*argv,strlen(*argv));if(argc!=1){*argv[0]=';';;write(1,*argv,1);}}*argv[0]=';\n';;write(1,*argv,1);return 0;}

如果让我们的C编译器(clang 10.0.0)将其编译成程序集,我们会得到以下结果。.text.File";echo.c";.globl main#--BEGIN Function main.type main,@functionmain:#@main#%bb.0:Push q%R15 Push q%r14 Push q%R12 movq%rsi,%r14 movl%edi,%r15d Push q$1 popq%r12.LBB0_1:#=>;此循环标题:Depth=1#子循环BB0_3 Depth 2 decl%r15d je.LBB0_6#%bb.2:#In Loop:Header=BB0_1 Depth=1 movq 8(%r14),%rdi addq$8,%r14 leaq-1(%rdi),%rsi.LBB0_3:#父循环BB0_1 Depth=1#=>;此内循环标头:Depth=2 CMPB$0,1(%RSI)leaq 1(%RSI),%RSI jne.LBB0_3#%bb.4:#in Loop:Header=BB0_1 Depth=1 subq%RDI,%RSI Callq Write cmpl$1,%r15d je.LBB0_1#%bb.5:#In Loop:Header=BB0_1 Depth=1 moop%rax movb$10,(%rax)movq(%r14),%rdi push q$1 popq%rsi callq write xorl%eax,%eax popq%r12 popq%r14 popq%r15 retq.Lfunc_end0:.size main,.Lfunc_end0-main#--结束函数.type write,@function#--BEGIN函数写入:#@write#%bb.0:movq%rsi,%rcx movq%rdi,%rdx push q$4 popq.Lfunc_end1-Write#--End函数.Section";.note.GNU-STACK";,";";,@程序位.addrsig。

看来,Cang和我都找到了非常相似的优化程序的方法。根据大小,我比clang做得稍微好一些:我的手工编译的程序集生成了一个104字节的目标文件,而clang生成了一个非常重的125字节的目标文件。考虑到必要的粘合代码(C版本为_start.s、crt、s和_syscall.s)时,我手工编译的程序集结束时为152字节,而clang为197字节。但是clang笑到了最后:在最终的二进制文件上运行ls-l会导致手工编译的程序集使用848字节,而clang使用840字节。因此,很明显,在我的手工编译的程序集中,在引擎盖下面还发生了一些我正在错过的事情。也许你能比我做得更好。

不足为奇的是,一个拥有大型开发团队和更大资金池的生产就绪编译器比我更聪明。但我觉得我做得不错。我相信学习几种不同处理器的汇编是值得的,但我认为我的大部分工作都会坚持使用高级语言(除非你付钱让我做其他事情……)。因为有一种成本我们还没有提到:时间。我花了30秒来优化C代码,而编写汇编版本可能需要半个小时。

我喜欢集合。我教的课程需要深厚的汇编知识。但我认为我会坚持优化高级代码,而不是手工编写汇编。也可能不是。即使是我们最初的C代码也确实足够好。我很高兴生活在一个我们可以选择智能编译器的世界里。