C++性能优化笔记-7-编译器中的优化-5-编译器做了什么

编译器中的优化

检查编译器做了什么

研究编译器产生的代码,看它优化代码的程度,是非常有用的。有时,编译器会相当奇妙地使代码更高效,有时它蠢得难以置信。看编译器输出通常可以透露出什么可以通过修改源代码来改进,如下面例子所示。

检查编译器产生代码最好的方式是使用汇编语言输出的编译器选项。在大多数编译器上,你可以通过从命令行调用编译器来完成并使用相关优化选项,以及汇编输出的选项-S/Fa。如果编译器没有汇编输出选项,使用目标文件反汇编器。

注意,Intel编译器有在汇编输出中注释源代码的选项(/Fas-fsource-asm)。这个选项使汇编输出可读性更好,但不幸的是,它阻止了某些优化。如果你希望看到完全优化的结果,不要使用源代码注释选项。

在调试器的反汇编窗口看编译器产生代码也是可能的。不过,你在调试器中看到的代码不是优化的,因为调试选项阻止了优化。调试器不能在完全优化代码中设置断点,因为它没有行号信息。使用中断3的内联汇编指令,在代码中插入一个固定的断点,通常是可能的。代码是__asm int 3;或者__asm(“int 3”);或者__debugbreak();。如果你在调试器中运行优化代码(发布版本),它将在中断3断点处暂停,并显示汇编,可能没有函数名及变量名的信息。记住删除中断3断点。

下面的例子展示了编译器汇编输出看起来像什么,及你如何可以使用它来改进代码。

// Example 8.26a
void Func(int a[], int & r) {
     int i;
     for (i = 0; i < 100; i++) {
          a[i] = r + i/2;
     }
}

对例子8.26a,Intel编译器产生下面的汇编代码(32位模式):

; Example 8.26a compiled to assembly:
ALIGN 4															; align by 4
PUBLIC ?Func@@YAXQAHAAH@Z										; mangled function name
?Func@@YAXQAHAAH@Z PROC NEAR    								; start of Func
; parameter 1: 8 + esp											; a
; parameter 2: 12 + esp                                       	; r
$B1$1:                                                         	; unused label
               push ebx                                        	; save ebx on stack
               mov ecx, DWORD PTR [esp+8]						; ecx = a
               xor eax, eax                                     ; eax = i = 0
               mov edx, DWORD PTR [esp+12]                      ; edx = r
$B1$2:                                                          ; top of loop
               mov ebx, eax                                 	; compute i/2 in ebx
               shr ebx, 31                                         ; shift down sign bit of i
               add ebx, eax                                     ; i + sign(i)
               sar ebx, 1                                        	; shift right = divide by 2
               add ebx, DWORD PTR [edx]           				; add what r points to
               mov DWORD PTR[ecx+eax*4], ebx                	; store result in array
               add eax, 1                                          ; i++
               cmp eax, 100                                     ; check if i < 100
               jl $B1$2                                              ; repeat loop if true
$B1$3:                                                               ; unused label
               pop ebx                                               ; restore ebx from stack
               ret                                                        ; return
               ALIGN 4                                              ; align
?Func@@YAXQAHAAH@Z ENDP                   						; mark end of procedure

编译器产生大多数注释被替换了。名字?Func@@YAXQAHAAH@ZFunc的名字,带有关于函数类型与其参数的信息。这称为名字重整。名字重整的细节在手册5《不同C++编译器他操作系统的调用惯例》中解释。参数ar分别在栈上地址esp+8esp+12处传递,载入ecxedx。(在64位模式中,参数在寄存器中传递,而不是在栈上)。现在ecx包含数组a第一个元素的地址,edx包含r指向变量的地址。在汇编代码中,引用与指针相同。寄存器ebx在使用前被压入栈,在函数返回前从栈弹出。这是因为寄存器使用惯例,函数不允许改变ebx的值。仅寄存器eaxecxedx可以自由改变。循环计数器i作为寄存器变量保存在eax中。循环初始化i=0;被翻译为指令xor eax, eax。这是寄存器置零的一个常用方式,比mov eax, 0更高效。循环主体在标签$B1S2:处开始。这只是编译器为该标签选择的一个随意名字。它把ebx用作计算i/2+r的临时寄存器。指令mov ebx, eax / shr ebx, 31i的符号位拷贝到ebx的最低有效位。下两条指令add ebx, eax / sar ebx, 1ebxi,并右移一位把i2。指令add ebx, DWORD PTR [edx]ebx加上地址在edx的变量,而不是edx。中括号表示把edx中的值用作内存指针。这是r指向的变量。现在,ebx包含i/2+r。下一条指令mov DWORD PRT [ecx+eax*4], ebx将这个结果保存在a[i]中。注意数组地址的计算是如何做到高效。ecx包含该数组的起始地址。eax保存索引i。这个索引必须乘上每个数组元素的大小(字节),计算元素i的地址。int的大小是4。因此数组元素a[i]的地址是ecx+eax*4。然后,结果ebx保存在地址[ecx+eax*4]处。这都在一条指令里完成。CPU支持这种指令,用于快速访问数组元素。指令add eax, 1是循环递增i++cmp eax, 100 / jl $B1$2是循环条件i < 100。它将eax100比较,如果i < 100,跳回标签$B1$2pop ebx恢复一开始保存的ebx值。ret从函数返回。

汇编列表揭示了可以进一步优化的3件事情。我们注意到的第一件是,为了i/2,对i的符号位做了一些有趣的处理。编译器没有注意到i不会是负数。把i声明为unsigned int,或者在除2之前类型转换到unsigned int,可以通知编译器。

我们注意到的第二件事情是,r指向的值从内存重新载入了一百次。这是因为我们忘记告诉编译器假设没有指针别名。添加编译器选项“假设没有指针别名”(如果有效),可能改进代码。

可以改进的第三件事情是,可以通过归纳变量计算r+i/2,因为它是循环索引的一个阶梯函数。整数除法阻止了编译器使用归纳变量,除非以步进2进行循环展开。

结论是,我们可以通过展开循环,并显示使用一个归纳变量,帮助编译器优化例子8.26b。

欢迎交流
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值