【问题标题】:Performance issues with simple calculations简单计算的性能问题
【发布时间】:2015-07-07 07:47:34
【问题描述】:

编辑 2:程序计算时间减少 16%! 计算见底部


我编写了一个 N 体模拟器,实现了 Barnes-Hut 算法。现在我有一个看起来很无辜的函数,叫做CheckNode。它简单且计算时间不长,但问题是,它被调用了 数百万 次,因此占用了每帧之间的大部分计算时间。

我分析了代码,这个函数负责84.58%的总计算时间,而且这个只有10K个粒子,当我用最多10倍这个的时候,这个函数使用的百分比越来越大。


现在这里是函数,在右侧花费的时间百分比为红色。


现在这里有一些令人担忧的事情,比如一个简单的 if 语句占用了 9.17% 和另一个 if 语句占了超过 20% 的计算时间!有没有可以在这里完成的任何优化,即使是最轻微的优化,也可以乘以数百万次函数调用以使我的程序运行得更快?

编辑:

这是CalculateForceNode函数:

void CalculateForceNode(Body* bi, Node* bj)  //bi is being attracted to bj. 15 flops of calculation
{
    //vector from the body to the center of mass
    double vectorx = bj->CenterOfMassx - bi->posX;
    double vectory = bj->CenterOfMassy - bi->posY;

    //c^2 = a^2 + b^2 + softener^2
    double distSqr = vectorx * vectorx + vectory * vectory + Softener * Softener;

    // ivnDistCube = 1/distSqr^(3/2)
    double distSixth = distSqr * distSqr * distSqr;
    double invDistCube = 1.0f / (sqrt(distSixth));

    double Accel = (bj->TotalMass * invDistCube * _GRAV_CONST);

    bi->AccelX += vectorx * Accel;
    bi->AccelY += vectory * Accel;
}

编辑 2:

优化结果

CheckNode 函数现在占用了总计算时间的 82.03%(在 1 分 37 秒的样本中测量),而之前它占用了 84.58%

现在逻辑告诉15% 的剩余计算时间,与第二个程序的剩余18% 计算时间相同。所以这些相同的时期(它的代码相同)占用了第一个程序的15%,和第二个程序的18%。让完成此其他代码的时间为x,第一个程序花费了1/0.15 = 6.666x,第二个程序花费了1/0.18 = 5.555x。然后你可以找到5.555x6.666x 的分数,它计算为~0.83,因此程序计算时间减少了 (1 - 0.83 = 0.16)16%!

【问题讨论】:

  • 在您的 if 语句中,首先检查 HasChildren,避免双重除法并执行 widthSqr
  • 我认为您应该向我们展示CalculateForceNode() 的代码,它约为45%。关于 23% 的条件,我认为你应该反转其中一个元素,你应该使用 if(pNode->HasChildren == false || (withSqr / distanceSqr HasChildren == false)
  • 另一个问题是..您是否在每个节点上多次调用 checknode,答案是否随时间而变化?如果您保存结果可能会有所帮助,因此您只需计算一次特定节点的校验节点
  • @kieren if (sqrt(x) > 6) 可能变成 if (x > 6 * 6)
  • 是的,这实际上用于检查节点功能,我检查 if (widthSqr / distanceSqr < NodeThresholdSq) 而不是 (width / distance < NodeThreshold) 我会在距离上使用 sqrt(),现在我不必这样做,但唯一的另一个sqrt left 我需要它的确切值,而不是关系

标签: c++ optimization


【解决方案1】:

我会尝试的第一件事是在您的一个条件下反转元素,替换:

if(withSqr / distanceSqr < nodeThresholdSqr || pNode->HasChildren == false)

与:

if(pNode->HasChildren == false || (withSqr / distanceSqr < nodeThresholdSqr))

如果条件的第一部分为真pNode-&gt;HasChildren == false,那么第二部分(withSqr / distanceSqr &lt; nodeThresholdSqr) 将永远不会被执行(阅读:评估)。检查简单条件比对浮点数的操作快得多(在您的情况下是除法)。你甚至可以把它提升到一个新的水平:*当pNode-&gt;HasChildren == false 时你需要计算distanceSqr 吗?

编辑:更好:

if(pNode->HasChildren == false)
{
    CalculateForceNode(pBody,pNode);
}
else
{
    double distanceSqr = ((diffX * diffX) + (diffY * diffY));
    double withSqr     = pNode->width * pNode->width;
    if(withSqr / distanceSqr < nodeThresholdSqr)
    {
        CalculateForceNode(pBody,pNode);
    }
    else
    {//if not, repeat function with child
        if(pNode->Child[0]->Bodies.size() > 0)
            CheckNode(pNode->Child[0],pBody);
        //..... - all the rest of your code
    }
}

【讨论】:

  • 不,如果HasChildren 为假,我不需要计算它。所以我应该检查HasChildren是否为真,并且只有为真,然后检查(withSqr / distanceSqr &lt; nodeThresholdSqr)
  • 没有。乔治的安排会为你做到这一点。这就是为什么它更快。根据我的评论,还可以考虑用乘法替换除法。需要明确的是,如果 HasChildren 为假,则 IF 语句返回真而不计算第二部分。
  • 这是假设编译器不能自己重新排序,我觉得这有点奇怪。
  • @Puppy - 我同意,但这并没有什么坏处,无论如何他应该尝试许多解决方案(和他的编译器)
【解决方案2】:

基于花费的时间进行分析是不够的,您需要知道这段时间花费在什么地方 - 换句话说,使用更高级的分析器。

您也没有提及有关您正在使用的编译器或平台的任何信息。

对于使用 9% 时间的 if 语句,我认为它不是用于比较,而是用于获取数据。您有多个间接级别(使用指向另一个指针的指针访问数据,依此类推)。这对缓存和分支预测不利,我猜你是因为分支未命中预测而花时间从内存中获取数据或进行无用计算,而不是进行实际比较。

我注意到的另一个注意事项:如果 (pNode->HasChildren == false) 那么您不需要进行所有计算来找到 widthSqr。我认为你应该重组你的逻辑以首先检查这一点,如果条件为假,那么你可以计算 widthSqr 并继续你的逻辑。

【讨论】:

  • 我之前用过Intel的vTune,它提供了详细的信息。
  • 如果我没记错的话它很好而且功能齐全。您可以搜索提供有关分支预测和缓存命中和未命中信息的任何其他更便宜的分析器。这将是分析代码性能的一大步。
【解决方案3】:

由于该函数被多次调用,您应该通过手动内联代码来消除调用CalculateForceNode(...) 的开销。如果你这样做,你会注意到其他的技巧:

void CheckNode(Node* pNode, Body* pBody)
{    
    double diffX = (pNode->CenterOfMass - pBody->posX);
    double diffY = (pNode->CenterOfMass - pBody->posY);

    double distanceSqr = ((diffX * diffX) + (diffY * diffY));
    double widthSqr = pNode->width * pNode->width;

    if (widthSqr / distanceSqr < NodeThresholdSqr || pNode->hasChildren == false)
    {       
        //vector from the body to the center of mass
        double vectorx = pNode->CenterOfMassx - pBody->posX;
        double vectory = pNode->CenterOfMassy - pBody->posY;

        //c^2 = a^2 + b^2 + softener^2
        double distSqr = vectorx * vectorx + vectory * vectory + Softener * Softener;

        // ivnDistCube = 1/distSqr^(3/2)
        double distSixth = distSqr * distSqr * distSqr;
        double invDistCube = 1.0f / (sqrt(distSixth));

        double Accel = (pNode->TotalMass * invDistCube * _GRAV_CONST);

        pBody->AccelX += vectorx * Accel;
        pBody->AccelY += vectory * Accel;
    }
    else
    {
       CheckChildren(pNode, pBody);
    }

}

现在您可以看到diffX = vectorxdiffY = vectorydistSqr = distanceSqr*Softner*Softner。重用一些已经进行的计算并预先计算任何可能的东西应该可以节省一些周期:

void CheckNode(Node* pNode, Body* pBody)
{    
    double diffX = (pNode->CenterOfMass - pBody->posX);
    double diffY = (pNode->CenterOfMass - pBody->posY);

    double distanceSqr = ((diffX * diffX) + (diffY * diffY));
    double widthSqr = pNode->width * pNode->width;
    double SoftnerSq = Softener * Softener;  //precompute this value

    if (widthSqr / distanceSqr < NodeThresholdSqr || pNode->hasChildren == false)
    { 
        //c^2 = a^2 + b^2 + softener^2
        double distSqr = distanceSqr + SoftnerSq;

        // ivnDistCube = 1/distSqr^(3/2)
        double distSixth = distSqr * distSqr * distSqr;
        double invDistCube = 1.0f / (sqrt(distSixth));

        double Accel = (pNode->TotalMass * invDistCube * _GRAV_CONST);

        pBody->AccelX += diffX * Accel;
        pBody->AccelY += diffY * Accel;
    }
    else
    {
       CheckChildren(pNode, pBody);
    }

}

希望这对你有用。

【讨论】:

    【解决方案4】:
    • 首先,您应该inline 函数Bodies.size() 或直接访问size,这样就没有函数调用的开销(将所有需要的信息推送到堆栈并弹出它需要时间)。
    • 我没有看到所有代码,但看起来你可以预先计算 widthSqr。当width不在函数中赋值时可以计算出来。
    • 您在这里使用了很多指针,看起来您的结构分散在整个内存中。这将产生大量的 CPU 缓存未命中。确保计算所需的所有数据都在一个长、连续且紧凑的内存区域中。
    • CalculateForceNode 中检查Softener*Softener 是否可以预先计算。 sqrt 函数非常耗时。 sqrt 算法是迭代的,因此您可以通过减少迭代次数来牺牲准确性来提高速度,或者您可以使用查找表。
    • 您在CalculateForceNode 中进行了两次相同的计算。

      void CalculateForceNode(Body* bi, Node* bj) 
          {
              //vector from the body to the center of mass
              double vectorx = bj->CenterOfMassx - bi->posX;
              double vectory = bj->CenterOfMassy - bi->posY;
      
              //c^2 = a^2 + b^2 + softener^2
              double distSqr = vectorx * vectorx + vectory * vectory...
      

    vectorx,vectory and distSqr 已在 CheckNode 中计算为 diffX, diffY and distanceSqr。手动内联整个函数CalculateForceNode

    【讨论】:

    • inline 这些天在很大程度上是多余的 - 如果编译器知道得更好,它可以并且会选择忽略你:)
    • 忽略明确的inline?我不这么认为。
    • 预先计算 widthSqr 不会有任何效果,因为无论如何每个节点只会计算一次,并且每一帧所有节点都会重置,并重新计算。
    • 关于inline 的解释非常模糊,真的。你给我这个来说服我?无论如何,看起来他正在访问某个成员size,所以他可以直接进行而不使用功能。
    【解决方案5】:

    交换 if 语句并将所有计算移到 pNode-&gt;hasChildren == false 部分中:

    void CheckChildren(Node* pNode, Body* pBody)
    {
        if (pNode->Child[0]->Bodies.size() > 0)
            CheckNode(...
    }
    
    void CheckNode(Node* pNode, Body* pBody)
    {
        if (pNode->hasChildren != false)
        {
            double diffX = (pNode->CenterOfMass - pBody->posX);
            double diffY = (pNode->CenterOfMass - pBody->posY);
    
            double distanceSqr = ((diffX * diffX) + (diffY * diffY));
            double widthSqr = pNode->width * pNode->width;
    
            if (widthSqr / distanceSqr < NodeThresholdSqr)
            {
                CalculateForceNode(pBody, pNode);
            }
            else
            {
                CheckChildren(pNode, pBody);
            }
        }
        else
        {
            CheckChildren(pNode, pBody);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-18
      • 1970-01-01
      相关资源
      最近更新 更多