循环展开后退出的指令减少

时间:2014-03-16 03:39:39

标签: c multicore computer-architecture vtune loop-unrolling

我有一个O(N ^ 4)图像处理循环,在对其进行分析后(使用英特尔Vtune 2013),我看到退役的指令数量大幅减少。我需要帮助理解多核架构上的这种行为。 (我使用的是英特尔至强x5365-每8个内核有8个核心,共享二级缓存)。此外,分支错误预测的数量也大幅增加! ///////////////编辑///////////我的非展开代码示例如下所示:

for(imageNo =0; imageNo<496;imageNo++){
for (unsigned int k=0; k<256; k++)
{
    double z = O_L + (double)k * R_L;
    for (unsigned int j=0; j<256; j++)
    {
        double y = O_L + (double)j * R_L;

        for (unsigned int i=0; i<256; i++)
        {
            double x[1] = {O_L + (double)i * R_L} ;             
            double w_n =  (A_n[2] * x[0] + A_n[5] * y + A_n[8] * z + A_n[11])  ;
            double u_n =  ((A_n[0] * x[0] + A_n[3] * y + A_n[6] * z + A_n[9] ) / w_n);
            double v_n =  ((A_n[1] * x[0] + A_n[4] * y + A_n[7] * z + A_n[10]) / w_n);                      

            for(int loop=0; loop<1;loop++)
            {
                px_x[loop] = (int) floor(u_n);
                px_y[loop] = (int) floor(v_n);
                alpha[loop] = u_n - px_x[loop] ;
                beta[loop]  = v_n - px_y[loop] ;
            }
///////////////////(i,j) pixels ///////////////////////////////
                if (px_x[0]>=0 && px_x[0]<(int)threadCopy[0].S_x && px_y[0]>=0 && px_y[0]<(int)threadCopy[0].S_y)                   

                    pixel_1[0] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + px_x[0]];
                else
                    pixel_1[0] = 0.0;               

                if (px_x[0]+1>=0 && px_x[0]+1<(int)threadCopy[0].S_x && px_y[0]>=0 && px_y[0]<(int)threadCopy[0].S_y)                   

                    pixel_1[2] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + (px_x[0]+1)];
                else
                    pixel_1[2] = 0.0;                   

/////////////////// (i+1, j) pixels/////////////////////////    

                if (px_x[0]>=0 && px_x[0]<(int)threadCopy[0].S_x && px_y[0]+1>=0 && px_y[0]+1<(int)threadCopy[0].S_y)
                    pixel_1[1] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + px_x[0]];
                else
                    pixel_1[1] = 0.0;                   

                if (px_x[0]+1>=0 && px_x[0]+1<(int)threadCopy[0].S_x && px_y[0]+1>=0 && px_y[0]+1<(int)threadCopy[0].S_y)

                    pixel_1[3] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + (px_x[0]+1)];

                else 

                    pixel_1[3] = 0.0;

                pix_1 = (1.0 - alpha[0]) * (1.0 - beta[0]) * pixel_1[0] + (1.0 - alpha[0]) * beta[0]  * pixel_1[1]

                +  alpha[0]  * (1.0 - beta[0]) * pixel_1[2]   +  alpha[0]  *  beta[0]  * pixel_1[3];                                

            f_L[k * L * L + j * L + i] += (float)(1.0 / (w_n * w_n) * pix_1);
                        }
    }

}
   }

我通过4次迭代展开最内层的循环。(你将有一个理想的方法我如何剥离循环。基本上我创建了一个Array [4]数组并在其中填充了相应的值。)数学,我将迭代总数减少了75%。假设每个循环有4个循环处理指令(加载i,inc i,cmp i,jle循环),展开后的总指令数应减少(256-64)* 4 * 256 * 256 * 496 = 24.96G 。 配置结果如下:

Before UnRolling: Instr retired: 3.1603T      no of branch mis-predictions: 96 million
After UnRolling:  Instr retired: 2.642240T    no of branch mis-predictions: 144 million

退出的无instr减少了518.06G。我不知道这是怎么回事。我将不胜感激任何有关这方面的帮助(即使它发生的可能性很小)。此外,我想知道为什么分支误预测会增加。提前谢谢!

1 个答案:

答案 0 :(得分:4)

目前尚不清楚gcc将减少指令数量。增加的寄存器压力可能会鼓励gcc使用加载+操作指令(因此相同数量的基本操作但指令更少)。 f_L的索引只会在每个最里面的循环中递增一次,但这只会节省6.2G(3 * 64 * 256 * 256 * 496)指令。 (顺便说一句,循环开销应该只有三条指令,因为i应保留在寄存器中。)

使用双向展开的以下伪装配(对于类似RISC的ISA)显示了如何保存增量:

// the address of f_L[k * L * L + j * L + i] is in r1
// (float)(1.0 / (w_n * w_n) * pix_1) results are in f1 and f2
load-single f9 [r1];    // load float at address in r1 to register f9
add-single f9 f9 f1;    // f9 = f9 + f1
store-single [r1] f9;   // store float in f9 to address in r1
load-single f10 4[r1];  // load float at address of r1+4 to f10
add-single f10 f10 f2;  // f10 = f10 + f2
store-single 4[r1] f10; // store float in f10 to address of r1+4
add r1 r1 #8;           // increase the address by 8 bytes

非展开版本的两次迭代的跟踪看起来更像是:

load-single f9 [r1];  // load float at address of r1 to f9
add-single f9 f9 f2;  // f9 = f9 + f2
store-single [r1] f9; // store float in f9 to address of r1
add r1 r1 #4;         // increase the address by 4 bytes
...
load-single f9 [r1];  // load float at address of r1 to f9
add-single f9 f9 f2;  // f9 = f9 + f2
store-single [r1] f9; // store float in f9 to address of r1
add r1 r1 #4;         // increase the address by 4 bytes

因为存储器寻址指令通常包括添加立即偏移(Itanium是一种不寻常的例外),并且通常不实现流水线来优化立即数为零时的情况,所以使用非零立即偏移通常是&#34;免费&#34 ;. (在这种情况下,它肯定会减少指令数量7对8,但通常也会提高性能。)

关于分支预测,根据Agner Fog的英特尔,AMD和VIA CPU的微体系结构:汇编程序员和编译器制造商的优化指南PDF Core2微体系结构的分支预测器使用8位全局历史。这意味着它跟踪最后8个分支的结果,并使用这8位(以及指令地址中的位)来索引表。这允许识别附近分支之间的相关性。

对于您的代码,对应于例如第8个前一个分支的分支在每次迭代中不是相同的分支(因为使用了短路),因此要概念化相关性的识别并不容易。< / p>

分支机构中的一些相关性是显而易见的。如果px_x[0]>=0为真,px_x[0]+1>=0也将为真。如果px_x[0] <(int)threadCopy[0].S_x为真,则px_x[0]+1 <(int)threadCopy[0].S_x 可能为真。

如果完成展开以便对px_x[n]的所有四个值测试n,那么这些相关性将被推得更远,以便分支预测器不使用结果。

一些优化可能性

虽然您没有询问任何优化可能性,但我将提供一些探索途径。

首先,对于分支,如果不是严格可移植,那么测试x>=0 && x<y可以简化为(unsigned)x<(unsigned)y。这不是严格便携的,因为例如机器理论上可以以符号幅度格式表示负数,其中最高有效位作为符号,而负数由零位指示。对于有符号整数的常见表示,只要y是正有符号整数,这样的重新解释强制转换就会起作用,因为负x值将设置最高有效位,因此大于{{ 1}}被解释为无符号整数。

其次,使用ypx_x的100%相关性可以显着减少分支数量:

px_y

(如果上面的代码部分被复制用于展开,它可能应该被复制为块而不是交错测试不同的if ((unsigned) px_y[0]<(unsigned int)threadCopy[0].S_y) { if ((unsigned)px_x[0]<(unsigned int)threadCopy[0].S_x) pixel_1[0] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + px_x[0]]; else pixel_1[0] = 0.0; if ((unsigned)px_x[0]+1<(unsigned int)threadCopy[0].S_x) pixel_1[2] = threadCopy[0].I_n[px_y[0] * threadCopy[0].S_x + (px_x[0]+1)]; else pixel_1[2] = 0.0; } if ((unsigned)px_y[0]+1<(unsigned int)threadCopy[0].S_y) { if ((unsigned)px_x[0]<(unsigned int)threadCopy[0].S_x) pixel_1[1] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + px_x[0]]; else pixel_1[1] = 0.0; if ((unsigned)px_x[0]+1<(unsigned int)threadCopy[0].S_x) pixel_1[3] = threadCopy[0].I_n[(px_y[0]+1) * threadCopy[0].S_x + (px_x[0]+1)]; else pixel_1[3] = 0.0; } px_x值以允许px_y分支靠近px_y分支,第一个px_y+1分支靠近另一个px_x分支和px_x分支。)

另一种可能的优化是将px_x+1的计算更改为其倒数的计算。这会将乘法和三个除法分为四个乘法和一个除法。除了乘法之外,分部很多。此外,计算近似倒数对SIMD更友好,因为通常有倒数估计指令,它们提供了一个可以通过Newton-Raphson方法改进的起点。

如果可以接受更糟糕的代码混淆,您可以考虑将w_n之类的代码更改为double y = O_L + (double)j * R_L;。 (我运行了一个测试,gcc似乎没有认识到这种优化,可能是因为使用了浮点和强制转换为双倍。)因此:

double y = O_L; ... y += R_L;

我猜这只能适度提高性能,因此混淆成本相对于收益会更高。

可能值得尝试的另一项更改是将部分for(int imageNo =0; imageNo<496;imageNo++){ double z = O_L; for (unsigned int k=0; k<256; k++) { double y = O_L; for (unsigned int j=0; j<256; j++) { double x[1]; x[0] = O_L; for (unsigned int i=0; i<256; i++) { ... x[0] += R_L ; } // end of i loop y += R_L; } // end of j loop z += R_L; } // end of k loop } // end of imageNo loop 计算合并到有条件设置pix_1值的部分中。这会严重模糊代码,可能没有多大好处。此外,它可能使编译器的自动向量化更加困难。 (通过有条件地将值设置为适当的pixel_1[]或者为零,SIMD比较可以将每个元素设置为-1或0,而具有I_n值的简单and将提供正确的值在这种情况下,形成I_n向量的开销可能不值得,因为Core2仅支持2宽双精度SIMD,但是在收集支持或甚至更长的向量时,权衡可能会发生变化。)

然而,当I_npx_x中的任何一个超出范围时,此更改增加基本块的大小并减少计算量(我猜这个是不常见的,所以最好的好处是非常小。)

px_y

理想情况下,像你这样的代码会被矢量化,但我不知道如何让gcc识别机会,如何使用内在函数来表达机会,也不知道在SIMD宽度下手动矢量化这些代码的重要努力是否值得只有两个。

我不是程序员(只是喜欢学习和思考计算机体系结构的人)而且我对微观优化有很大的兴趣(从上面可以清楚地看出),所以应该从这个角度考虑上述建议。 / p>