linux编程的108种奇淫巧计-4(编译展开)(续)

来源:互联网 发布:上班族怎么减肥 知乎 编辑:程序博客网 时间:2024/06/08 12:57

     编译展开的这篇博客被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

原创粉丝点击