C或C++程式在编译器的作用下,最终都会转为汇编指令,为了能提高程式的执行性能,有时,我们就需要对这些汇编指令进行优化,GNU编译器本身就提供了一些选项参数可以用于优化生成的汇编指令,我们...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第452页到第460页,对应原著第15章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为460 / 577)。

    C或C++程式在编译器的作用下,最终都会转为汇编指令,为了能提高程式的执行性能,有时,我们就需要对这些汇编指令进行优化,GNU编译器本身就提供了一些选项参数可以用于优化生成的汇编指令,我们可以从编译器的优化功能中学习到相关的优化技巧。下面我们就对编译器提供的优化功能进行介绍。

Optimized Compiler Code 编译器提供的优化功能

    在早期的C编程时代,程序员如果想优化他们的程式,就必须一行行的查看生成的汇编指令,然后对需要优化的地方进行手工调整。现在,大部分的编译器都提供了优化功能,GNU编译器也不例外,它提供了一个"-O"的编译选项,该选项目前提供了三种可用的优化级别:
  • -O :最基本的优化级别,会采用一些基本的优化方案
  • -O2 :更高的优化级别,在基础的优化方案上,再提供更多的优化方案
  • -O3 :最高级别的优化,在前两种优化方案的基础上,再多应用一些优化功能
    每种优化级别都是一系列具体的优化方案的集合,我们可以在Linux的命令行下输入man gcc来查看GCC提供的优化选项:

$ man gcc
.............................................
.............................................
-O turns on the following optimization flags:

           -fauto-inc-dec -fcprop-registers -fdce -fdefer-pop -fdelayed-branch
           -fdse -fguess-branch-probability -fif-conversion2 -fif-conversion
           -fipa-pure-const -fipa-reference -fmerge-constants
           -fsplit-wide-types -ftree-builtin-call-dce -ftree-ccp -ftree-ch
           -ftree-copyrename -ftree-dce -ftree-dominator-opts -ftree-dse
           -ftree-forwprop -ftree-fre -ftree-phiprop -ftree-sra -ftree-pta
           -ftree-ter -funit-at-a-time
.............................................
-O2 turns on all optimization flags specified by -O.  It also turns
           on the following optimization flags: -fthread-jumps
           -falign-functions  -falign-jumps -falign-loops  -falign-labels
           -fcaller-saves -fcrossjumping -fcse-follow-jumps  -fcse-skip-blocks
           -fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
           -fgcse-lm -finline-small-functions -findirect-inlining -fipa-sra
           -foptimize-sibling-calls -fpeephole2 -fregmove -freorder-blocks
           -freorder-functions -frerun-cse-after-loop -fsched-interblock
           -fsched-spec -fschedule-insns  -fschedule-insns2 -fstrict-aliasing
           -fstrict-overflow -ftree-switch-conversion -ftree-pre -ftree-vrp
.............................................
-O3 Optimize yet more.  -O3 turns on all optimizations specified by -O2
           and also turns on the -finline-functions, -funswitch-loops,
           -fpredictive-commoning, -fgcse-after-reload, -ftree-vectorize and
           -fipa-cp-clone options.
.............................................
.............................................
$


    从上面的输出可以看到,在我的4.5.2版本的GCC编译器里,-O选项会打开-fauto-inc-dec -fcprop-registers -fdce -fdefer-pop -fdelayed-branch等的优化参数,其中的每个优化参数都对应一个优化方案,这些优化参数的含义有的也可以在man gcc命令的输出中查看到:

$ man gcc
.............................................
.............................................
-fif-conversion
           Attempt to transform conditional jumps into branch-less
           equivalents.  This include use of conditional moves, min, max, set
           flags and abs instructions, and some tricks doable by standard
           arithmetics.  The use of conditional execution on chips where it is
           available is controlled by "if-conversion2".
.............................................
-fif-conversion2
           Use conditional execution (where available) to transform
           conditional jumps into branch-less equivalents.
.............................................
-fauto-inc-dec
           Combine increments or decrements of addresses with memory accesses.
           This pass is always skipped on architectures that do not have
           instructions to support this.  Enabled by default at -O and higher
           on architectures that support this.
.............................................
-fcprop-registers
           After register allocation and post-register allocation instruction
           splitting, we perform a copy-propagation pass to try to reduce
           scheduling dependencies and occasionally eliminate the copy.
.............................................
-fdce
           Perform dead code elimination (DCE) on RTL.  Enabled by default at
           -O and higher.
.............................................
-fdse
           Perform dead store elimination (DSE) on RTL.  Enabled by default at
           -O and higher.
.............................................
-fdelayed-branch
           If supported for the target machine, attempt to reorder
           instructions to exploit instruction slots available after delayed
           branch instructions.
.............................................
-fmerge-constants
           Attempt to merge identical constants (string constants and floating
           point constants) across compilation units.

           This option is the default for optimized compilation if the
           assembler and linker support it.  Use -fno-merge-constants to
           inhibit this behavior.
.............................................
.............................................
$


    上面显示了各个优化参数的英文说明,当然,这些英文说明都有点深奥,我只能对我理解的优化参数进行说明:
  • -fif-conversion:在一个C或C++的应用程式里,除了循环语句外,最耗时的就是if-then之类的条件选择语句,因为一个简单的if-then条件选择语句可以生成很多的条件跳转指令,这些条件跳转指令会增大处理器的开销,让处理器的指令缓存经常失效,使用该优化参数后,编译器会尽可能的减少或消除这些条件跳转指令,并尽量用CMOV之类的条件传值指令来代替,再加上一些算法技巧,编译器就可以最大限度的减少if-then语句生成的汇编指令所消耗的执行时间。
  • -fif-conversion2:该优化参数对应的优化方案里,采用了更先进的数学算法,来减少if-then语句会生成的条件跳转指令。
  • -fdelayed-branch:该优化技术会尝试根据指令的执行周期来对指令进行重新排序,并在条件跳转指令之前,尽可能的移动很多指令,以便能充分发挥出处理器的指令缓存的功能。
  • -fmerge-constants:使用了该优化参数后,编译器会尝试去合并相同的常量,不过,合并相同的常量会增加编译时间,因为编译器必须去分析C或C++程式里用过的每个常量,并对这些常量进行判断比较。
  • -fdefer-pop:这个优化参数在man gcc命令里没找到具体的解释,这里的解释来源于汇编教程的英文原著。通常情况下,由于函数的输入参数存储在栈里,所以,每当函数返回时,就会立即将栈里的输入参数给弹出栈,使用了该优化参数后,函数返回时并不会马上将参数弹出栈,而是在一个适当的时间,将多个函数在栈里残留的参数一次给清理掉(可以通过将ESP栈顶指针的值简单的加上某个数来释放掉栈里的数据)。当然这么做的弊端在于,由于栈里的堆叠的旧数据较多,容易发生栈溢出。
  • -fguess-branch-probability:该优化参数的解释也来源于汇编教程的英文原著,使用了该优化参数后,编译器会去预测条件跳转指令的结果,并根据预测的结果来相应的移动和调整一些指令,让处理器的缓存功能可以发挥作用,不过由于这种预测是在编译时进行的,每次预测的结果都不一定相同,所以同一段C或C++代码在使用该优化参数后,所生成的汇编指令可能会不同。所以,很多程序员不喜欢使用该优化功能,并且会通过–fno-guess-branch-probability选项来关闭掉它。
  • -fcprop-registers:在函数内部,变量的值通常是存储在寄存器里来进行拷贝传值等操作的,使用该优化参数后,编译器就会对寄存器的拷贝传值操作进行相关的优化。

-O2对应的优化级别:

    -O2第二个级别的优化选项,是在-O的基础上开启了一些额外的优化参数,例如:-fthread-jumps, -falign-functions, -falign-jumps, -falign-loops等。

    下面我们就对-O2额外开启的优化功能进行介绍,当然我也只能对我理解的优化参数进行介绍:
  • -fthread-jumps:有时候,某个跳转指令可能会跳到另一个跳转指令,再由该跳转指令跳到目的地,有的时候,可能还要经过多次跳转才能到达目的地,使用了该优化参数后,编译器就会自动检测这些多次跳转的最终目的地,然后将第一次的跳转指令直接指向最终的目的地。
  • -foptimize-sibling-calls:使用该优化参数后,递归函数调用可以展开成一串指令,处理器的缓存功能就可以将这串指令缓存执行,这比用跳转指令一个个的执行函数调用要快很多。
  • -fgcse:该优化功能会在生成的所有汇编指令上执行Global Common Subexpression Elimination (gcse)即全局公共子表达式消除操作,该操作会将公共的部分进行合并,同时消除冗余的代码段,不过需要注意的是,如果应用程式使用了computed gotos(一个GCC的扩展功能)的话,最好使用-fno-gcse参数来关闭掉gcse,因为这样可以获得更好的执行性能。
  • -fcse-follow-jumps:Common Subexpression Elimination(cse)即公共子表达式消除技术,会对跳转指令进行扫描,将不会到达的跳转部分消除掉,例如if...else语句,如果else部分不会被执行的话,cse就会进入else里,将else部分的指令消除掉。
  • -frerun-cse-after-loop:在循环代码被优化后,再对这段优化后的循环体执行一次Common Subexpression Elimination(cse)即公共子表达式消除操作,这样可以让循环代码得到进一步的优化。
  • -fdelete-null-pointer-checks:通过全局的数据流分析来识别和删除所有对空指针的检测操作,编译器假定对空指针的解引用会造成程序的终止。但因为有些环境下,这一结论并不一定成立,因此编译器增加了-fno-delete-null-pointer-checks参数来禁用掉该优化。
  • -fexpensive-optimizations:该优化参数会执行一系列的优化技术,这些优化技术是比较消耗编译时间的,不过,却可以提高程序的运行性能。
  • -fregmove:使用了该优化参数后,编译器会尝试对MOV指令所使用的寄存器进行重新分配,并将某些寄存器用于其他的指令,以最大限度的提高寄存器的使用率。
  • -fschedule-insns:编译器会尝试调整指令的顺序,以充分利用处理器等待数据的时间,例如,当处理器在执行和浮点相关的运算时,由于浮点运算得出结果需要一些时间,处理器就可以利用这段时间来加载执行其他的指令。
  • -fcaller-saves:如果使用该优化参数,那么编译器就会让多个函数调用的寄存器的保存与恢复工作放在一次执行完,不用像常规那样,每个函数调用都执行一次寄存器的保存与恢复操作,这样就可以减少指令的执行时间。
  • -freorder-blocks:该优化方案会使指令块被重新排序,以减少分支跳转指令的数目,优化指令位置,从而提高指令的运行效率。
  • -falign-functions:该优化技术可以让函数按照指定的边界来进行对齐,例如,可以让整个函数按照4K页来对齐,由于大多数处理器是以页的方式来访问内存的,将函数按页对齐后,就可以改善性能。
  • -falign-loops:类似于-falign-functions,可以让循环体的代码按照4K页来对齐,从而提高执行效率。

-O3对应的优化级别:

    -O3的优化级别除了会开启前两个优化级别里的优化功能外,还会开启一些额外的优化功能,例如:-finline-functions, -funswitch-loops, -fpredictive-commoning, -fgcse-after-reload, -ftree-vectorize等。

    下面也只对其中的部分优化功能做个简单的介绍。
  • -finline-functions:将所有简单的函数整合进调用他们的函数中。编译器会自动试探并决定哪些函数值得被整合,虽然这么做会增大可执行文件的体积,但是却可以提升程式的执行性能,因为它可以充分发挥指令缓存的作用,不用每次都通过跳转指令来跳转到被调用的函数里。
  • -fweb:建立经常使用的缓存器网络。提供更佳的缓存器使用率。不过也会增加除错的困难度。这是一个比较偏向实验性质的选项,虽然建议你开启,但是若开启之后程序变得不稳定的话,可以使用-fno-web参数将其禁用掉。
  • -fgcse-after-reload:该技术会在重新加载已生成的和已优化过的汇编指令后,再执行一次gcse的优化操作,这样有助于消除不同的优化过程所产生的冗余的部分。
    上面是对GCC的三种优化级别的简单介绍,下面就通过实例来看下这些优化级别所产生的具体效果。

Creating Optimized Code 创建优化过的汇编指令

    要查看一段C或C++代码是否被优化过了,首先就必须知道编译器会生成怎样的汇编指令,下面就先介绍如何查看编译器生成的汇编指令的方法。

Generating the assembly language code 输出汇编指令:

    GCC编译器提供了一个-S选项,该选项会生成一个汇编文件,并将编译器可能会生成的汇编指令都写入到该文件里,通过查看该文件就可以清楚的知道编译器会生成哪些指令。

    例如,下面的tempconv.c程式:

/* tempconv.c - An example for viewing assembly source code */
#include <stdio.h>

float convert(int deg)
{
	float result;
	result = (deg - 32.) / 1.8;
	return result;
}

int main()
{
	int i = 0;
	float result;
	printf(" Temperature Conversion Chart\n");
	printf("Fahrenheit Celsius\n");
	for(i = 0; i < 230; i = i + 10)
	{
		result = convert(i);
		printf(" %d => %5.2f\n", i, result);
	}
	return 0;
}


    上面的代码中,先定义了一个convert函数,该函数可以将输入的华氏温度转成摄氏温度,并将转换后的摄氏温度进行返回,华氏(F)转摄氏(C)的公式为:C = (F-32)/1.8,所以上面的convert函数里,对应的转换语句就是result = (deg - 32.) / 1.8 。

    我们可以使用gcc -S命令来查看编译器没经过优化时,会生成的汇编代码:

$ gcc -S tempconv.c
$ ls

tempconv.c  tempconv.s
$

    可以看到,gcc -S命令会创建一个tempconv.s的文件,该文件里包含了gcc编译器生成的汇编代码,由于上面没有使用-O选项,所以这些汇编代码是没有经过优化过的,tempconv.s里的内容如下:

$ cat tempconv.s
	.file	"tempconv.c"
	.text
.globl convert
	.type	convert, @function
convert:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	fildl	8(%ebp)
	fldl	.LC0
	fsubrp	%st, %st(1)
	fldl	.LC1
	fdivrp	%st, %st(1)
	fstps	-4(%ebp)
	movl	-4(%ebp), %eax
	movl	%eax, -24(%ebp)
	flds	-24(%ebp)
	leave
	ret
	.size	convert, .-convert
	.section	.rodata
.LC3:
	.string	" Temperature Conversion Chart"
.LC4:
	.string	"Fahrenheit Celsius"
.LC5:
	.string	" %d => %5.2f\n"
	.text
.globl main
	.type	main, @function
main:
	leal	4(%esp), %ecx
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%ecx
	subl	$20, %esp
	movl	$0, -12(%ebp)
	subl	$12, %esp
	pushl	$.LC3
	call	puts
	addl	$16, %esp
	subl	$12, %esp
	pushl	$.LC4
	call	puts
	addl	$16, %esp
	movl	$0, -12(%ebp)
	jmp	.L3
.L4:
	subl	$12, %esp
	pushl	-12(%ebp)
	call	convert
	addl	$16, %esp
	fstps	-16(%ebp)
	flds	-16(%ebp)
	movl	$.LC5, %eax
	leal	-8(%esp), %esp
	fstpl	(%esp)
	pushl	-12(%ebp)
	pushl	%eax
	call	printf
	addl	$16, %esp
	addl	$10, -12(%ebp)
.L3:
	cmpl	$229, -12(%ebp)
	jle	.L4
	movl	$0, %eax
	movl	-4(%ebp), %ecx
	leave
	leal	-4(%ecx), %esp
	ret
	.size	main, .-main
	.section	.rodata
	.align 8
.LC0:
	.long	0
	.long	1077936128
	.align 8
.LC1:
	.long	-858993459
	.long	1073532108
	.ident	"GCC: (GNU) 4.5.2"
	.section	.note.GNU-stack,"",@progbits
$


    由于convert函数比较简单,所以我们就以convert函数来做分析,先看该函数没被优化时生成的汇编指令,再看优化后生成的汇编指令,两者进行比较,就可以清楚的知道编译器做了哪些优化工作。

    上面的输出显示,没经过优化前,convert函数对应的汇编代码如下:

convert:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$24, %esp
	fildl	8(%ebp)
	fldl	.LC0
	fsubrp	%st, %st(1)
	fldl	.LC1
	fdivrp	%st, %st(1)
	fstps	-4(%ebp)
	movl	-4(%ebp), %eax
	movl	%eax, -24(%ebp)
	flds	-24(%ebp)
	leave
	ret


    上面汇编代码里,先通过fildl 8(%ebp)指令将输入参数即华氏温度压入ST0,再通过fldl .LC0指令将C = (F-32)/1.8公式里的32压入ST0,之前压入栈的输入参数就会自动进入ST1,然后通过fsubrp %st, %st(1)指令用ST1里的输入参数即华氏温度减去ST0里的32,得到的结果先存储在ST1里,又因fsubrp会弹出ST0,弹出后,原来的ST1就会变为新的ST0,所以(F-32)的结果就会存储在ST0里,接着fldl .LC1指令会将公式里的1.8压入ST0,同时(F-32)的结果自动移入ST1,再通过fdivrp %st, %st(1)指令,就可以将ST1除以ST0,结果先存储在ST1里,在弹出ST0后,(F-32)/1.8的最终结果就会自动移入ST0里了。

    上面的汇编代码里,有关fsubrp与fdivrp指令的用法可以参考之前的"高级数学运算 (二) 基础浮点运算"章节里的内容。

    下面我们可以给GCC编译器添加一个-O3的优化选项,然后再看下convert函数对应的汇编代码:

$ gcc -O3 -S -o tempconv2.s tempconv.c 
$ cat tempconv2.s 
......................................
......................................
convert:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$4, %esp
	flds	.LC0
	fisubrl	8(%ebp)
	fdivl	.LC1
	fstps	-4(%ebp)
	flds	-4(%ebp)
	leave
	ret
......................................
......................................
$


    上面汇编输出里显示,在将.LC0对应的32通过flds指令加载到ST0后,直接使用fisubrl 8(%ebp)指令将整数类型的输入参数减去ST0里的32,结果存储在ST0里,最后通过fdivl .LC1指令用ST0里的值除以LC1里的1.8,就可以得到(F-32)/1.8的结果,与之前没优化过的汇编代码相比,移除了不必要的汇编指令,直接两步就算出了结果。

    当然-O3的优化还并不彻底,fdivl后面的fstps与flds这两条指令完全没必要,所以你还可以手动对tempconv2.s进行修改,将不必要的fstps与flds指令给删除掉。

    在你手动修改了tempconv2.s后,你可以通过下面的命令来生成最终的可执行程式:

$ gcc -o tempconv2 tempconv2.s
$ ./tempconv2
 Temperature Conversion Chart
Fahrenheit Celsius
 0 => -17.78
 10 => -12.22
 20 => -6.67
 30 => -1.11
 40 =>  4.44
.............................
.............................
$ 


    上面直接用gcc编译器,就可以将优化过的tempconv2.s生成为tempconv2的可执行文件,该可执行文件里就包含了优化过的指令。

    限于篇幅,本章就到这里,下一篇继续介绍一些和优化技巧相关的东东。

    OK,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 优化汇编指令 (二)

上一篇: 调用汇编模块里的函数 (三) 静态库、共享库、本章结束篇

相关文章

高级数学运算 (二) 基础浮点运算

汇编流程控制 (二) 条件分支及汇编循环

什么是汇编语言(二) 高级语言与汇编

调用汇编模块里的函数 (二)

使用IA-32平台提供的高级功能 (一)

汇编字符串操作 (一)