INTEL C++ COMPILER之常用的编译器选项

提交新文章

2011年12月09日 07:00


4.2 常用的编译器选项



4.2.1 选用编译器选项的基本步骤

编译器的强大的优化功能可以使得用户不需要耗费大量的精力来进行手动的优化,而且也有助于软件的可移植性,同时用户也可以专注于算法的选择和体系结构的设计。

由于编译器优化时可能会改变代码的结构,从而使得执行代码的结构可能无法直接和源代码对应起来,从而使得调试起来相对带来困难。软件开发过程中,代码的正确性是首先要保证的,因此代码调试和优化的第一步不需要进行任何优化,当然这个时候也经常会打开调试选项以便借助于相应的调试工具来寻扎代码中的错误。下列例子中表示不进行优化(/Od),生成调试信息(/Zi),把三个文件进行编译和连接后生成执行文件myprog.exe。

icl /Od /Zi /Fe:myprog.exe myprog1.c myprog2.c myprog3.c

软件的优化一般是试图使得程序的执行速度更快,执行代码大小更小。在软件的代码得到完善并且没有错误之后,大部分应用都应该采用/O2选项来打开编译器的主要优化支持来使得代码的执行速度最快,有的应用可能会采用/O1选项以使得进行速度优化的同时也使得代码的大小不会太大,当然也可以采用/O3选项来进行一些比较激进的优化以获得最大程度的性能提高。 接下来可以尝试使用那些针对特定处理器的优化,比如针对Intel® Core™2 Duo处理器,可以使用/O2 /QxT(或者/QaxT)选项。

代码的性能可以进一步通过过程间优化IPO(/Qipo)和profile导向的优化PGO(/Qprof-gen,/Qprof-use)来提高,通过测试相应的性能指标来判别这些优化是否有效,最后可以通过并行处理的选项(/Qparallel,/Qopenmp)来利用那些多核、多处理器和超线程处理器的能力。

Intel C++ Compiler是一个功能非常强大的编译器,包含很多的编译选项,那么在什么时候需要选择哪些编译选项?编译器选项可以分为自动优化选项、高级优化选项、代码生成、语言和兼容性、编译器诊断、内联控制、过程间优化、Profile导向优化、优化报告、OpenMP和并行处理、浮点数、预处理、输出控制和调试、连接等。下表列出了一些常用的编译器选项,注意其中有些选项是可以关闭的,Intel编译器一般采用在原有的选项后面添加一个-来表示关闭对应的选项,比如/Qipo-表示关闭过程间优化选项。下面我们将针对其中某些常用的选项作进一步的解释,其他一些选项可以参考Intel编译器的用户手册。

Windows

描述

/Od

不进行优化

/O1

优化以使得执行速度最快,但是不进行那些增加代码但是提高速度有限的优化

/O2

缺省,优化以使得执行速度最快

/O3

除了O2的优化外,还包含一些比较激进的优化(不是对所有程序都会提高性能)

-fast

-xP -O3 -ipo -no-prec-div -static

/Qvc8

Visual Studio 2005兼容

/Qlocation,<str>,<dir>

说明str对应的工具(比如linker)所在目录为dir

/Qoption,<str>,<opts>

为str对应的工具传递选项opts

/c

编译成目标代码

/S

编译成汇编代码

/FA[cs]

在编译过程中要求也生成汇编代码,可以通过c和s来说明是否还包含对应的机器代码和源代码

/Fe<file>

给出生成得执行文件的名字或者所在目录

/I<dir>

把dir加入到头文件搜寻目录

/Zi

在目标文件中生成相关调试信息

/Tc<file>

把file看成C源文件来编译

/Tp<file>

把file看成C++源文件来编译

/w

不显示警告信息

/W0

仅仅显示差错信息

/W2

显示警告、差错和说明信息,缺省

/Qip

开启单个文件中的过程间优化

/Qipo[n]

允许多个文件间的过程间优化

/Qipo-separate

在进行IPO时,每个源文件产生一个目标文件

/Qipo-jobs<n>

给出在连接过程中同时运行的job数

/Qipo-C

生成进行了IPO的目标文件ipo_out.obj

/Qipo-S

生成进行了IPO的汇编文件ipo_out.asm

/Qprof-gen[x]

编译时插入相应的代码来收集profile信息

/Qprof-use

采用收集到的profile信息进行优化

/Qfnsplit[-]

是否允许函数分割(/Qprof-use时自动打开)

/Qprof-dir <d>

指定保存profiling输出文件(*.dyn和*.dpi)的目录

/Qprof-file <f>

给出profiling汇集文件的名字

/Qvec-report[n]

控制矢量化的相关信息,n取值为0到5,缺省n=1表示给出矢量化的循环

/Qopt-report[:n]

输出优化报告到标准错误输出stderr,n取值为0到3,缺省为2,表示输出详细程度为中等的报告信息

/Qopt-report-file

优化报告的文件名

/Qopt-report-phase

给出输出哪个阶段的优化报告

/Qopt-report-routine<name>

输出那些包含name的函数的优化报告

/Qx<KWNPTO>

针对特定Intel处理器进行优化

/Qax<KWNPTO>

和/Qx一样针对特定的Intel处理器进行优化,不过也包含可以在任何Intel处理器上执行的代码

/Ob<0-2>

控制内联扩展,/Ob0不进行内联,/Ob1根据源代码的显示说明进行内联,/Ob2由编译器决定是否内联

/Oi[-]

进行或者不进行intrinsic函数的内联

/Qinline-min-size=<n>

控制哪些函数的大小小于n时认为为小函数

/Qinline-max-size=<n>

控制哪些函数的大小大于n时认为为大函数

/Qunroll[n]

进行循环展开的最大次数,n为0时表示不进行循环展开,/Qunroll表示由编译器决定

/Qa[-]

假设没有alias

/Ow[-]

假设在函数内部没有alias,但是函数调用可能有alias

/Qalias-args[-]

函数参数可能有alias

/Qalias-const[-]

指向const的指针不会是指向非const的指针的alias

/Qrestrict[-]

允许restrict关键字

/Qopenmp

允许编译器根据OpenMP directive生成多线程代码

/Qopenmp-report{0|1|2}

控制显示的OpenMP诊断消息的详细程度

/Qparallel

允许为可以并行执行的循环自动生成多线程代码

/Qpar-report{0|1|2}

控制显示循环自动并行化的诊断信息的详细程度

/fp:<name>

控制浮点数的优化选项

/Qpc32,/Qpc64,/Qpc80

内部FPU的精度为24/53/64比特,缺省为64比特

/Qftz[-]

开启或者关闭微小数下溢为0优化

/Zp[n]

给出结构类型的对齐限制,缺省为/Zp16,即16比特对齐。



4.2.2 自动优化选项

Intel编译器的自动优化选项主要是4个,分别是/Od、/O1、/O2、/O3。/Od表示不进行任何优化,这个选项主要用在调试代码的正确性中,另外也可以用于选择只进行某些优化,下面例子中表示只选择进行全局优化和内联优化 icl /Od /Og /Ob1 myprog.c

大多数应用都希望执行得更快,因此/O2是这些应用建议采用的优化级别,其目的是开启那些提供更快的执行速度的优化功能,包括内联、常数传播、copy传播、不用的函数和变量移除、不用代码移除、全局寄存器分配、强度降低、peephole优化、循环展开等。

有些应用除了要求更快的执行速度之外,同时对于代码的大小也有要求,这时可以采用/O1,/O1包含了大部分/O2中为速度而优化的选项,不过为了避免代码量的增加,关闭那些可能会导致代码大小增加较大的选项。比如循环展开、intrinsic内联等。

/O3选项除了包含那些/O2中采用的优化之外,还包含一些对于循环和内存访问的进一步相对比较激进的优化,比如循环展开、复制代码来消除分支、更有效利用缓存而通过填充使得数组占用的空间为2的幂等。由于采用那些比较激进的优化,有的时候其性能可能会比/O2选项的性能要差,因此需要通过比较来确定到底选择哪个选项。

从上面有关/O1、/O2、/O3的描述可以看出,/O1在兼顾速度的同时不希望代码大小太大,因此适合于代码量比较大、主要的执行时间不是在循环体中的应用,比如那些比较大的服务方或者数据库应用。/O2主要是使得执行速度更快,满足大部分的应用的要求,而/O3引入了一些针对循环和内存访问的比较激进的优化,因此适合于那些执行时间集中在循环体中,而且循环体中使用大量的浮点计算或者要处理大量数据的应用。



4.2.3 输出控制

如果不特别指定,编译器会试图把源程序编译成目标代码,然后连接成执行文件。生成的执行文件名可以通过源文件来导出,比如icl. prog..c命令生成的执行文件名为prog.exe,编译器选项/Fe可以用来指定生成的执行文件的名字,比如icl –Femyprog.exe prog.c生成的执行文件名为myprog.exe。有的时候我们可能并不一定每次编译就要从源文件开始编译最后连接生成执行代码,这在有多个源文件时经常会碰到。我们可以通过/c和/S选项来告诉编译器不需要生成最后的执行代码,而是只要把源代码编译生成目标代码或者汇编代码就可以了。有的时候我们可能会要求生成执行代码或者目标代码,但是同时为了调试和优化的方便,希望也能够查看在这个过程中生成的汇编代码,这时可以通过/FA选项来完成。/FAc表示除了汇编代码外还包含对应的机器代码,而/FAs则表示除了汇编代码外还包含对应的源程序,而/FAcs则表示除了汇编代码外还包含对应的机器代码和源程序。比如下面的示例中,编译test.c生成目标代码(.obj),同时生成一个包含了源文件的汇编代码test.asm。

icl /c /FAs test.c

在编译过程中可能会出现编译错误和警告等信息,缺省选项为/W2,表示显示编译过程中的警告、差错和其他信息,你也可以通过/W0或者/w来把所有警告信息的显示关闭,也可以通过/W1要求只显示警告和差错信息。



4.2.4 处理器相关的选项

计算机硬件的发展非常迅速,使用的处理器可能会包含从早期的Pentium 3、Pentium 4到现在的Core 2 Duo处理器,这些处理器有不同的特性,采用的体系结构可能不同,支持不同的单指令多数据流SIMD扩展(包括SSE、SSE2、SSE3、SSSE3和SSE4等)。为了能够获得更好的性能,可以打开和处理器相关的选项来利用处理器的特定特性,这些选项主要包括/Qx[TPONK]和/Qax[TPONK],其中/Qx选项指出生成针对特定处理器的代码,如果包含多个/Qx选项,最后针对的特定处理器是从这些选项中选择一个性能最好的那个处理器。由于只生成针对某个特定处理器的代码,这样那些比该处理器老的机器就由于兼容而可能无法执行,即生成的代码对于硬件有一个最低的要求。

如果希望能够可以针对某几个处理器进行优化,同时也希望代码也可以在其它处理器也能够运行,这时可以采用/Qax选项。和/Qx选项不同,除了生成针对特定处理器优化的代码外,还会包含一个可以运行在其他处理器上的一个相对较慢但是通用的代码。如果有多个/Qax选项,则会分别针对这些处理器生成对应的优化代码。在代码在某个机器上执行时,会检测该机器所采用的处理器,然后跳转到针对该处理器优化的代码执行,或者在找不到匹配的情况下执行那些通用的代码。采用/Qax的好处是生成的代码不仅仅可以在特别优化的处理器上运行,而且也可以在别的处理器上运行,兼容性比较好,但是由于要为每个特别优化的处理器生成代码,并且还要生成一个运行时检测代码来决定执行哪个优化的分支,显然代码量会略有增加,而且由于要判断当前是哪个处理器,执行时间也会受到一些影响。/Qx则只对某个处理器优化,代码量和执行时间会要好一些,但是有最低的硬件限制,兼容性不是太好。这两种选项各有优缺点,需要根据应用要运行的硬件环境的实际情况来选择是采用/Qax还是/Qx选项。

/Qx和/Qax选项中的processor取值为S、T、P、O、N、K,S是为将来的Intel处理器而设计的,支持SSE4扩展和媒体加速指令;T是为Intel® Core™2 Duo处理器家族设计的,可以支持SSSE3、SSE3、SSE2和SSE指令;P是为基于Intel® Mobile和NetBurst 微架构的处理器而设计的,包括Core Duo 处理器、支持SSE3 的Pentium 4 和Xeon 处理器等,可以生成SSE3、SSE2 和SSE 指令;O和P 有些类似,也支持Mobile和NetBurst 体系结构,不过其考虑到可以运行在其他公司生产的支持SSE3指令集的处理器之上,因此P中的有些优化可能不支持;N是为那些支持SSE2的Pentium 4和Xeon处理器而设计;K则为比较早的支持SSE指令集的Pentium III和Xeon处理器而设计的。

Intel编译器对于特定处理器所作的优化主要包括:通过指令选取和调度以使得最终的指令顺序能够在速度和延迟上表现出色;通过利用单指令多数据SIMD指令集进行自动矢量化;通过一些循环优化来提高内存访问的延迟性能。Intel编译器还支持流式存储优化,通过流式存储使得原来要首先写到缓存再写到内存中的数据直接写到内存中,减少了内存写的延迟,同时也减少了缓存污染,原来要被占用的缓存可以空出来给别的数据使用。

考虑下面一个小程序test_Qax.c:

int array_sum(int *array, int len)
{
int i;
int sum=0;
for (i = 0; i< len; i++)
sum += *array++;
return (sum);
}

采用下列命令进行编译:

icl /O2 /QaxT /S test_Qax.c
test_Qax.c(5): (col. 2) remark: LOOP WAS VECTORIZED.
test_Qax.c(2): (col. 1) remark: _array_sum has been targeted for automatic cpu d
ispatch.

编译器报告对于循环采用了自动矢量化来利用SIMD指令集,同时加入了为特定处理器优化的代码。

下面来看看采用上面选项编译后的汇编文件test_Qax.asm,首先是运行时检查,判断当前的CPU是什么来分别调用优化版本_array_sum$J和通用版本:

_array_sum$A:
_array_sum PROC NEAR
$B1$2: ; Preds $B1$1
L1: ;2.1
test DWORD PTR ___intel_cpu_indicator, -512 ;2.1
jne _array_sum$J ;2.1
test DWORD PTR ___intel_cpu_indicator, -1 ;2.1
jne _array_sum$A ;2.1
call ___intel_cpu_indicator_init ;2.1
jmp L1 ; Prob 100% ;2.1

注意到上面的代码中的最后字段为;2.1,这具有什么特殊的意义呢。为了方便程序员对汇编代码和源程序的交叉定位,Intel编译器在每一行汇编指令后面都表明了其在源程序所对应的行号和列号,以上面的代码为例,这段代码表示对应着源程序中的第2行第1列。

$B2$9:                          ; Preds $B2$8
mov edx, DWORD PTR [esp+20]
; ; LOE edx ecx ebx ebp esi edi
$B2$10: ; Preds $B2$9 $B2$20
movd xmm0, esi ;5.2
; LOE edx ecx ebx ebp edi xmm0
$B2$11: ; Preds $B2$11 $B2$10
paddd xmm0, XMMWORD PTR [edx+edi*4] ;6.3
add edi, 4 ;5.2
cmp edi, ecx ;5.2
jb $B2$11 ; Prob 99% ;5.2
; LOE edx ecx ebx ebp edi xmm0

上面的代码给出了进行自动矢量化后的代码,可以看到其采用了SIMD指令padddd,当然在这个过程进行了循环展开,原来要4次循环的加法现在只需要一次循环就可以了。由于长度len可能不一定正好是4的倍数,因此在最后可能需要通过填充来利用SIMD指令,关于SIMD的介绍我们会在第六章中作进一步的详细介绍。

下面这段代码来自于array_sum函数的通用实现部分,可以看到其是通过普通的加法指令来实现的:

$B3$3:                          ; Preds $B3$3 $B3$2
add eax, DWORD PTR [ecx] ;6.3
add ecx, 4 ;6.11
inc edx ;5.22
cmp edx, esi ;5.17
jl $B3$3 ; Prob 82% ;5.17


4.2.5 过程间优化

编译器在源代码一级对于代码的优化一般可以分为4类:

  • 局部(Local)优化:优化局限于代码或者控制块内
  • 全局(Global)优化:优化局限于函数或者过程体内
  • 过程间优化:考虑到不同函数之间的关系来进行优化
  • 完整程序优化:把所有连接成执行文件的这些函数之间的关系考虑在一起来进行优化

Intel编译器进一步把过程间优化分为在单一源程序内的函数之间的优化和不同源程序的函数之间的优化,其实完整程序优化(Whole Program Optimization)也可以算过程间优化,一般是在连接阶段来进行的,不过由于在这时知道执行程序要调用的所有函数,可以进行一些更进一步的优化,比如可以把那些没有用到的函数和变量的代码删除掉,可以作更进一步的alias分析等。

如图4.5所示,过程间优化IPO实际上包括两个阶段:编译和连接阶段,在编译阶段IPO会创建一个信息文件,该文件包含了源文件的中间表示IR(Intermediate Representation)以及优化的汇总信息;在连接阶段IPO通过汇总所有在第一阶段创建的信息文件中包含的所有函数来分析以进行相应的优化,也就是说只有那些也是采用IPO编译的函数才会用来进行IPO第二阶段的优化,其它函数直接传递给连接器作进一步的优化。

图4.5 过程间优化

过程间优化的一个最主要的优化就是内联函数展开,编译器一般在发现一个函数经常使用或者函数比较小的时候,把原来的函数调用直接用对应的函数体来代替,这样减少了函数调用的开销,同时也减少了分支的个数。通过过程间优化,还可以把那些经常要访问的数据或者使用函数放在一起,进行数据布局优化,从而提高缓存的使用效率。过程间优化还可以通过对内存应用的分析,从而允许通过寄存器来保存数据,从而减少内存读写带来的开销。过程间优化能够帮助对内存alias的分析,从而有助于进行自动矢量化和循环变换等优化。除此之外,过程间优化还支持常数传播、公共块分割等。

在Intel编译器中,选项/Qip表示要开启单一源文件内的过程间优化,而/Qipo则表示打开多个文件内的过程间优化,比如考虑下面两段代码:

// test_ipo.c
int update1(int i) {
return (i+1);
}

int main()
{
int i = rand();
int j = rand();
int result;
result = update1(i) + j ;
result += update2(i);
result += update45(i);
update3(i);
printf(“result = %d”,result);
}

// test_ipo2.c
int update2(int i) {
return (i+2);
}

int update3(int i) {

int j;
for (j =0; j < 100; j++)
i+=j;
return i;
}

int update45(int i) {
int result = update5(i);
return (result +4);
}

static int update5(int i) {
return (i+5);
}

如果采用下面命令进行编译:

icl /O2 /Qip test_ipo.c test_ipo2.c

/Qip表示只是进行单个文件的过程间优化,因此由于main和update1在同一个文件中,main对于update1的调用会进行内联扩展,代之以update1的函数体的代码,而在test_ipo2.c中update45函数中要调用的update5在同一个文件中,因此update5会被展开,同时由于update5声明为static函数,而且仅仅在update45中调用并被内联,因此update5为不会使用的函数,该函数对应的代码可以移除。

如果采用下面命令进行编译:

icl /O2 /Qipo test_ipo.c test_ipo2.c

/Qipo表示对于多个文件的过程间优化,除了和上面/Qip选项中对于update1和update45的内联之外,现在main还会调用update2和update3,虽然update2和update3在test_ipo2.c中定义,但是由于采用多个文件的过程间优化,这两个函数也会被内联展开。注意由于update1、update2、update3都被内联,而且再没有被其他函数调用,因此最后连接时这些函数的代码也会被当作不会用的代码被移除。

上面的例子都是让编译器自动完成从编译到连接的自动过程间优化,你也可以通过选项/Qipo-c和/Qipo-s告诉编译器只完成第一步编译部分的优化,连接阶段的过程间优化放在后面进行。/Qipo-c表示进行多个文件的第一步的过程间优化,生成相应的包含了中间表示的obj文件,同时还生成一个进行过程间优化后的obj文件(ipo_out.obj),供连接阶段使用。注意ipo_out.obj是一个真正的object文件,并没有包含任何的中间表示IR,可以直接用于连接阶段。/Qipo-s同样也是进行编译阶段的过程间优化,但是除了生成包含了中间表示的obj文件供连接阶段使用外,还生成一个汇编文件(ipo_out.asm)。有人也许会问/Qipo-c和采用选项/Qipo /c有什么不同。这两者都会生成包含了中间表示的obj文件,不过是前者还生成了一个ipo_out.obj文件,这在有些文件或者模块不采用过程间优化,而其他程序采用过程间优化可以发挥作用,通过/Qipo-c选项把那些需要进行过程间优化的文件编译生成一个ipo_out.obj,然后在连接阶段把ipo_out.obj与其它不支持过程间优化的文件连接在一起进行优化。

icl /O2 /Qipo-c test_ipo.c test_ipo2.c
icl /O2 /Qipo /Fetest_ipo.exe test_ipo.obj test_ipo2.obj

或者

icl /O2 /Qipo /c test_ipo.c test_ipo2.c
icl /O2 /Qipo /Fetest_ipo.exe test_ipo.obj test_ipo2.obj

或者

icl /O2 /Qipo-c test_ipo.c test_ipo2.c
icl /O2 /Fetest_ipo.exe ipo_out.obj

在对于比较大的程序进行过程间优化时,由于要对于函数之间的调用、内存的访问等进行分析,可能会需要比较长的时间,同时编译阶段生成的obj文件会包含中间表示IR,最后生成的obj文件可能会非常庞大。为了在程序比较大时减少过程间优化的开销,Intel编译器引入了/Qipo中允许后面跟一个数字n(如果只有/Qipo时相当于n=0),该数字给出了编译阶段生成的obj文件的个数,当n=0时表示由编译器决定。

前面说过过程间优化有助于内存别名和数据依赖的分析,由于编译器的首要目的是保证程序的正确运行,因此在进行优化时会采取比较保守的策略,在无法确保某些优化的前提条件是否满足时会选择不进行这个优化。考虑下面的代码:

// test_ipo_alias2.c
void func(int *a, int *b)
{
int i;
for (i = 0; i < array_len(); i++)
a[i] = b[i];
}
// test_ipo_alias.c
int a[MAX],b[MAX];

int main()
{ int i;
for (i=0; i< MAX; i++)
b[i] = rand();
func(a,b);
printf("a[5]=%d",a[5]);
}

int array_len() {
return (MAX);
}

如果没有IPO的帮助,编译器不知道array_len是如何实现的,是返回一个固定的值还是每次调用都可能返回一个不同的取值,因此无法对于func进行优化:

icl /O2 /QxT /Ob0 test_ipo_alias.c test_ipo_alias2.c

但是如果用下面命令进行编译,其中/Ob0不进行内联扩展:

icl /O2 /Ob0 /QxT /Qipo test_ipo_alias.c test_ipo_alias2.c

从下面的输出可以看到通过对于func进行了自动矢量化,这是因为通过IPO的帮助,知道func中循环体调用的array_len每次都返回一个固定的取值MAX,因而不会出现其他额外效应。

test_ipo_alias.c
test_ipo_alias2.c
ipo: remark #11000: performing multi-file optimizations
ipo: remark #11005: generating object file ipo_3148.obj
test_ipo_alias2.c(7): (col. 18) remark: LOOP WAS VECTORIZED.
-out:test_ipo_alias.exe

关于自动矢量化和SIMD我们会在后面的章节中进行详细描述。



4.2.6 编译器优化报告

Intel编译器可以提供相关的优化报告,这样程序员能够知道到底进行了哪些优化,或者了解到是因为什么原因有些优化无法进行,这样就可以通过修改代码或者通过编译器选项等使得该优化的条件得到满足,从而产生高质量的优化代码。

为了生成优化报告,Intel编译器提供了/Qopt-report选项,编译器支持的优化非常多,这样最后的优化报告可能会比较长,而程序员在进行优化时一般会根据应用和代码的情况对某些优化特别关注,这时可以通过/Qopt-report-phase告诉编译器不需要输出所有的优化报告,而只要告诉某个阶段的优化报告就可以了。更进一步,你还可以指定优化报告的详细程度,目前Intel编译器支持三种程度,分别是最小、中等、最大,这是通过/Qopt-report[n]来说明的,如果不指定数字n,则相当于n=2,即详细程度为中等的优化报告输出,n=0表示不输出优化报告。/Qopt-report-routine用于告诉编译器只输出那些函数名中包含的函数的优化报告,这在我们通过Vtune找到程序的热点函数之后再进一步试图对该函数进行优化时非常有用。一般优化报告是输出到标准错误输出stderr中,有的时候可能希望能够把优化报告保存在某个文件,以便打印或者进一步的阅读来找到优化的情况,这时可以通过/Qopt-report-file来指定优化报告保存在文件file中。

/Qopt-report-help可以用来显示Intel编译器支持的优化报告阶段,这些阶段主要包括:

  • pgo:Profile导向的优化
  • ipo:过程间优化,进一步又可以分为ipo_inl(过程间优化阶段的内联展开优化报告)、ipo_cp(常数传播)等
  • ilo:中间语言优化,进一步又可以分为ilo_strength_reduction(强度减少)、ilo_copy_propagation(拷贝传播)等。
  • hpo:高性能优化,进一步又可以分为hpo_openmp、hpo_vectorization、hpo_threadization等。
  • hlo:高级优化,进一步又可以分为hlo_unroll(循环展开)、hlo_fusion(循环合并)等

我们以上一小节的最后一个例子来说明如何使用优化报告,采用下列命令来生成有关高性能优化阶段(这里我们主要关心自动矢量化)的优化报告,详细程度为中等,优化报告保存在log.txt中。

icl /O2 /Qipo /QxT /Ob0 /Qopt-report2 /Qopt-report-phasehpo
/Qopt-report-filelog.txt test_ipo_alias.c test_ipo_alias2.c

log.txt的优化报告如下,main中的循环由于出现数据依赖而没有自动矢量化,而func则被自动矢量化:

<test_ipo_alias.c;-1:-1;hpo_vectorization;_main;0>
HPO Vectorizer Report (_main)

test_ipo_alias.c(9:2-9:2):VEC:_main: loop was not vectorized: existence of vector dependence

<test_ipo_alias2.c;-1:-1;hpo_vectorization;_func;0>
HPO Vectorizer Report (_func)

test_ipo_alias2.c(8:18-8:18):VEC:_func: LOOP WAS VECTORIZED



4.2.7 profile导向的优化PGO

测试Intel编译器的另外一个强大的优化支持是profile导向的优化(Profile-guided optimization),它的基本思想是在编译生成执行文件时插入PGO代码,这样在执行该执行文件时插入的PGO代码就可以收集有关变量、函数的使用情况等信息,最后再利用这些收集到的信息进行重新编译,这个时候由于知道了哪些函数或者哪些分支是程序的热点和冷点,因此可以针对其作相应的优化,比如通过把经常使用的函数的代码放在一起可以增加指令缓存的利用率,可以根据函数执行的情况来控制内联展开以及减少分支错误预测等,从而可以大大提高应用的性能。

要使用PGO优化,需要三个阶段:

  1. 第一个阶段是通过选项/prof-gen插入收集profile信息的PGO代码
  2. 第二个阶段是运行第一阶段生成的执行程序,在执行过程中收集到的信息会存放到后缀为.dyn的文件中。为了更有效地提高应用的性能,这个阶段应该利用在应用投入使用之后经常使用的输入来执行该程序,即通过基准来测试,这样收集到的信息是非常具有代表性的。多次执行程序,每次都会生成一个后缀为.dyn的动态信息文件。
  3. 第三个阶段是最终的编译阶段。利用选项/Qprof-use再次编译程序时,第二个阶段多次运行后生成的动态信息文件会合并成一个汇总文件,然后通过分析汇总文件中包含的profile信息,编译器可以针对那些经常使用的程序分支路径进行特别的优化。

由于第二个阶段每次执行程序都会要生成一个动态信息文件,这样多次执行会产生很多动态信息文件,从而使得执行文件所在的目录会充斥着这些.dyn文件,这个时候你可以使用选项/Qprof-dir

来告诉编译器把以后运行时生成的信息文件保存到dir指出的目录而不是执行文件所在的目录。

我们用一个例子来说明如何使用profile导向的优化,首先用下列命令进行第一阶段的编译:

mkdir profile
icl /O2 /Qprof-gen /Qprof-dirprofile test_pgo.c

你也可以先利用/S选项生成汇编文件来查看第一阶段的编译器插入的PGO指令和代码:

icl /O2 /Qprof-gen /Qprof-dirprofile /S test_pgo.c
$B1$1: ; Preds $B1$0
push 0 ;50.2
push OFFSET FLAT: __pgopti_2inst_string$0 ;50.2
push OFFSET FLAT: __pgo_segment_2pack$0 ;50.2
call __PGOPTI_Prof_Begin ;50.2
$B1$2: ; Preds $B1$1
add DWORD PTR __pgo_segment_2pack$0+52, 1 ;50.2
adc DWORD PTR __pgo_segment_2pack$0+56, 0 ;50.2
push OFFSET FLAT: _2__STRING$0 ;51.11

在第一阶段的编译生成执行文件之后,运行该执行文件:

test_pgo 1 2

test_pgo 1 2 3

每次运行我们都可以在profile所在的目录找到一个后缀为.dyn的动态信息文件,比如46b7cbfd_03016.dyn。最后完成PGO的第三个阶段,利用收集的信息反馈重新编译,在这个过程中编译器会首先调用profmerge把第二个阶段生成的.dyn文件合并成一个汇总文件pgopti.dpi,然后利用汇总文件中的信息进行优化生成一个新的执行文件,这个新的执行文件不再包含PGO的代码。如果你对于源代码有改变,或者发现经常使用的输入数据集有比较大的变化,你可以再使用/Qprof-gen来重新进行profile导向的优化。大多数情况下,你会选择在第一阶段编译时通过/O2来进行一般的优化,然后在第三个阶段你可以包含一些更高级的优化选项,特别是/Qipo来进行编译。IPO和PGO会互相影响,通过使用PGO编译器在进行函数内联时可以作更好的决定。

icl /O3 /Qipo /Qprof-use /Qprof-dirprofile test_pgo.c
C:\PROGRA~1\Intel\Compiler\C__~1\100~1.025\Ia32\Bin\profmerge: looking at dynami c file: profile\46b7dc4b_03428.dyn
C:\PROGRA~1\Intel\Compiler\C__~1\100~1.025\Ia32\Bin\profmerge: looking at dynamic file: profile\46b7dc4e_02420.dyn

test_pgo.c: warning #11503: Total routines 5, routines w/o profile info: 1

为了看看PGO优化的作用,我们可以查看相应的优化报告:

icl /O3 /Qipo /Qprof-use /Qprof-dirprofile /Qopt-report-phasepgo test_pgo.c

<test_pgo.c;-1:-1;PGO;_main;0>
DYN-VAL: test_pgo.c _main

<test_pgo.c;-1:-1;PGO;_one_arg;0>
NO-DYN: test_pgo.c _one_arg

<;-1:-1;PGO;;0>
4 FUNCTIONS HAD VALID DYNAMIC PROFILES
1 FUNCTIONS HAD NO DYNAMIC PROFILES
IPO CURRENT QUALITY METRIC: 90.0%
IPO POSSIBLE QUALITY METRIC: 90.0%
IPO QUALITY METRIC RATIO: 100.0%

同时你也可以通过调用profmerge命令来合并动态信息文件.dyn成汇总文件.dpi,当然你也可以通过该命令把多个汇总文件进一步合并,同时你也可以通过dump选项来查看收集到的profile信息:

profmerge -prof_dir profile –dump

通过第二阶段收集到的profile信息,PGO可以进行寄存器分配优化,减少内存读写的开销;同时把那些经常访问的“热点”代码放在一起,而把那些很好使用的“冷点”代码放在最后,这样大大提高了指令缓存的利用率;同样地PGO也可以把那些经常使用的变量放在一起,提高数据缓存的利用率;由于知道各个部分的使用频率,有助于提高分支预测的成功率;根据函数使用的频率,PGO在进行内联扩展时可以不仅仅根据代码段的大小,而是考虑到实际使用的情况,比如一个函数a要被两个函数b、c调用,通过分析profile知道在函数体b中的a经常被调用,这样在b可以选择把被调用的函数a内联展开,而在函数体中的c很少会用到,这时就不需要把b内联展开,这样内联又被称为部分内联(partial inling),可以在执行速度和代码大小方面作一个很好的平衡。与部分内联类似,如果发现一个可以自动矢量化的循环体的循环次数在大部分情况下只执行很少的次数,这时可以选择不进行矢量化,这样可以减少自动矢量化代码带来的开销。

如果一个大的应用包含许多的函数调用或者程序分支,在编译时可能无法预测那些分支经常被执行,但是在实际运行时通过提供稳定的输入数据,每次执行的行为也基本类似,这样可以通过收集profile来了解到其中有些分支被经常使用,这个时候profile导向的优化能够大大提升应用的性能。如果每次执行,应用所执行的分支路径都大不相同,具有很大的随机性,则PGO优化带来的性能提升可能并不是那么显著。

前面介绍的profile信息的收集都是通过插入相应的PGO代码来进行的,你也可以通过CPU的性能监测单元FPU硬件来收集动态信息。而要利用FPU来收集信息,在PGO的第一个阶段需要把原来的/Qprof-gen选项代替以/Qprof-gen-sampling:

icl /O2 /Qprof-gen-sampling /Qprof-dirprofile test_pgo.c

然后调用profrun来收集相关的数据,你可以通过指定dcache、icache和branch选项来收集有关数据缓存、指令缓存和分支预测方面的采样数据,同时你也可以通过event选项来选择其他感兴趣的事件。

profrun –dcache test_pgo.exe

最后和前面介绍的类似,在收集好profile信息之后,第三个阶段使用/Qprof-use选项重新编译。

icl /O3 /Qipo /Qprof-use test_pgo.c

一般在第二个阶段程序执行时当系统函数exit()被调用时把这次执行过程中收集到的profile信息保存到一个.dyn文件中。但是如果程序非正常退出或者不调用exit退出时就无法收集到这些profile,有的时候可能还会希望更进一步控制profile的收集,比如不是一次执行只生成一个.dyn文件,而是希望生成多个.dyn文件。Intel编译器提供了相应的API接口来对于profile信息的收集进行控制。

PGO API接口非常简单,注意它没有办法控制收集哪些profile信息,而只是控制什么时候把收集到的profile信息输出到.dyn文件。因此使用PGO API仍然要求在第一个阶段使用/Qprof-gen选项进行编译。所有的PGO API函数在头文件pgouser.h中声明,主要包含下列函数:

void _PGOPTI_Prof_Dump_All(void);
void _PGOPTI_Prof_Reset(void);
void _PGOPTI_Prof_Dump_And_Reset(void);
void _PGOPTI_Set_Interval_Prof_Dump(int interval);

第一个函数用于把当前收集的profile计数器的内容输出到.dyn文件,而第二个函数则要求把profile计数器的取值重置回复到初始状态,第三个函数相当于把第一个和第二个函数结合在一起,首先生成.dyn文件,然后重置计数器。最后一个函数是每隔一段时间把收集到的profile信息输出到.dyn文件,并且重置计数器的取值, interval给出了以毫秒为单位的间隔时间,比如interval取值6000,表示每隔6秒生成一个.dyn文件。

下面的代码片断给出了如何使用PGO API:

#include 
while (flag) {
_PGOPTI_Prof_Reset();
Dothings();
_PGOPTI_Prof_Dump_All();
}

下面的代码片断给出了如何使用PGOPTI_Set_Interval_Prof_Dump,每隔6秒自动生成一个动态信息文件.dyn:

#include 
PGOPTI_Set_Interval_Prof_Dump(6000);
while (flag) {
Dothings();
}


4.2.8 浮点数

浮点运算在科学计算中占有很重要的地位,因此浮点运算的优化对于性能的提升非常有帮助。内存中存储浮点类型的值有3种格式,分别是单精度(32个比特)的浮点数、双精度(64个比特)double和双精度扩展(80个比特)extended或者long double。我们知道浮点类型的数由两个部分组成,一个是整数部分,另外一个是小数点后面的取值,单精度的浮点数的小数部分占用24个比特,双精度double的小数部分占用53个比特,而双精度扩展extended的小数部分占用64个比特。

Intel处理器的浮点计算都是只针对某种浮点类型,两个不同类型的浮点计算需要首先转换成相同的类型,因此在进行浮点计算的编程时应该尽量采用同一种浮点类型,而且精度越低,浮点计算的速度也越快,因此如果不是有特殊的精度要求,应该把浮点变量声明为float。

值得注意的是,由于精度方面的原因,从精确的角度来说,原来的整数运算的结合律、交换律等可能都不再适用,考虑:

float t0, t1, t2;
t0 = 4.0f + 1.0f + t1 + t2;

如果t0、t1和t2都是整数,则首先可以进行常数传播优化,把4+1代之以5,另外5+t1+t2可以解析为:(t1+5)+t2,(t1+t2)+5,(t2+5)+t1等等,正是由于整数运算的交换和结合律,使得公共子表达式、循环不变量等优化可以发挥作用。但是现在要求浮点运算的精度的精确,则这些优化都无法进行。显然那些对于精度要求不是特别严格的应用可以允许表达式的顺序进行交换,从而可以进行类似于整数运算的优化策略,使得计算的速度更快。Intel编译器的/fp:keyword选项允许用户根据应用的具体要求,选择合适的浮点运算模式,在性能、精度、结果的一致性等作一个平衡。/fp:precise告诉编译器要保证最后计算的值的精度,浮点数常量运算不在编译时就计算,严格按照表达式本来的顺序进行计算,同时浮点表达式的中间结果的表示也保证精度不会丢失,在IA-32体系结构中采用双精度类型来表示。/fp:fast[=1|2]告诉编译器在进行浮点运算时优先考虑到性能的因素,也就是说允许采用一些比较激进的优化策略,比如允许改变表达式的计算顺序,常量传播优化等,表达式的中间结果可以根据性能方面的考虑采用单精度、双精度、双精度扩展其中任何一种类型。Intel编译器的在使用/O2选项时浮点运算的缺省选项为/fp:fast=1,fast取值2告诉编译器作一些更加高级的优化。/fp:source选项告诉编译器在进行浮点运算时不改变原来的计算顺序,但是可以把可以合并的那些浮点常数运算在编译时就计算出来,另外计算的中间结果采用源代码中采用的浮点类型。除了上面3种模式之外,还有strict、double、extended模式,具体的优化可以参考Intel编译器的用户手册。

每个应用在进行浮点运算时的目标会各不相同,如果希望结果与正确的结果接近,也就是追求精度,则可以采用/fp:precise选项。如果考虑到性能的因素,即执行速度更快,则可以采用/fp:fast选项。而如果希望计算的结果在不同的机器、不同的操作系统、不同的编译器环境下都保持一致,也就是考虑到结果的一致和移植,可以采用/fp:source选项。考虑下面代码,由于精度的因素,calc_1和calc_2的计算结果可能会不相同,但是如果采用/fp:fast选项进行编译,编译器可能会进行循环分割优化把calc_1转换为calc_2,这样导致两个函数的计算结果是相同,而采用/fp:source就可以关闭某些优化以保证计算结果的一致性。

void calc_1(float *a, float *b, float *x, float *y, float *z, int n){
int i;
for(i = 0; i < n; i++){
a[i] = 0.05 * fabs(x[i] + y[i] + z[i]);
b[i] = a[i];
}
}
void calc_2(float *a, float *b, float *x, float *y, float *z, int n){
int i;
for(i = 0; i < n; i++)
a[i] = 0.05 * fabs(x[i] + y[i] + z[i]);
for(i = 0; i < n; i++)
b[i] = a[i];
}

当浮点数值变得非常小,而标准的归一化浮点格式无法表示这个数时,这个数就称为微小数。如果直接把这个微小数用0来代替,显然在精度方面就会牺牲不少,因此Intel的处理器或者FPU支持微小数的表示和运算,不过需要比较长的时间,可能需要上百个时钟周期。要处理微小数带来的性能下降可以有三种方法,一种方法是修改代码在原来的表达式上添加一个放大因子使得计算出来的结果可以用标准的归一化浮点格式表示,另外一种方法使用一个精度更高的浮点类型,最后一种方法就是牺牲精度,把微小数用0来代替。Intel编译器提供的选项/Qftz就是告诉编译器遇到微小数时采用最后一种方法来处理,当然带来的结果就是会丢失精度。

程序员也可以通过手工修改代码来提高浮点运算的性能,前面讲过浮点表示的精度限制,有的时候编译器可能会选择不进行某些优化,比如抽取公共子表达式,这样用户可以改变浮点运算执行的顺序,比如下面的代码:

double a, b, c, d, e, f;
e = b * c / d;
f = b / d * a;

可以通过引入一个临时变量来抽取公共子表达式,代码改变为:

double a, b, c, d, e, f, t;
t = b / d;
e = c * t;
f = a * t;

另外在浮点运算中除法的开销要比乘法大很多,因此如果有可能,可以考虑把浮点除法以乘法运算来代替,考虑下列代码:

double a, b, c, e, f;
e = a / c;
f = b / c;

引入一个临时变量保存1/c,代码改变为:

double a, b, c, e, f, t;
t = 1 / c;
e = a * t
f = b * t;