编译展开的这篇博客被CSDN推了首页http://blog.csdn.net/pennyliang/archive/2010/10/28/5971059.aspx,有些读者反映有些太难,考虑到有些地方没有讲得太清楚,本文一并进行深入讨论。
首先关键的代码是:
#define DO(x) x
#define DO4(x) x x x x
#define DO8(x) DO4(x) DO4(x)
#define DO16(x) DO8(x) DO8(x)
读者可以按葫芦画瓢,继续展开,这并不难理解。
我们选取了计算斐波那契数作为例子,将代码展开为16的倍数,但是因为Fx是用户输入的,可能是16的倍数也可能不是,因此需要做一些转换,将Fx变为Fx=16idx+r,这样可以确保可以做idx次展开,最后尾部再用一个循环计算完,如下:
int r= Fx%16;
int idx = Fx/16;
int i=2;
for(;i<idx;)
{
DO16(F[i]=F[i-1]+F[i-2];i++;); //展开成16段代码
}
for(;i<Fx;i++)
{
F[i]=F[i-1]+F[i-2];
}
如果我们不用编译来做循环展开,我们的代码可能就得是
for(;i<idx;)
{
F[i]=F[i-1]+F[i-2];i++;
F[i]=F[i-1]+F[i-2];i++;
F[i]=F[i-1]+F[i-2];i++;
....写16遍相同的代码,多难看啊。
}
for(;i<Fx;i++)
{
F[i]=F[i-1]+F[i-2];
}
第二,我们要特别注意memset的使用,memset在计时之前进行,是因为这样的计算更为准确,malloc分配大内存是采用mmap的方式,即只分配虚地址,而没有实际的调页,而我们做一个memset是为了调页,大家可以做这样的实验,做两次相同的malloc,用rdtsc的方式进行计时,你会发现第一次malloc会慢一些,而第二次会快,因为第一次有调页,而第二次几乎无调页(内存要足够大,否则可能会swap,也可能有少量调页)。
用memset相信也不难理解,因为这个时间混在里面可能会看不出误差,假定某市一季度GDP为10,二季度为15,看上去提高了50%,但是因为在计算过程中,有5份的GDP(可以想象成memset的代价)是多算在里面的,如果扣除这5份,则实际上是从5提升了100%。因此我们在设计实验时,需要把一些干扰项扣除,来比较,实验的数据才具有价值。
第三,我的实验结果是展开为4次是比较快的,我认为有这样一些主要原因,但还需要设计进一步的实验来证明:
(1)编译展开的层次过大,代码会变大,而代码存储在文件中,进入执行需要有一个读磁盘的过程,另外代码大,代码局部性就不强,L1 cache的一部分是存放代码段的,如果代码越小越紧凑,那么执行起来就会越开,展开出1024层,代码的局部性肯定很差。
(2)编译展开的层次过小,代码虽然紧凑,但是流水线不通畅,跳转太多,因此也容易导致性能变低。
因此总会有一种展开的层次可以trade off的流水线和代码段的紧凑性,我的实验结果是展开到4层是最快的,这是受机器环境影响的,不知道其他同学的实验结果是怎样的。
最后提到的细节是DO16(F[i]=F[i-1]+F[i-2];i++;); 的最后一个分号是不必要的,但是添加在这里是为了让代码更自然,相信大部分细心的读者都能看到这一点。
本系列其他文章:http://blog.csdn.net/pennyliang/category/746545.aspx