我正在为我的大学开发一些科学软件。它是用Windows(VS2008)的C ++编写的。该算法必须为大量矩阵对计算一些值,即在核心处存在迭代矩阵的循环,收集一些数据,例如:
sumA = sumAsq = sumB = sumBsq = diffsum = diffsumsq = return = 0;
for (int y=0; y < height; ++y)
{
for (int x=0; x < width; ++x)
{
valA = matrixA(x,y);
valB = matrixB(x,y);
sumA+=valA;
sumAsq+=valA*valA;
sumB+=valB;
sumBsq+=valB*valB;
diffsum+=valA-valB;
diffsumsq+=(valA-valB)*(valA-valB);
}
}
return = sumA + sumB / sumAsq + sumBsq * diffsum * diffsumsq
对于不同的matrixA,matrixB对,该例程执行了数百万次。我的问题是这个程序非常慢,在发布模式下编译,激活了所有优化。使用“暂停时繁忙检查”调试器技术,我确定程序几乎每个时间都位于此循环中,即使如您所料,此例程被一个一大堆条件和控制分支。令我最困惑的是,在基于Xeon的双处理器系统上执行时,该程序使用了4个内核中的一个(毫不奇怪,现在它是单线程的)但只有最多约25%的限制并且具有相对较大的振荡,在程序终止之前我会期望稳定,100%负载。
当前版本实际上是一个重写版本,它是在优化性能的基础上创建的。当我发现它实际上比原来慢时,我感到很沮丧。之前的版本使用了Boost矩阵,我将其替换为OpenCV矩阵,在比较两个1000x100矩阵乘法的执行时间后,将其建立的速度提高了10倍以上。我通过手动取消引用指向其数据的原始指针来访问矩阵,我希望这会获得一些性能。我使计算例程成为一个多行#define宏来强制执行其内联并避免函数调用和返回。我改进了计算背后的数学运算,以便在单次通过矩阵时计算最终值(旧版本需要两次通过)。我希望获得巨大的收益,但事实恰恰相反。我远远不及我的旧程序的效率,更不用说特定应用程序的商业软件。
我想知道它是否可能与8位字符的矩阵数据有关,我曾经看到浮点数的访问实际上比我的旧程序中的加倍要慢,因为处理器检索后字符甚至更慢32位块中的数据(这个Xeon可能甚至可以抓取64位)。我还考虑将矩阵转换为向量以避免循环内循环结构,以及某种形式的向量化,例如在单个循环迭代中计算4个(更少?更多?)连续矩阵单元的数据。还有其他想法吗?
编辑:新的基于OpenCV的版本中的实际代码:
const char *Aptr, *Bptr;
double sumA = 0, sumB = 0, sumAsq = 0, sumBsq = 0, diffsum = 0, diffsumsq = 0;
char Aval, Bval;
for (int y=0; y < height; ++y)
{
Aptr = (char*)(AMatrix.imageData + AMatrix.widthStep * y);
Bptr = (char*)(BMatrix.imageData + BMatrix.widthStep * y);
for (int x=0; x < width; ++x)
{
Aval = Aptr[x];
Bval = Bptr[x];
sumA+=Aval;
sumB+=Bval;
sumAsq+=Aval*Aval;
sumBsq+=Bval*Bval;
diffsum+=Aval-Bval;
diffsumsq+=(Aval-Bval)*(Aval-Bval);
}
}
答案 0 :(得分:3)
你的内循环是调用函数!无论他们多么微不足道,你都要付出沉重的代价。 您应该尝试线性化矩阵访问(实际上使它们成为1D),以便您可以使用指针解除引用来访问它们
vala = *matrixA++;
valb = *matrixB++;
并且由于你是简单的加法和减法,请查看SSE / SSE2等,具体取决于您的目标CPU能力和算术(整数,浮点等)。
编辑:MMX SSE2内在函数是使用CPU SIMD指令一对一映射的函数。 请参阅these microsoft pages以开始使用,此外我建议查看英特尔网站上的IA-32/ Intel64 programmers guides或AMD的类似手册。
我还强烈推荐有关英特尔架构优化的this book。这将解释CPU和编译器的所有隐藏功能。
答案 1 :(得分:3)
各种想法:
diffsum
吗?看起来好像你可以在循环之外做diffsum=sumA-sumB
- 但是可能会有数字考虑因素阻止你这样做。答案 2 :(得分:2)
你能检查一下这个循环正在生成的汇编代码吗?如果您只使用25%的处理器,则可能是此循环受内存限制。那里有大约8个局部变量,我想编译器并没有将它们全部映射到寄存器,因此每个循环中都有很多内存操作。一个考虑因素是在汇编程序中编写该循环。
为什么逐列走矩阵?矩阵将逐行存储在内存中,因此如果您访问内部循环中的整个列,则可能需要更多内存加载到不同的内存级别(缓存等)。
答案 3 :(得分:1)
如果我在你的鞋子里,我会试着找出导致旧代码与新代码之间性能差异的确切原因。也许增强矩阵使用某种缓存或懒惰/急切的评估。
答案 4 :(得分:1)
如果你不能通过像OpenMP这样的简单设置多线程循环,你也应该尝试。 25%的CPU使用率听起来像是运行单个工作线程的四核。
答案 5 :(得分:1)
你应该尝试摆脱你的循环,并尝试矢量化操作。使用像Eigen这样的库,您的代码看起来像这样:
Eigen::MatrixXd matrixA(height, width);
Eigen::MatrixXd matrixB(height, width);
double sumA = matrixA.sum();
double sumAsq = matrixA.cwise().square().sum();
double sumB = matrixB.sum();
double sumBsq = matrixB.cwise().square().sum();
Eigen::MatrixXd diff = matrixA - matrixB;
double diffSum = diff.sum();
double diffSumSq = diff.cwise().square().sum();
return sumA + sumB / sumAsq + sumBsq * diffSum * diffSumSq;
答案 6 :(得分:1)
如果你使用“暂停”技术,它应该告诉你的不仅仅是你在那个循环中。它应该告诉你在循环中的位置。
永远猜不到什么时候可以找到答案。也就是说,这是我的猜测:-)你在浮点变量中进行所有求和,但是将原始数字作为整数字符,对吧?那么你可以期望从int到double的转换花费一些时间,如果是这样的话,你会在很长一段时间内看到你的停顿发生在那些指令中。所以基本上我想知道你为什么不用整数运算来做这一切。
你说利用率从未超过25%。可能是因为它只使用了4个核心中的一个吗?
你说利用率通常低于25%。这表明线程可能阻止进行文件I / O.如果是这样,你的停顿应该在行动中抓住它并确认它。如果是这样,您可以通过使用更大的块来加速I / O,或者可能更少地打开/关闭。请注意,对内循环的改进会缩短在该循环中花费的时间,但不会缩短在I / O中花费的时间,因此I / O中的时间百分比将增加(导致利用率明显下降) ,直到你也缩小它。
利用率实际上不是一个非常有用的数字。它只是CPU / IO分割的真正指标,如果您对其中任何一个做了太多的话,它根本不会告诉您。
正如@renick所说,摆脱地址计算。你应该能够在汇编语言层面上逐步完成这个循环,并且看到它只是你穿上你的“大师”帽并自己编写程序集时所做的更多。
无论如何,矢量化可能是一个巨大的胜利。
答案 7 :(得分:0)
在循环外存储具有相同参数的矩阵?我认为这样可以节省一些。
答案 8 :(得分:0)
“它的极限的25%,并且具有相对较大的振荡,在程序终止之前,我预期稳定,100%负载。”
你提到函数被一大堆条件和控制分支包围,所以我猜它会导致CPU管道被刷新而不是被有效利用。尝试重写您的软件,这样就不需要大量分支。
答案 9 :(得分:0)
TLDR:首先,优化矩阵乘法算法,然后观察临时数量,然后优化矩阵内部分配。
答案很长:
我认为最重要的是要优化矩阵乘法。对于最直观的算法,矩阵的乘法是O(n ^ 3)(即使对于小矩阵也是如此)。
举个例子,对于2x2矩阵乘法,你有16个乘法运算(“mo”)。对于3x3矩阵乘法,你有27 mo,对于4x4你有64 mo。
我不确定它是如何在您的情况下实现的,但如果它是直观的算法(作为三for
循环),使用LU decomposed matrices将其更改为矩阵乘法应该会大大提高您的性能
这是因为一旦你有了分解的矩阵,就可以大量优化乘法算法(没有意义就是将零元素的行和列相乘)。
此外,请考虑使用缓存值,而不是重复添加到diffsumsq
的操作:
旧代码:
diffsum+=valA-valB; // compute difference once
diffsumsq+=(valA-valB)*(valA-valB); // compute it two more times, then multiply
新代码:
diff = valA-valB; // compute difference once
diffsum+= diff; // use computed difference
diffsumsq+=diff*diff; // just multiply
差异计算的第二个变量快三倍(两个for
个循环 - x * y操作只执行一次而不是三次)。
您可以进一步监视临时对象的数量:每个二进制操作都会创建一个临时对象(这意味着在内存中分配另一个x * y矩阵并复制值)。例如表达式:
diffsumsq+=(valA-valB)*(valA-valB);
为第一个paranthesis中的差异创建一个临时值,然后为第二个中的差异创建另一个,然后为产品创建另一个。
在我上面的例子中,你最好写
diff = valA;
diff -= valB;
而不是
diff = valA - valB;
这样就可以避免分配分配给diff的临时值(在第二个版本中)。
要注意的另一件事是你的内存消耗:因为这会被执行很多,你可以预先分配内存,或者汇集使用过的矩阵并重用内存而不是创建新对象。
我确定还有其他值得关注的事情。
编辑:如何将矩阵相乘?您应该按列x行匹配它们。也就是说,valA中的列数应该等于valB中的行数(如果我记得我的矩阵乘法正确的话)。
还有一件事:
我做了计算例程a 多行#define宏来强制执行 它的内联和避免功能 来电和退货。
您不需要宏来优化C ++代码。要避免函数调用和返回,请使用inline
d函数。宏有自己的问题。