《高性能科学与工程计算》——2.4 编译器作用

2.4 编译器作用

通过利用编译器自动优化,高性能计算程序可以获得不同程度的性能改进。几乎每个现代编译器都可以在命令行上设置编译选项,以便对编译器优化目标程序进行细粒度控制。有些情况下可以简单地通过更换一个编译器来检查程序是否还存在性能提升空间。编译器需要进行复杂的工作以将高级代码编写成的源程序编译为机器代码,同时要顾及到处理器内部资源。本章和下一章讨论的一些优化方法可以在某些简单情况下被编译器实现,但是涉及复杂的情况时就无法用编译器自动完成优化工作。始终要注意的一点是编译器可能足够聪明但是又可能非常蠢笨。在讨论编译器能力时,一条常用评价是“编译器应该能够识别”,这经常是一个错误的假设。
参考文献[C91]概述了几种当前常用C/C++编译器的优化能力,并介绍了一些手工优化的技巧和指导。
2.4.1 通用优化选项
每种编译器都提供了一组标准优化选项(-O0,-O1,…),每个级别的优化都包括哪些优化方法并没有固定标准,需要参考相关手册。但是,所有编译器在-O0选项级别禁止大多数优化,因此这是调试和分析程序的正确方法,在更高级别的优化层次上,编译器进行检测和消除冗余变量、重排算术表达式等优化,因此调试器不能在代码和数据间提供一致的视图。
需要注意的是,某些问题只在高级优化层次上才出现。这可能是编译器的错误或缺陷,也可能是典型的错误,例如数组访问越界(读写索引超过了数组的界限),在-O0层次和-O3层次,数据被按照不同的方式组织,因此可能只在某个优化层次出错。这种错误很难被定位,有些情况下由于与优化器的冲突,甚至最常用的printf函数也不能帮助定位该类错误。
2.4.2 内联
内联通过插入被调用函数或子程序的全部代码来减少程序运行时的调用开销。例如每个被调用函数都会使用寄存器或者栈(具体要以来参数数量和调用方式决定)中资源来存储和传递函数参数,内联确实移除了将参数压入栈中的必要,并且使编译器可以在它认为需要的时候使用寄存器(而不是根据某些调用约定),从而减轻了寄存器压力,寄存器压力是指CPU没有足够的寄存器存储复杂计算或者循环体内的所有操作数(更多介绍见第2.4.5节)。最后,内联使得编译器可见的代码段增大并可以利用更多的在非内联情况下不可用的优化手段。程序员不应该依赖编译器优化内联代码,在性能关键段(例如循环体内),编译器看不见“真正的”代码反而无法优化。
函数调用是否会影响性能依赖于调用次数,通常情况下,内联频繁调用的小程序将会获得最大的性能加速。在C++代码中,内联是提高性能的基本方法,因为对简单数据类型的重载操作将会变为较小的函数,并且当内联函数返回一个对象时,临时复制可以被省略(更多C++优化细节见2.5节)。
编译器通常有不同的编译选项来控制内联的自动优化程度,例如,在什么程度上(即代码行数量)一个子程序可以作为一个被内联的候选对象等。注意C99和C++ inline关键字只是对编译器的一个提示,应该检查编译器日志(如果可用,见2.4.6节)判断函数是否真正被内联。
相反,在多处内联一个函数可能会增大目标代码而导致过量使用L1指令高速缓存,例如如果循环体内的指令不能存储在L1指令高速缓存中,将会与数据传输一起竞争更高层次的高速缓存或者主内存,因此读取指令延迟就会增大。所以在设置内联标识时需要考虑正反两个方面的效用。
2.4.3 别名
通过程序语言规则和对源代码的理解,编译器需要提出确切的假设来限制自身产生优化机器代码的能力。典型的例子是在C(以及C++)语言中利用指针(或引用)形式参数:

假设被指针a和b指向的内存区域不重叠,即[a,a+n-1]与[b,b+n-1]不重叠,那么该循环体中的读取和存储操作就可以按照任意顺序重排,编译器会应用它认为合适的任何软件流水方式或者展开循环并将读取和存储打包在一个程序块中,就像下面伪码(忽略其余循环):

此时循环可以简单地进行SIMD–向量化优化(见2.3.3节)。
然而,C和C++标准允许指针的别名,所以在优化时不能假设两个指针指向的区域不重叠,例如,如果a==b,该循环变成了1.2.3节的“真依赖”的Fortran实例,读取和存储的执行顺序必须与程序中声明的一致:

编译器在缺少更多信息的情况下必须按照这种方式生成代码,SIMD向量化优化也必须被排除,处理器硬件在某些限制条件下允许读取和存储的重排[V104,V105],但是必须保证程序语义。
在Fortran标准中禁止参数别名,这也是Fortran程序比相同的C程序快的主要原因之一。所有的C/C++编译器都有控制别名程度的命令行选项(例如Intel编译器中的-fno-fnalias选项和GCC中的-fargument-noalias,表明任何函数的两个指针实参都不指向同一区域)。如果编译器被告知没有参数别名,那么原则上就可以进行类似Fortran代码中的优化。但是应该注意,如果对优化过后的程序使用参数别名将会产生错误结果。
2.4.4 计算准确性
2.3.1节已经提到,当要求满足结合律时,编译器有时禁止重排算术表达式,除非极高层次的优化开关被打开。原因在于浮点运算不满足结合律[135]:如果a、b和c是有限精度的浮点数,则(a+b)+c一般不等于a+(b+c)。如果必须保证优化前代码的正确性,就不能使用结合律规则,因此应由程序员确定是否可以手工重组算术表达式。现代编译器都有相关编译选项用来控制算术表达式的重组,即使是在高层次的优化开关被打开的情况下。
同时要注意非正规数,即比用非零最高有效位表示的最小值还小的浮点数,会极大地影响计算性能,如果可能并且轻微的正确性损失是可接受的,那么这些数应该在硬件计算中被设为零。
2.4.5 寄存器优化
这是编译器优化(考虑使用寄存器)中最关键也是最复杂的任务之一,编译器试图将寄存器分配给使用最频繁的操作数并将这些操作数尽可能长的保留在寄存器中(如果这样做安全)。例如,如果一个变量的地址被访问,该变量的值很可能被改变(通过地址操作由程序其他部分改变),这种情况下编译器要决定是否将该变量写回内存中。
内联(见2.4.2节)可以减轻寄存器优化负担,因为编译器可以将本应该在函数调用前写入内存并随后再读出的变量保存在寄存器中。相反,优化拥有大量变量和算术表达式的循环体(可能出现在内联优化后)对于编译器来说非常困难,因为编译器要保持的操作数数目过大而不能在一次迭代中同时存储所有操作数。前面提到过,处理器中整数寄存器和浮点数寄存器的数量通常是有限的。目前典型的数目为8~128,如果寄存器数量不够,将会带来寄存器溢出,即将寄存器变量写回内存以供后续使用。如果程序的性能瓶颈是算术操作,那么寄存器溢出将会极大地降低性能。这种情况下可以划分一个大循环为多个循环来减轻寄存器使用压力。
一些处理器具备硬件支持可以处理寄存器溢出,例如Intel Itanium2处理器具有硬件性能计数器可以直接检测存储器溢出。
2.4.6 利用编译日志
前面几节指出编译器在编写高效程序中的关键作用。可以简单地操作以阻止编译器获得重要信息从而限制优化的级别和种类。为了更好地利用编译器的智能,需要编译器允许产生注释源代码表或者编译日志来表示本次编译过程都使用了哪些优化方法。代码清单2-1展示了一个MIPS R14000处理器上(已过时)编译注释的例子,即代码清单1-1中的标准三元组向量程序。该处理器是一个四路超标量处理器,在一个时钟周期内可以同时执行一个读取或存储操作,两个整数操作,一个浮点加和一个浮点乘操作(后两个操作通过一条融合的乘–加指令“madd”实现)。假设所有的数据都存储在最高层的高速缓存中,编译器可以计算程序一次循环迭代所需的最小计算周期数(第3行)。4~9行展示了处理器峰值,即每类指令的最大吞吐量。
代码清单2-1 流水化三元组软件的编译日表。其中“峰值”(peak)指该体系结构(MIPS R14000)上各种操作类型的最大执行速率

除此之外,寄存器利用和寄存器溢出(第11行和第12行),循环展开因子和软件流水因子(第2行,见1.2.3节和3.5节)、SIMD指令的使用(见2.3.3节)以及循环次数的编译器假定(第1行)都在表中展示,可以用来判断生成的机器代码的质量。然而,并不是所有编译器都有产生如此丰富的代码标注特性的能力,剩下的工作需要程序员完成。
也有人工检查汇编代码的编译选项。所有编译器提供命令行选项以输出汇编而不是可连接文件。然而,将该汇编文件与源代码进行对应并分析指令序列的效率需要很多经验[O55]。毕竟这是摒弃编写汇编语言代码的原因。

时间: 2024-10-03 13:34:03

《高性能科学与工程计算》——2.4 编译器作用的相关文章

《高性能科学与工程计算》——1.2 基于高速缓存的通用微处理器体系结构

1.2 基于高速缓存的通用微处理器体系结构 微处理器可能是人类发明的最复杂的机器.然而,就像前面章节中描述的那样,它们都基于存储程序数字计算机的概念.对于科学家而言,理解CPU所有内部工作细节是不可能的,也是不必要的,尽管把握其高级特性对于了解潜在的性能瓶颈是有帮助的.图1-2展示了现代基于高速缓存的通用微处理器的简图.对于一个运行的程序,真正执行计算的部分是仅占芯片一小部分的浮点型(FP)和整型(INT)计算单元.其他逻辑控制单元用来向计算单元提供操作数.一般将CPU寄存器区分为浮点数和整数两

《高性能科学与工程计算》——2.5 C++优化

2.5 C++优化 目前,有大量关于如何编写高效C++代码的文献[C92,C93, C94, C95].我们的目标不是取代它们.所以我们特意忽略了引用计数.写时复制.智能指针等关键技术.本节以循环代码为例,根据我们的经验指出C++编程中经常存在的性能错误和误解. C++编程存在着一个根深蒂固的假象:编译器应该能够识别高级C++程序包含的所有抽象和代码混淆.首先,C++是一门支持复杂管理的高级编程语言,且自身特征明显(如运算符重载.面向对象.自动构建/销毁等).然而,这些特征绝大多数都不适合编写高

《高性能科学与工程计算》——1.6 向量处理器

1.6 向量处理器 从Cray 1超级计算机开始,直到基于RISC的高度并行计算机出现之前,向量机一直占据着科学计算的主要领域.在写这本书时,只有两家公司还在制造和销售向量机.但因为对内存带宽和运行时间有高度需求,向量机还是有着一个充满商机的市场. 根据设计,对于合适的可向量化的代码,向量处理器相较于标准的微处理器可以达到一个较好的实际性能.这种设计遵循单指令多数据(SIMD)的范例,即一条简单的机器指令被自动地应用于很多类型相同的参数.许多现代的基于cache的微处理器以扩展SISD指令集的形

《高性能科学与工程计算》——第1章 当代处理器1.1 存储程序的计算机体系结构

第1章 当代处理器 在1975-1995年的"旧时代"的科学计算时期,先进的高性能系统是专门为HPC市场设计的,主要的厂商有Cray.CDC.NEC.Fujitsu和Thinking Machines等.在性能和价格方面,这些系统远远超越了标准的"商品"电脑.20世纪70年代初发明的单芯片通用微处理器,是20世纪80年代末唯一足够成熟.可以打入HPC市场的技术.直到20世纪90年代末,标准的工作站集群甚至基于PC的硬件至少在理论峰值性能上才具备相应的竞争力.如今,情

《高性能科学与工程计算》——1.3 存储层次

1.3 存储层次 数据以不同的方式存储在计算机系统中.前面章节提到,CPU有一组可以不延时访问的寄存器.另外,有一个或几个容量小但存取速度快的cache,用以保存最近被访问过的数据项副本.主存要慢得多,但是容量比cache大很多.最后,数据能够存放在磁盘上,在需要的时候再复制到主存中.这是一个复杂的层次结构,为了搞清楚它的性能瓶颈,理解数据在不同层次之间的传递原则是至关重要的.下面我们将集中描述从CPU到主存的各个层次(见图1-3).1.3.1 高速缓存 高速缓存是低容量.高速度的存储器,通常集

《高性能科学与工程计算》——2.2 优化常识

2.2 优化常识 简单的代码修改经常会带来性能的显著提升.下面的章节总结了避免性能缺陷的几个最重要的"优化常识".这些方法看似微不足道,但许多科学应用程序在应用这些方法后,性能都有了显著提升.2.2.1 少做工作 重新组织代码以减少代码工作量,在很多情况下可显著提升性能.最常见的例子是循环检测一组对象是否具有特定属性,任一对象具备该属性即可: 如果complex_func()函数没有其他作用,FLAG是唯一和循环外部通信的变量.这种情况下,FLAG值一经改变,就立即退出循环可明显减少计

《高性能科学与工程计算》——第3章 数据访存优化3.1 平衡分析与lightspeed评估

第3章 数据访存优化 在高性能计算中,访存是最重要的性能限制因素.如前所述,微处理器的理论峰值性能和访存带宽存在固有的"不平衡性".因为很多科学和工程应用程序由需要大量数据传输的基于循环的代码构成,所以相对较低的内存(甚至是硬盘)访存带宽,就会导致片上资源的低效利用和程序性能的降低.图3-1综合显示了现代并行计算机系统的数据通路构成,以及在不同层次上的带宽和延迟范围.执行计算任务的功能部件位于该层次结构的顶部.在这些不同层次的数据通路中,访存带宽最大有3-4个数量级的差异,访存延迟最高

《高性能科学与工程计算》——第2章 串行代码基本优化技术2.1 标量剖析

第2章 串行代码基本优化技术 在千核级并行计算机时代,有些观点认为编写高效串行代码在许多领域已经有些过时了.因为增加更多CPU以获得大规模并行能力要比投入大量精力优化串行代码简单得多.这似乎是一个合理的理论,5.3.8节的论述中也体现了对这种观点的支持.然而,本书认为程序在单处理器上的性能优化毫无疑问是最重要的,如果通过一些简单的优化方法就可以实现两倍加速比,那么用户会更倾向于使用较少的CPU.这样不仅可把宝贵的计算资源释放给其他用户或项目,而且还可以使投入大量资金购买的硬件获得更加有效的利用.

《高性能科学与工程计算》——3.4 案例分析:稠密矩阵转置

3.4 案例分析:稠密矩阵转置 在下面的实例分析中,假定矩阵按列存储.计算一个稠密矩阵的转置(A = BT),根据循环的组织顺序,矩阵A或者B会有一个矩阵的访存是非连续的.矩阵转置最不幸的实现方式如下: 矩阵A的写入操作是非连续的(见图3-7).由于写分配操作的影响,非连续写比非连续读的代价要大得多.从这个最坏的代码出发,我们尝试获得期望性能.由于矩阵转置不执行任何算术操作,因此我们使用有效带宽(即应用程序达到的GB/s)来表示性能. 图3-7 vanilla矩阵转置中的cache行遍历(非连续