我有一个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。我不知道这是怎么回事。我将不胜感激任何有关这方面的帮助(即使它发生的可能性很小)。此外,我想知道为什么分支误预测会增加。提前谢谢!
答案 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}}被解释为无符号整数。
其次,使用y
或px_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_n
和px_x
中的任何一个超出范围时,此更改会增加基本块的大小并减少计算量(我猜这个是不常见的,所以最好的好处是非常小。)
px_y
理想情况下,像你这样的代码会被矢量化,但我不知道如何让gcc识别机会,如何使用内在函数来表达机会,也不知道在SIMD宽度下手动矢量化这些代码的重要努力是否值得只有两个。
我不是程序员(只是喜欢学习和思考计算机体系结构的人)而且我对微观优化有很大的兴趣(从上面可以清楚地看出),所以应该从这个角度考虑上述建议。 / p>