如何利用已知的瓶颈优化计算密集型C ++程序?

时间:2010-07-29 10:50:32

标签: c++ performance visual-c++ optimization

我正在为我的大学开发一些科学软件。它是用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);
    }
}

10 个答案:

答案 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)

各种想法:

  • 你说你只能实现大约25%的CPU负载。我可以想到两个原因:
    1. 你正在交换。你的矩阵大小是多少?它们完全适合物理记忆吗?查看应用程序的内存使用情况和工作集大小。
    2. 应用程序代码的其余部分在I / O上阻塞。围绕核心例程的代码是否可以执行任何I / O操作?它可能会在很长一段时间内阻塞,但当然你没有看到使用“暂停时繁忙和检查”技术,因为无论何时进程再次解锁,它都会直接进入计算密集型核心例程。
  • 查看核心例程的汇编代码。它看起来合理吗?
  • 你真的需要在循环中计算diffsum吗?看起来好像你可以在循环之外做diffsum=sumA-sumB - 但是可能会有数字考虑因素阻止你这样做。
  • 正如renick已经评论过,这看起来像是SSE优化的主要目标。同样,您应该确保编译器生成合理的汇编代码(如果您使用内在函数而不是自己编写程序集)。
  • 如果您不想自己编写SSE代码,至少要确保编译器的SSE标志已设置。这将允许编译器使用SSE单元而不是FPU进行标量浮点运算,这本身将提高性能,因为x86上的基于堆栈的FPU非常不适合编译器代码生成。

答案 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管道被刷新而不是被有效利用。尝试重写您的软件,这样就不需要大量分支。

我还建议使用其中一个数学库,如EigenATLASGSL

答案 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函数。宏有自己的问题。