| 网站首页 | 文章中心 | 电子书下载 | 矢量图库 | 视频教程 | 素材下载 | 程序代码下载 | JS代码 | 论坛 | 
常用软件类:
|杀毒安全 |联络聊天 |网络软件 |多媒体类 |系统工具 |图形图像 |系统工具 |应用软件 |行业软件
开发设计类:
|动画制作 |图像处理 |3D设计 |操作系统 |站长学院 |网络相关 |WEB设计 |数据库类 |程序开发
简明x86汇编语言教程(7)编译优化概述
作者:佚名    文章来源:网络    点击数:    更新时间:2006-12-9
 

 

5.3 全局寄存器优化

[待修订内容] 本章中,从这一节开始的所有优化都是在微观层面上的优化了。换言之,这些优化是不能使用高级语言中的对应设施进行解释的。这一部分内容将进行较大规模的修订。

通常,此类优化是由编译器自动完成的。我个人并不推荐真的由人来完成这些工作——这些工作多半是枯燥而重复性的,编译器通常会比人做得更好(没说的,肯定也更快)。但话说回来,使用汇编语言的程序设计人员有责任了解这些内容,因为只有这样才能更好地驾驭处理器。

在前面的几章中我已经提到过,寄存器的速度要比内存快。因此,在使用寄存器方面,编译器一般会做一种称为全局寄存器优化的优化。

例如,在我们的程序中使用了4个变量:i, j, k, l。它们都作为循环变量使用:

for(i=0; i<1000; i++){
  for(j=0; j<1000; j++){
    for(k=0; k<1000; k++){
      for(l=0; l<1000; l++)
        do_something(i, j, k, l);
    }
  }
}

这段程序的优化就不那么简单了。显然,按照通常的压栈方法,i, j, k, l应该按照某个顺序被压进堆栈,然后调用do_something(),然后函数做了一些事情之后返回。问题在于,无论如何压栈,这些东西大概都得进内存(不可否认某些机器可以用CPU的Cache做这件事情,但Cache是写通式的和回写式的又会造成一些性能上的差异)。

聪明的读者马上就会指出,我们不是可以在定义do_something()的时候加上inline修饰符,让它在本地展开吗?没错,本地展开以增加代码量为代价换取性能,但这只是问题的一半。编译器尽管完成了本地展开,但它仍然需要做许多额外的工作。因为寄存器只有那么有限的几个,而我们却有这么多的循环变量。

把四个变量按照它们在循环中使用的频率排序,并决定在do_something()块中的优先顺序(放入寄存器中的优先顺序)是一个解决方案。很明显,我们可以按照l, k, j, i的顺序(从高到低,因为l将被进行1000*1000*1000*1000次运算!)来排列,但在实际的问题中,事情往往没有这么简单,因为你不知道do_something()中做的到底是什么。而且,凭什么就以for(l=0; l<1000; l++)作为优化的分界点呢?如果do_something()中还有循环怎么办?

如此复杂的计算问题交给计算机来做通常会有比较满意的结果。一般说来,编译器能够对程序中变量的使用进行更全面地估计,因此,它分配寄存器的结果有时虽然让人费解,但却是最优的(因为计算机能够进行大量的重复计算,并找到最好的方法;而人做这件事相对来讲比较困难)。

编译器在许多时候能够作出相当让人满意的结果。考虑以下的代码:

int a=0;

for(int i=1; i<10; i++)
  for(int j=1; j<100; j++){
    a += (i*j);
  }

让我们把它变为某种形式的中间代码:

00: 0 -> a
01: 1 -> i
02: 1 -> j
03: i*j -> t
04: a+t -> a
05: j+1 -> j
06: evaluate j < 100
07: TRUE? goto 03
08: i+1 -> i
09: evaluate i < 10
10: TRUE? goto 02
11: [继续执行程序的其余部分]

程序中执行强度最大的无疑是03到05这一段,涉及的需要写入的变量包括a, j;需要读出的变量是i。不过,最终的编译结果大大出乎我们的意料。下面是某种优化模式下Visual C++ 6.0编译器生成的代码(我做了一些修改):

xor eax, eax               ; a=0(eax: a)
mov edx, 1                 ; i=1(edx: i)
push esi                   ; 保存esi(最后要恢复,esi作为代替j的那个循环变量)
nexti:
mov ecx, edx               ; [t=i]
mov esi, 999               ; esi=999: 此处修改了原程序的语义,但仍为1000次循环。
nextj:
add eax, ecx               ; [a+=t]
add ecx, edx               ; [t+=i]
dec esi                    ; j--
jne SHORT nextj            ; jne 等价于 jnz. [如果还需要,则再次循环]
inc edx                    ; i++
cmp edx, 10                ; i与10比较
jl SHORT nexti             ; i < 10, 再次循
pop esi                    ; 恢复esi

这段代码可能有些令人费解。主要是因为它不仅使用了大量寄存器,而且还包括了5.2节中曾提到的子表达式提取技术。表面上看,多引入的那个变量(t)增加了计算时间,但要注意,这个t不仅不会降低程序的执行效率,相反还会让它变得更快!因为同样得到了计算结果(本质上,i*j即是第j次累加i的值),但这个结果不仅用到了上次运算的结果,而且还省去了乘法(很显然计算机计算加法要比计算乘法快)。

这里可能会有人问,为什么要从999循环到0,而不是按照程序中写的那样从0循环到999呢?这个问题和汇编语言中的取址有关。在下两节中我将提到这方面的内容。

5.4 x86体系结构上的并行最大化和指令封包

考虑这样的问题,我和两个同伴现在在山里,远处有一口井,我们带着一口锅,身边是树林;身上的饮用水已经喝光了,此处允许砍柴和使用明火(当然我们不想引起火灾:),需要烧一锅水,应该怎么样呢?

一种方案是,三个人一起搭灶,一起砍柴,一起打水,一起把水烧开。

另一种方案是,一个人搭灶,此时另一个人去砍柴,第三个人打水,然后把水烧开。

这两种方案画出图来是这样:

仅仅这样很难说明两个方案孰优孰劣,因为我们并不明确三个人一起打水、一起砍柴、一起搭灶的效率更高,还是分别作效率更高(通常的想法,一起做也许效率会更高)。但假如说,三个人一个只会搭灶,一个只会砍柴,一个只会打水(当然是说这三件事情),那么,方案2的效率就会搞一些了。

在现实生活中,某个人拥有专长是比较普遍的情况;在设计计算机硬件的时候则更是如此。你不可能指望加法器不做任何改动就能去做移位甚至整数乘法,然而我们注意到,串行执行的程序不可能在同一时刻同时用到处理器的所有功能,因此,我们(很自然地)会希望有一些指令并行地执行,以充分利用CPU的计算资源。

CPU执行一条指令的过程基本上可以分为下面几个阶段:取指令、取数据、计算、保存数据。假设这4个阶段各需要1个时钟周期,那么,只要资源够用,并且4条指令之间不存在串行关系(换言之这些指令的执行先后次序不影响最终结果,或者,更严格地说,没有任何一条指令依赖其他指令的运算结果)指令也可以像下面这样执行:

指令1 取指令 取数据 计 算 存数据      
指令2   取指令 取数据 计 算 存数据    
指令3     取指令 取数据 计 算 存数据  
指令4       取指令 取数据 计 算 存数据

这样,原本需要16个时钟周期才能够完成的任务就可以在7个时钟周期内完成,时间缩短了一半还多。如果考虑灰色的那些方格(这些方格可以被4条指令以外的其他指令使用,只要没有串行关系或冲突),那么,如此执行对于性能的提升将是相当可观的(此时,CPU的所有部件都得到了充分利用)。

当然,作为程序来说,真正做到这样是相当理想化的情况。实际的程序中很难做到彻底的并行化。假设CPU能够支持4条指令同时执行,并且,每条指令都是等周期长度的4周期指令,那么,程序需要保证同一时刻先后发射的4条指令都能够并行执行,相互之间没有关联,这通常是不太可能的。

最新的Intel Pentium 4-XEON处理器,以及Intel Northwood Pentium 4都提供了一种被称为超线程(Hyper-Threading TM)的技术。该技术通过在一个处理器中封装两组执行机构来提高指令并行度,并依靠操作系统的调度来进一步提升系统的整体效率。

由于线程机制是与操作系统密切相关的,因此,在本文的这一部分中不可能做更为深入地探讨。在后续的章节中,我将介绍Win32、FreeBSD 5.x以及Linux中提供的内核级线程机制(这三种操作系统都支持SMP及超线程技术,并且以线程作为调度单位)在汇编语言中的使用方法。

关于线程的讨论就此打住,因为它更多地依赖于操作系统,并且,无论如何,操作系统的线程调度需要更大的开销并且,到目前为止,真正使用支持超线程的CPU,并且使用相应操作系统的人是非常少的。因此,我们需要关心的实际上还是同一执行序列中的并发执行和指令封包。不过,令人遗憾的是,实际上在这方面编译器做的几乎是肯定要比人好,因此,你需要做的只是开启相应的优化;如果你的编译器不支持这样的特性,那么就把它扔掉……据我所知,目前在Intel平台上指令封包方面做的最好的是Intel的C++编译器,经过Intel编译器编译的代码的性能令人惊异地高,甚至在AMD公司推出的兼容处理器上也是如此。

上一页  [1] [2] [3] 下一页


  • 上一篇文章:

  • 下一篇文章: 没有了
  • 相关文章