一、优化编译器代码
编译器的-O选项系列提供了GNU编译器的优化步骤。每个步骤都提供更高级别的优化。当前优
化可用的有3个级别:
- -O:提供基础级别的优化
- -O2:提供更加高级的代码优化
- -O3:提供最高级别的优化
不同的优化级别使用的优化技术也可以单独地应用于代码。可以使用-f命令行选项引用每个单独
的优化技术。-O选项和各种-f选项捆绑在一起构成单一选项。
1、编译器优化级别1
在优化的第一个级别执行基础代码优化。在这个级别试图执行9种单独的优化功能。使用”试
图“这个词是因为不能保证任何优化功能肯定实现,编译器只是试图执行它们而已。下列清单介绍
这个级别包含的-f优化功能:
- -fdefer-pop:这种优化技术与汇编语言代码在函数完成时如何进行操作有关。一般情况下,函数的输入值被存放到堆栈中并且被函数访问。函数返回时,输入值还是在堆栈中。一般情况下,函数返回之后,输入值被立即弹出堆栈。这个选项允许编译器跨越函数调用,使输入值累积在堆栈中。然后使用单一指令一次把所有这些累积的输入值删除(通常通过把堆栈指针改动为适当的值完成)。对大多数操作,这是完全合法的,因为新的函数的输入值被存放到堆栈中旧的输入值的顶部。但是,这样使堆栈中的内容有些杂乱。
- -fmerge-constants:使用这种优化技术,编译器试图合并相同的常量。这一特性有时候导致很长的编译时间,因为编译器必须分析C或者C++程序中用到的每个常量,并且相互比较它们。
- -fthread-jumps:这种优化技术与编译器如何处理汇编代码中的条件和非条件分支有关。在某些情况下,一条跳转指令可能转移到另一条分支语句。通过一连串跳转,编译器确定多个跳转之间的最终目标并且把第一个跳转重新定向到最终目标。
- -floop-optimize:通过优化如何生成汇编语言中的循环,编译器可以在很大程度上提高应用程序的性能。通常,程序由很多大型且复杂的循环构成。通过删除在循环内没有改变值的变量赋值操作,可以减少循环内执行的指令的数量,在很大程度上提高性能。此外,优化那些确定合适离开循环的条件分支,以便减少分支的影响。
- -fif-conversion:if-then语句是应用程序中仅次于循环的最消耗时间的部分。简单的if-then语句可能在最终的汇编语言代码中生产众多条件分支。通过减少或者删除条件分支,以及使用条件传送、设置标志和使用运算技巧替换它们,编译器可以减少if-then语句中花费的时间量。
- -fif-conversion2:这种技术结合更高级的数学特性,减少实现if-then语句所需的条件分支。
- -fdelayd-branch:这种技术试图根据指令周期时间重新安排指令。它还试图把尽可能多的指令移动到条件分支之前,以便最充分地利用处理器指令缓存。
- -fguess-branch-probability:就像其名称所暗示的,这种技术试图确定条件分治最可能的结果,并且地移动指令,这和延迟分支(delay-branch)技术类似。因为是在编译时预测代码的安排,所以使用这一选项两次编译相同的C或者C++代码很可能会产生不同的汇编语言源代码,这取决于在编译时编译器认为会使用哪些分支。因为这个原因,很多程序员不喜欢采用这个特性,并且专门地使用-fno-guess-branch-probability选项关闭这个特性。
- -fcprop-register:因为在函数中把寄存器分配给变量,所以编译器执行第二次检查以便减少调用依赖性(两个段要求使用相同的寄存器)并且删除不必要的寄存器复制操作。
2、编译器优化级别2
优化代码的第二个级别(-O2)结合了第一个级别的所有优化技术,再加上很多其他技术。
这些计数涉及更特定类型的代码,比如循环和条件分支。如果编译器生成的基础汇编语言代码没
有利用这个级别中分析的代码类型,就不会执行附加的优化。下面的清单描述这个级别试图执行
的附加的-f选项。
- -fforce-mem:这种优化在任何指令使用变量之前,强制把存放在内存位置中的所有变量都复制到寄存器中。对于只涉及单一指令的变量,这样也许不会有很大的优化效果。但是,对于在很多指令(比如数学操作)中都涉及到的变量来说,这会是很显著的优化,因为和访问内存中的值相比,处理器访问寄存器的值要快得多。
- -foptimize-sibling-calls:这种技术处理相关的和/或递归的函数调用。通常,递归的函数调用可以被展开为一系列一般的命令,而不是使用分支。这样使处理器的指令缓存能够加载展开的指令并且处理它们,和指令保持为需要分支操作的单独函数调用相比,这样更快。
- -fstrength-reduce:这种优化技术对循环执行优化并且删除迭代变量。迭代变量是捆绑到循环计数器的变量,比如使用变量、然后使用循环计数器变量执行数学操作的for-next循环。
- -fgcse:这种技术对生成的所有汇编语言代码执行全局通用子表达式消除(Global Common Subexpression Elimination,gcse)例程。这些优化操作试图分析生成的汇编语言代码并且组合通用片段,消除冗余的代码段。注意,如果代码使用计算性的goto,gcc指令推荐使用-fno-gcse。
- -fcse-follow-jumps:这种特别的通用子表达式消除(Common Subexpression Elimination,cse)技术扫描跳转指令,查找程序中通过任何其他途径都不会到达的目标代码。这种情况最常见的例子就是if-then-else语句的else部分。
- -freturn-cse-after-loop:这种技术在对任何循环已经进行过优之后重新运行通用子表达式消除例程。这样确保在展开循环代码之后更进一步地优化循环代码。
- -fdelete-null-pointer-checks:这种优化技术扫描生成的汇编语言代码,查找检查空指针的代码。编译器假设间接引用空指针将停止程序。如果在间接引用之后检查指针,它就不可能为空。
- -fexpensive-optimization:这种技术执行从编译时的角度来说代码高昂的各种优化技术,但是它可能对运行时的性能产生负面影响。
- -fregmove:编译器试图重新分配MOV指令中使用的寄存器,并且将其作为其他指令的操作数,以便最大化捆绑的寄存器的数量。
- -fschedule-insns:编译器将试图重新安排指令,以便消除等待数据的处理器。对于在进行浮点运算时有延迟的处理器来说,这使处理器在等待浮点结果时可以加载其他指令。
- -fsched-interblock:这种技术使编译器能够跨越指令块调度指令。这可以非常灵活地移动指令以便使等待期间完成的工作最大化。
- -fcaller-saves:这个选项指示编译器针对函数调用保存和恢复寄存器,使函数能够访问寄存器值,而且不必保存和恢复它们。如果调用多个函数,这样能够节省时间,因为只进行一次寄存器的保存和恢复操作,而不是在每个函数调用中都进行。
- -fpeephole2:这个选项允许进行任何计算机待定的观察孔优化。
- -freorder-blocks:这种优化技术允许重新安排指令块以便改进分支操作和代码局部性。
- -fstrict-aliasing:这种技术强制实行高级语言的严格变量规则。对于C和C++程序来说,它确保不在数据之间共享变量。例如,整数变量不和单精度浮点变量使用相同的内存位置。
- -funit-at-a-time:这种优化技术指示编译器在运行优化例程之前读取整个汇编语言代码。这使编译器可以重新安排不消耗大量时间的代码以便优化指令缓存。但是,这会在编译时花费相当多的内存,对于小型计算机可能是个问题。
- -falign-function:这个选项用于使函数对准内存中特定边界的开始位置。大多数处理器按照页面读取内存,并且确保全部函数代码位于单一页面之内能够改进性能。如果函数跨页面,为了完成函数就必须处理内存的另一个页面。
- -falign-loops:和对准函数类似,在内存中的页面边界内对准包含多次被处理的代码的循环是有好处的。处理循环时,如果它包含在单一内存页面内,就不需要交换代码所需的页面。
- -fcrossjumping:这是对跨越跳转代码的处理,以便组合分散在程序各处的相同代码。这样可以减少的长度,但是也许不会对程序性能有直接影响。
3、编译器优化级别3
使用-O3选项访问编译器提供的最高级别的优化。它整合了第一和第二级别中的所有优化技
术,还有一些非常专门的附加优化技术。再次说明,不能保证这个级别的优化将改进最终代码的
性能。下面是这个级别包含的-f优化选项:
- -finline-functions:这种优化技术不为函数创建单独的汇编语言代码,而是把函数代码包含在占用程序的代码中。对于多次被调用的函数来说,为每次函数调用复制函数代码。虽然这样对减少代码长度不利,但是通过最充分地利用指令缓存代码,而不是在每次函数调用时进行分支操作,可以提高性能。
- -fweb:构建用于保存变量的伪寄存器网络。伪寄存器包含数据,就像它们是寄存器一样,但是可以使用各种其他优化技术进行优化,比如cse和loop优化技术。
- -fgcse-after-reload:这种技术在完全重新加载生成的且优化后的汇编语言代码之后执行第二次gcse优化,帮助消除不同哟化方式创建的任何冗余段。
二、创建优化的代码
可以使用gcc编译器从C和C++程序创建优化后的汇编语言代码。默认情况下,优化后的代码被编译为目标文件并且连接为可执行文件。
1、生成汇编语言代码
GNU编译器的-S选项创建一个文件,这个文件包含从高级语言源代码生成的汇编语言代码。
#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;
}
生成的汇编语言代码如下:
.file "tempconv.c"
.text
.globl convert
.type convert, @function
convert:
.LFB0:
.cfi_startproc #“以.cfi”开头的伪指令是辅助汇编器创建栈帧(stack frame)信息
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $28, %esp # 前面为一般C样式函数开头设置堆栈的基指针
movl 8(%ebp), %eax
subl $32, %eax
movl %eax, -24(%ebp)
fildl -24(%ebp) # 从堆栈读取输入值并且把它加载到FPU堆栈中
fldl .LC0 # 把数字型常量结果(reg - 32)加载到FPU堆栈中
fdivrp %st, %st(1) #执行除法步骤
fstps -4(%ebp)
movl -4(%ebp), %eax
movl %eax, -28(%ebp)
flds -28(%ebp)
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size convert, .-convert
.section .rodata
.align 4
.LC2:
.string " Temperature Conversion Chart"
.LC3:
.string "Fahrenheit Celsius"
.LC4:
.string " %d %5.2f\n"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $0, 24(%esp)
movl $.LC2, (%esp)
call puts
movl $.LC3, (%esp)
call puts
movl $0, 24(%esp)
jmp .L4
.L5:
movl 24(%esp), %eax
movl %eax, (%esp)
call convert
fstps 28(%esp)
flds 28(%esp)
fstpl 8(%esp)
movl 24(%esp), %eax
movl %eax, 4(%esp)
movl $.LC4, (%esp)
call printf
addl $10, 24(%esp)
.L4:
cmpl $229, 24(%esp)
jle .L5
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size main, .-main
.section .rodata
.align 8
.LC0:
.long -858993459
.long 1073532108
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
2、查看优化的代码
这里用GNU编译器的-O3选项以便执行所有优化操作。
convert:
.LFB24:
.cfi_startproc
subl $12, %esp
.cfi_def_cfa_offset 16
movl 16(%esp), %eax
subl $32, %eax
movl %eax, (%esp)
fildl (%esp)
fdivl .LC0
fstps 4(%esp)
flds 4(%esp)
addl $12, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
优化后诶有使用FLD指令单独加载常量值。而是在数学指令中直接使用了它们。
三、优化技巧
本节演示汇编语言中使用的5种最为常用的优化方法:
- 优化运算
- 优化变量
- 优化循环
- 优化条件分支
- 优化通用子表达式
1、优化运算
处理方程式时,几乎总有机会可以简化一些运算。有时候,为了在使用涉及到的变量时显示
方程式流程,按照未经简化的形式把这些运算输入到C或者C++源码中。还有些时候,缺乏经验的
程序员输入了混乱的源代码。
1)未经优化的运算
#include <stdio.h>
int main()
{
int a = 10;
int b, c;
a = a + 15;
b = a + 200;
c = a + b;
printf( "The result is %d\n", c );
return 0;
}
.file "calctest.c"
.section .rodata
.LC0:
.string "The result is %d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $10, 20(%esp)
addl $15, 20(%esp)
movl 20(%esp), %eax
addl $200, %eax
movl %eax, 24(%esp)
movl 24(%esp), %eax
movl 20(%esp), %edx
addl %edx, %eax
movl %eax, 28(%esp)
movl 28(%esp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
编译器生成的汇编语言代码使用标准C样式的函数格式创建程序。编译器保留24个字节用于局
部变量的存储,并且使用这个位置引用下面3个程序变量:
程序变量 | 堆栈存储位置 |
a | 20(%esp) |
b | 24(%esp) |
c | 28(%esp) |
然后,编译器生成适当的汇编语言代码对所有局部变量执行好的定义的运算:
movl $10, 20(%esp)
addl $15, 20(%esp)
movl 20(%esp), %eax
addl $200, %eax
movl %eax, 24(%esp)
movl 24(%esp), %eax
movl 20(%esp), %edx
addl %edx, %eax
movl %eax, 28(%esp)
2)查看优化后的运算
为了生成优化后的汇编语言代码,可以使用下面的代码:
.file "calctest.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "The result is %d\n"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB24:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $250, 8(%esp)
movl $.LC0, 4(%esp)
movl $1, (%esp)
call __printf_chk
xorl %eax, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE24:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
可以看到代码长度大大减少,代码中没有使用局部变量。执行用来确定变量a、b和c的值的
运算的所有代码都被删除了。因为程序中的其他位置没有使用任何变量值,唯一被计算出来的值
就是最终结果。编译器计算处最终结果并在汇编代码中供函数printf中直接使用。程序不需要在每
次运算时计算这些值。
2、优化变量
优化应用程序的最明显的途径之一是控制汇编语言程序如何处理变量。处理变量有3种方式:
- 使用.data或者.bss段在内存中定义变量
- 使用EBP基指针在堆栈中定义局部变量
- 使用可用的寄存器保存变量值
1)未经优化的全局和局部变量
很多C和C++程序员并不了解在它们的程序中使用全局变量和局部变量的影响。对于大多数程
序员来说,这仅仅是在程序中的什么位置声明变量的问题。但是,生成汇编语言代码时,如何处
理变量有巨大的区别。
下面程序演示把C程序转换为汇编语言代码时如何创建变量:
#include <stdio.h>
int global1 = 10;
float global2 = 20.25;
int main()
{
int local1 = 100;
float local2 = 200.25;
int result1 = global1 + local1;
float result2 = global2 + local2;
printf( "The result are %d and %f\n", result1, result2 );
return 0;
}
.file "vartest.c"
.globl global1 #gloabal1定义为全局变量
.data
.align 4
.type global1, @object
.size global1, 4
global1:
.long 10
.globl global2 #gloabal1定义为全局变量
.align 4
.type global2, @object
.size global2, 4
global2:
.long 1101135872
.section .rodata
.LC1:
.string "The result are %d and %f\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $100, 16(%esp) # 把第一个局部变量存储在16(%esp)
movl .LC0, %eax
movl %eax, 20(%esp) # 把第二个局部变量存储在20(%esp)
movl global1, %edx
movl 16(%esp), %eax
addl %edx, %eax
movl %eax, 24(%esp) # 把整数运算结果保存在24(%esp)
flds global2
fadds 20(%esp)
fstps 28(%esp)
flds 28(%esp)
fstpl 8(%esp)
movl 24(%esp), %eax
movl %eax, 4(%esp)
movl $.LC1, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.section .rodata
.align 4
.LC0:
.long 1128808448
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
2)经过优化的全局和局部变量
.file "vartest.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC1:
.string "The result are %d and %f\n"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB24:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl global1, %eax
flds .LC0
fadds global2
movl $.LC1, 4(%esp)
addl $100, %eax
movl %eax, 8(%esp)
movl $1, (%esp)
fstpl 12(%esp)
call __printf_chk # format and print data, with stack checking
xorl %eax, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE24:
.size main, .-main
.globl global2
.data
.align 4
.type global2, @object
.size global2, 4
global2:
.long 1101135872
.globl global1
.align 4
.type global1, @object
.size global1, 4
global1:
.long 10
.section .rodata.cst4,"aM",@progbits,4
.align 4
.LC0:
.long 1128808448
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
优化后没有在堆栈中建立局部变量,而是直接把局部变量的值传送到数学指令中。这样可以节
约大量时间。
3、优化循环
循环可能是应用程序中最为消耗时间的一部分。
1)一般的for-next循环代码
在汇编中实现for-next循环的伪代码如下:
for:
<condition to evaluate for loop counter value>
jxx forcode ; jump to the code of the condition is true
jmp end ; jump to the end if the condition is false
forcode:
< for loop code to excute >
<increment for loop counter>
jmp for ; go back to the start of the For statement
end:
这段代码需要使用3个分支语句来实现for-next循环。汇编语言代码中的分支对性能的影响可
能是灾难性的,因为它们使预加载到指令缓存中的指令完全失去了利用价值。
优化循环的最好方式是要么消除它们,要么至少试图简化它们,使用的手段是循环展开。展
开循环需要非常多的额外代码(不使用分支重复执行代码,而是按照循环应该处理的次数编写代
码)。虽然这样的代码不能减少程序的长度,但是确保指令预取缓存能够完成其工作并且预先加
载代码。性能提高也许能够超过程序更大而导致的代价,但也许不能够做到。
2)查看循环代码
#include <stdio.h>
int sums( int i )
{
int j, sum = 0;
for( j = 1; j <= i; j++ )
{
sum = sum + j;
}
return sum;
}
int main()
{
int i = 10;
printf( "Value: %d Sum: %d\n", i, sums( i ) );
return 0;
}
.file "sums.c"
.text
.globl sums
.type sums, @function
sums:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
movl $0, -4(%ebp)
movl $1, -8(%ebp)
jmp .L2
.L3:
movl -8(%ebp), %eax
addl %eax, -4(%ebp)
addl $1, -8(%ebp)
.L2:
movl -8(%ebp), %eax
cmpl 8(%ebp), %eax
jle .L3
movl -4(%ebp), %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size sums, .-sums
.section .rodata
.LC0:
.string "Value: %d Sum: %d\n"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $10, 28(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call sums
movl %eax, 8(%esp)
movl 28(%esp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
3)优化for-next循环
查看优化后的代码:
.file "sums.c"
.text
.p2align 4,,15
.globl sums
.type sums, @function
sums:
.LFB24:
.cfi_startproc
movl 4(%esp), %ecx
testl %ecx, %ecx
jle .L4
addl $1, %ecx
xorl %eax, %eax
movl $1, %edx
.p2align 4,,7
.p2align 3
.L3:
addl %edx, %eax
addl $1, %edx
cmpl %ecx, %edx
jne .L3
rep ret
.L4:
xorl %eax, %eax
ret
.cfi_endproc
.LFE24:
.size sums, .-sums
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "Value: %d Sum: %d\n"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB25:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $55, 12(%esp)
movl $10, 8(%esp)
movl $.LC0, 4(%esp)
movl $1, (%esp)
call __printf_chk
xorl %eax, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE25:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
4、优化条件分支
优化汇编语言程序的另一问题是优化条件分支。和循环类似,条件分支能够破坏预加载到指令
缓存中的指令,如果采用了没有预测到的分支,就会导致处理器进行额外的工作。
条件分支的主要应用之一是在if-then类型的语句中。C和C++程序包含众多的if-then语句用于
评估程序代码的条件,并且根据这些条件处理数据,这种情况并不少见。
1)普通的if-then代码
下面是汇编语言代码中实现if-then逻辑的一般方案:
if:
<condition to evalute>
jxx else ; jump to the else part if the condition is false
<code to implement the "then" statements>
jmp end ; jump to the end
else:
< code to implement the "else" statements >
end:
标准的if-then模板利用一个条件跳转和一个非条件跳转实现代码逻辑。
2)查看if-then代码
#include <stdio.h>
int conditiontest( int test1, int test2 )
{
int result;
if( test1 > test2 )
{
result = test1;
}
else if( test1 < test2 )
{
result = test2;
}
else
{
result = 0;
}
return result;
}
int main()
{
int data1 = 10;
int data2 = 30;
printf( "The result is %d\n", conditiontest( data1, data2 ) );
return 0;
}
.file "condtest.c"
.text
.globl conditiontest
.type conditiontest, @function
conditiontest:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $16, %esp
movl 8(%ebp), %eax # 把第一个输入值加载到EAX寄存器
cmpl 12(%ebp), %eax # 比较第二个输入值与第一个输入值的大小
jle .L2 # 如果第一个输入值小于或等于第二个输入值,则跳转到.L2
movl 8(%ebp), %eax # 否则把第一个输入值加载到EAX寄存器
movl %eax, -4(%ebp) # 储存到-4(%ebp)
jmp .L3
.L2:
movl 8(%ebp), %eax
cmpl 12(%ebp), %eax
jge .L4 # 第一个输入值等于第二个,则跳转到.L4
movl 12(%ebp), %eax
movl %eax, -4(%ebp)
jmp .L3 # 第一个输入值小于第二个,则跳转到.L3
.L4:
movl $0, -4(%ebp)
.L3:
movl -4(%ebp), %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size conditiontest, .-conditiontest
.section .rodata
.LC0:
.string "The result is %d\n"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
movl $10, 24(%esp) # 加载第一个输入值
movl $30, 28(%esp) # 加载第二个输入值
movl 28(%esp), %eax
movl %eax, 4(%esp) # 把第二个输入值加载到4(%esp)
movl 24(%esp), %eax
movl %eax, (%esp) # 把第一个输入值加载到(%esp)
call conditiontest
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
.align指令确保数据元素对准适当的内存边界,以便提高把值加载到寄存器(特别是FPU寄存
器)中的速度。指令.p2align和它类似,但是对如何对准元素提供更多的控制。
指令.p2align的格式如下:
.p2align number, value, max
/*参数number定义地址中必须为0的低位0位的位数。4这个值说明内存地址
必须是16的倍数(低4位必须为0)。
参数value定义将在填充字节中使用的值。如果跳过这个值,则在填充字节中使用0。
参数max定义对准指令应该跳过的最大字节数量。7表示对准下一段应该跳过的字节
数量不应该超过7字节,否则就会忽略指令.p2align*/
3)经过优化的if-then代码
可以是用-O3选项获得优化后的版本。
.file "condtest.c"
.text
.p2align 4,,15
.globl conditiontest
.type conditiontest, @function
conditiontest:
.LFB24:
.cfi_startproc
movl 4(%esp), %eax
movl 8(%esp), %edx # 把两个输入值加载到寄存器
cmpl %edx, %eax
jg .L2
movl $0, %eax
cmovl %edx, %eax
.L2:
rep ret
.cfi_endproc
.LFE24:
.size conditiontest, .-conditiontest
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "The result is %d\n"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB25:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $30, 8(%esp)
movl $.LC0, 4(%esp)
movl $1, (%esp)
call __printf_chk
xorl %eax, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE25:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
5、通用子表达式消除
编译器执行的更加高级的优化技术之一是通用子表达式消除(common subexpression
elimination,cse)。编译器必须扫描整个汇编语言代码,查找通用表达式。找到经常使用的表达
式时,就只需要计算表达式一次;之后,可以在用到这个表达式的所有其他位置使用结果值。
1)程序范例
#include <stdio.h>
void funct1( int a, int b )
{
int c = a * b;
int d = ( a * b ) / 5;
int e = 500 / ( a * b );
printf( "The results are c=%d d=%d e=%d\n", c, d, e );
}
int main()
{
int a = 10;
int b = 25;
funct1( a, b );
funct1( 20, 10 );
return 0;
}
funct1:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl %ebx
subl $36, %esp
.cfi_offset 3, -12
movl 8(%ebp), %eax
imull 12(%ebp), %eax
movl %eax, -20(%ebp)
movl 8(%ebp), %eax
imull 12(%ebp), %eax
movl %eax, %ecx
movl $1717986919, %edx
movl %ecx, %eax
imull %edx
sarl %edx
movl %ecx, %eax
sarl $31, %eax
subl %eax, %edx
movl %edx, %eax
movl %eax, -16(%ebp)
movl 8(%ebp), %eax
imull 12(%ebp), %eax
movl %eax, %ebx
movl $500, %eax
cltd
idivl %ebx
movl %eax, -12(%ebp)
movl -12(%ebp), %eax
movl %eax, 12(%esp)
movl -16(%ebp), %eax
movl %eax, 8(%esp)
movl -20(%ebp), %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
addl $36, %esp
popl %ebx
.cfi_restore 3
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
函数funct1包含3个方程式,每个方程式都包含相同的表达式a*b。未经优化的函数代码版本在
遇到这3个方程式时执行3次乘法。
2)使用cse进行优化
funct1:
.LFB24:
.cfi_startproc
subl $44, %esp
.cfi_def_cfa_offset 48
movl $500, %eax
movl 52(%esp), %ecx
cltd
imull 48(%esp), %ecx
movl $.LC0, 4(%esp)
movl $1, (%esp)
idivl %ecx
movl $1717986919, %edx
movl %ecx, 8(%esp)
movl %eax, 16(%esp)
movl %ecx, %eax
imull %edx
movl %ecx, %eax
sarl $31, %eax
sarl %edx
subl %eax, %edx
movl %edx, 12(%esp)
call __printf_chk
addl $44, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
优化后只执行了一次乘法操作。