英特尔编译器优化系列:初识IPO优化

认识IPO

IPO优化(全称Interprocedural Optimization)在很长一段时间内都是英特尔编译器的几大特色优化手段之一。在我刚接触英特尔编译器的时候,编译器版本号还是7.0,由于IPO易于使用而且优化效果出色,IPO就已经被英特尔编译器用户广泛使用了。

首先,我们来看一看IPO主要包含哪些优化:

    • Inlining:将函数体插入到函数调用点,以减少函数调用带来的开销。但是并不是一见到函数调用就要inline,因为inline也是有一定开销的,inline后生成的代码变大了,过大的代码可能导致i-cache miss的问题。所以英特尔编译器会分析源代码,根据具体情况来决定是否要做inlining。Inlining的变种是Partial inlining,例如下面伪代码 




void foo()
{
some work here...
if (...)
{
some other work here...
}
}

在调用函数foo的时候,其前半部分肯定会被执行,但被if语句包围的部分就不一定会被执行到,甚至可能很少被执行到。Partial inlining的意思是既然我把整个函数体都inline的话代码可能过大,那我就把函数体前半部分代码给inline到调用点了,而后半部分不做inline。这样的结果就是,编译器生成一个函数叫foo_2



void foo_2()
{
some other work here...
}

函数调用点就变成



some work here...
if (...)
call foo_2();

这样生成的代码相对完全inline较小,同时还仍然能减少函数调用次数,提高程序运行速度。




    • constant propagation:当我们把一个常量赋值给一个变量时,编译器通过常量扩散这种优化可以将该变量的使用替换为该常量。一般来说,常量扩散可以用在基本块内,也可以在更复杂的控制流上。IPO可以将常量扩散的使用范围跨越函数边界甚至跨越文件,也就是说,IPO可以将常量从一个函数到另一个函数。

    • alias analysis:别名分析,对其他优化,比如向量化、并行化,有非常大的帮助。因为向量化和并行化对循环有个基本的要求,循环的每次迭代(iteration)之间没有依赖关系(dependency),编译器需要对循环进行依赖性分析以确定循环是否是无依赖的。别名分析能帮助编译器更容易确定循环的依赖性。

    • dead function elimination:通过分析整个程序,发现未被调用过的函数,在生成的二进制代码中不包含这些函数。

    • whole program analysis:IPO能够跨文件做分析、优化,在后面关于如何使用IPO的介绍中,我们会看到英特尔编译器是如何做到跨文件分析和优化的。

    • array dimension padding:通过向数组内填充一些空字节,以减少cache组冲突(cache set conflict)来提高cache利用率,并且使数组访问能与cache line边界对齐来避免false sharing。关于false sharing的介绍,请看http://en.wikipedia.org/wiki/False_sharing

    • structure splitting and field reordering:将一个结构分解成两个或多个较小的结构,以便能让较小较热的结构尽可能留在cache中,获得更好的cache命中率。所谓较热,是指在某一段时间内被频繁访问。

    • C++ class hierarchy analysis:C++的重载使得C++代码非常灵活,而且比C代码更具可扩展性。然而,重载给编译器带来了一些小麻烦,在缺乏足够信息的情况下,编译器并不知道一个类的某个方法(method)最终是在哪个父类中定义的。C++类层次结构分析能解决这个问题,通过这个分析,编译器可以获得完整的类层次结构图,从而在编译时推断出定义该方法的类,以达到提高程序运行效率的目的。

    • Passing arguments in registers:函数调用时使用寄存器传递参数,以减少内存访问。



在某些特定条件和范围内(比如在一个基本块中),其他优化器也做上述部分优化,但IPO能在更大范围内使用这些优化,因此优化的效果更加出色。

IPO分两种模式:单文件编译模式和多文件编译模式。

在单文件模式下(使用/Qip或-ip开关),编译器在单个文件的范围内使用以上优化,给每个源文件都生成一个对应的二进制文件,然后再将所有二进制文件链接起来。使用IPO单文件模式的步骤与使用普通优化步骤完全一样。

在多文件模式下(使用/Qipo或-ipo开关),编译器在编译过程中给每个源文件生成的不再是真正的二进制文件,而是"虚拟的二进制文件"。之所以叫虚拟的二进制文件是因为这些文件中存储的不是二进制代码,而是编译器生成的中间语言。这些中间语言文件的大小通常是真实二进制文件大小的数十倍,不需要觉得惊奇。编译器在编译过程中实际上并不做任何优化,只是将源代码转换成中间语言。那么在多文件模式下,优化是在什么时候进行的呢?是在链接的时候进行的。请看以下IPO示意图:

 


因为在编译过程中生成的是中间语言文件,系统的链接器是无法识别的,所以我们在链接时不是使用link或ld,而应该使用icl、icc或icpc来链接,也可以使用英特尔编译器的xilink或xild。无论用哪一个来做链接,他们首先会将所有中间语言文件合并到一起,这个合并后的巨大的中间语言文件包含了所有源代码信息,这样英特尔编译器就能跨越文件对所有源代码(合并后的中间语言文件)进行集中的分析和优化了,然后再将优化后生成的真正的二进制文件传递给真正的链接器link或ld进行链接,最后得到可执行文件。因为编译器能够一次性看到所有源代码的信息,所以它做的分析具有更高的确定性和准确性,能够实施的优化也要多得多。

下面让我们来看一下如何使用IPO。 

 

使用IPO

IPO的使用并不复杂,只需要加上/Qipo或-ipo开关,例如:

















Operating System Example Command
Linux and Mac OS X icpc -ipo -o app a.cpp b.cpp c.cpp
Windows icl /Qipo /Feapp.exe a.cpp b.cpp c.cpp


以上命令行将编译这三个源文件并生成可执行文件app和app.exe。

有时候我们需要将编译和链接分开来做。

首先,加上/Qipo或-ipo开关编译源代码:

















Operating System Example Command
Linux and Mac OS X icpc -ipo -c a.cpp b.cpp c.cpp
Windows* icl /Qipo /c a.cpp b.cpp c.cpp


然后,我们将生成的二进制文件链接起来。

















Operating System Example Command
Linux and Mac OS X icpc -o app a.o b.o c.o
Windows icl /Feapp.exe a.obj b.obj c.obj


看上去跟我们不使用IPO时的步骤和命令行差不多。需要注意的是,我们这里使用的是icpc(如果是C代码则使用icc)和icl作为链接器来进行链接。其实也可以使用xild(Linux和Mac OS X)和xilink(Windows)来链接,但是不能直接使用ld和link来链接。

类似的,当您需要创建.lib文件或archive文件时,请使用xilib和xiar替代lib和ar。

如果是在Microsoft* Visual Studio*的IDE内使用英特尔编译器,英特尔编译器安装程序已经自动为您配置好xilink和xilib了,要使用IPO只需要打开/Qipo开关即可。

在下一篇文章《英特尔编译器优化系列:深入IPO优化》中,我们会深入IPO,介绍一些IPO的高级技巧。

Categories:
Tags:
For more complete information about compiler optimizations, see our Optimization Notice.