使用Intel 向量化编译器优化性能(2)
本文部分翻译自Intel网站,部分选自Intel编译器文档以及中科院计算所的一些资料
首先要向大家道歉,由于疏忽,才发现intel的这个教程可能只是针对5.0版本的编译器,现在已经是7.1了,所以文中的例子可能在现在的编译器上会有不同的表现,我会把每一个例子测试一下,并结合从其它渠道找到的关于intel编译器的文档来给大家介绍向量化编译技术.
1. 如何编写向量化程序
向量化代码是一个不断尝试的过程,每一个循环在被向量化前都可能需要调整很多次,依据下面几条原则去做能更好帮你完成工作.
a) 先尝试编译一次,会有一些循环是可以直接被向量化的.
一些现有的代码可能已经符合了编译器的要求,不需要做任何修改就可以直接被向量化,不管怎么样,先试一下.
b) 找到你的代码中的那些最影响程序性能的循环
使用代码分析工具来寻找你的代码中那些经常被调用的循环,这些循环最值得被向量化,即使是一个很小的循环,如果被频繁调用的话,对性能的影响是很可观的,所以能够向量化这个循环将给你的代码带来很大的性能的提高,Intel VTune Performance Analyzer 可以帮你找到这些性能上的瓶颈,你可以从Intel的网站上下载并免费试用 http://www.intel.com/software/products/vtune. 关于VTune的具体使用方法我会在别的时间给大家介绍,现在我就把它能做什么简单说一下
1. 附加在一个可执行文件上,在这程序运行的期间搜集处理器数据.
2. 跟踪你的代码,并把关于你的代码的性能的数据记录下来,比如函数A运行了多少时钟周期,使用了多少条指令…等,这样你就很容易找到那些最影响性能的循环.
3. 把运行的数据生成图形报表.
4. 对统计结果提出建议,知道你对代码的优化.
c) 使用#pragma ivdep
这个上次说明过了.
d) 重新编写这个循环
有时候一个循环在结构上注定无法向量化,这时只能另起炉灶了...
2. 循环中的数据依赖
为了从分利用Intel编译器,你需要了解数据依赖产生的原因以及如何处理这些情况
a) 这里因为Intel网站的例子可能有问题,应该是编译器版本问题,所以我换了个例子以说明问题,当然这样的例子在实际应用中没有什么意义,这里只是为了说明问题
下面这段程序是无法被向量化的.
float data[100];
int i = 0;
for (i = 0; i < 100-1; ++i)
{
s1: data[i] = data[i-1]*0.25 + data[i]*0.5+data[i+1]*0.25;
}
在这里在S1语句中的data[i]对data[i-1]产生数据依赖,data[i]被data[i-1]在上一个循环迭代中所修改,不明白?,这么看:
data[i-1] 的下标比 data[i]小1,操作中data[i-1]总是会覆盖data[i],这就说明了上一次说的向量化不是并行化,因为在向量化中是要考虑这样的相互影响,所以不能说是完全的并行化,虽然数据是被SIMD方式并行操作的.偶是这么理解的,或许有不正确的地方,请大家及时指正.再看下面的说明可能会更清晰一些.
上面的例子中 当i=1时: i=2时
read data[0] read data[1]
read data[1] read data[2]
read data[2] read data[3]
write data[1] write data[2]
当使用标量循环时不会有什么问题,data[1]数据依次被改写,但是在SIMD并行操作中问题就出现了i=2时 read data[1]时,数据就可能已经被i=1的write data[i]修改,这就改变了这个循环的本意.这是绝对不可以.
b) 上面是数据依赖的实际例子,下面我们从理论上解释一下数据依赖的原因.
数据依赖产生的条件就是在操作中发现可能对内存中发生的覆盖的存取,如果代码中有2个引用,那么.
1. 这2个引用可能是别名,而他们指向了同一块内存或是两块相互覆盖的内存
2. 如果是数组,那就很据他们的下标来判断是否覆盖同一块内存.
对于数组,编译器的数据依赖分析器将使用一系列逐渐强化的测试来获取循环在时间和空间上的开销.首先编译器会对数组的每个维进行一些简单的测试,直到在任何一个维上都没有发现数据依赖的问题.在测试前,使用多维数组时可能发生的在它们定义边界内的数据交叉可以被转换为一个线性化的形式,其中的一些测试会使用快速最大公约数测试, 测试会使用达到数组边界的数据已确保不会有数据覆盖的问题.
如果上面的测试失败,编译器就会使用Fourier-Motzkin消去法来解决所有维中的数据依赖问题.
这里我发现Intel网站的教程中并没有提到这一过程,可能这就是在高版本编译器中更容易解决数据依赖问题而不需要人为去修改代码的原因,实际上我说的那个有问题的例子就是直接可以进行向量化编译而不需要修改.
下面看几个循环的例子.
1. 循环结构
可以使用for/while/until来构成循环,但是循环只能有一个入口和一个出口.
While( i< 100)
{
a[i] = b[i] * c[i];
if (a[i] < 0.0)
{
a[i] = 0.0;
}
++i;
}
这个可以被向量化
While( i< 100)
{
a[i] = b[i] * c[i];
if (a[i] < 0.0)
{
a[i] = 0.0;
break;
}
++i;
}
这样不行,不能有多个出口
2. 循环退出条件
循环退出条件决定了,循环执行的迭代次数,比如一个固定的次数.总之这必须是一个可以确定循环次数的表达式.
a) 常量,如100
b) 一个不被改变的变量,如 i=100
c) 一个计算循环次数的线性函数 如 i=100-1
下面是几个示例
int k = 100;int i = 0;
while(i< k - 1)
{
a[i] = b[i] * c[i];
if (a[i] < 0.0)
{
a[i] = 0.0;
}
++i;
}
条件确定
int k = 100;int i = 0;
while(i< k – i)
{
a[i] = b[i] * c[i];
if (a[i] < 0.0)
{
a[i] = 0.0;
}
++i;
}
条件不确定,循环次数依赖i
for (i = m;i < n;++i)
{
a[i] = i + 1;
}
条件确定,循环次数为 n-m