【问题标题】:How to optimize this algorithm如何优化这个算法
【发布时间】:2010-07-02 22:10:04
【问题描述】:

我需要帮助来加快这段代码的速度:

UnitBase* Formation::operator[](ushort offset)
{
 UnitBase* unit = 0;
 if (offset < itsNumFightingUnits)
 {
  ushort j = 0;
  for (ushort i = 0; i < itsNumUnits; ++i)
  {
   if (unitSetup[i] == UNIT_ON_FRONT)
   {
    if (j == offset)
     unit = unitFormation[i];
    ++j;
   }
  }
 }
 else
  throw NotFound();
 return unit;
}

所以,为了提供一些背景知识,我有这个类Formation,它包含一个指向UnitBase 对象的指针数组,称为UnitFormationUnitBase* 数组有一个大小相等的数字数组,表示每个对应 UnitBase 对象的状态,称为 UnitSetup

我已经重载了[]操作符,以便只返回指向那些具有特定状态的UnitBase对象的指针,所以如果我要求itsFormation[5],该函数不一定返回UnitFormation[5],而是第5个元素状态为 UNIT_ON_FRONT 的 UnitFormation。

我已经尝试使用上面的代码,但根据我的分析器,它花费了太多时间。这是有道理的,因为算法必须在返回请求的指针之前计算所有元素。

我是否需要彻底重新考虑整个问题,或者可以以某种方式更快地做到这一点?

提前致谢。

【问题讨论】:

  • 将 ushort 更改为 unsigned int 或 size_t

标签: c++ performance algorithm optimization


【解决方案1】:

一个快速的优化方法是在找到该单元后立即返回它,而不是继续迭代所有其余的单元,例如

if (j == offset)
 unit = unitFormation[i];

变成

if (j == offset)
 return unitFormation[i];

当然,这仅在您要查找的单元位于 unitFormation 序列前面的情况下才有帮助,但这样做很简单,有时确实有帮助。

一个更复杂但更有效的方法是,为每个状态建立和维护具有该状态的单位的链接列表。您将与主单元数组并行执行此操作,并且链接列表的内容将是指向主单元数组的指针,因此您不会复制单元数据。然后,要在状态中找到给定的偏移量,您可以只遍历链表的offsetth 节点,而不是遍历每个单元。

使它成为一个双向链表并保留一个尾指针,这样您就可以像找到低偏移量一样快速地找到具有高偏移量的元素(通过从末端开始并向后移动)。

但是,如果有很多具有相同状态的单元并且您正在寻找偏移量接近中间的单元,这仍然会很慢。

【讨论】:

  • 我之前有过这样的情况(立即返回),但编译器抱怨,因为我还在抛出代码之后删除了返回。现在,如果找到该单元,我将中断,因此它应该提供与您所说的相同的功能。事实上,它确实提供了一点改进,比如 10%-20% 左右,但它仍然太慢了。问题是这个特定的运算符在我正在尝试的这个测试中被使用了大约 300k 次,所以平均有 1200 个单元,这给了很多行要通过...
  • 并行链表的想法听起来很有趣。我可能会尝试一下。或者在阵列结束时将“坏”单位换成“好”单位?我想可能会,但那样的话我会失去每个单元前面的位置......啊!
  • @Kristian:对于那种速度和随机访问,链表是不合适的。使用 std::vector 代替在您的前端单元上保持标签。您将付出加法/插入速度较慢的代价,但查找速度会快得多。
  • 欧文是对的;这是一个权衡。如果单元经常改变状态,链表是最好的主意,因为它们的插入/删除速度快但查找速度慢(但仍然比现在的查找速度更快,只要不是所有 - 或几乎所有 - 单元都在同一个状态)。如果单位很少更改状态,并且经常查找它们,那么为每个状态维护指向单位的指针向量将使您的查找速度更快,但状态更改速度要慢得多。
  • @Kristian 您的编译器在抱怨,因为它不够聪明,无法在编译时知道j == offset 条件在循环中的某个时刻总是会计算为真。你可以在最后用return NULL 来安抚编译器,因为你知道它永远不会到达,即使编译器没有。如您所见,break 也可以使用。
【解决方案2】:

无论这意味着什么,重新设计代码以维护“前面的单元”表怎么样,听起来很有趣:-)。如果该部分确实被大量查询并且不经常修改,那么您将节省一些时间。您无需检查完整的单位列表的全部或部分内容,而是立即获得结果。

P.S.:int 应该为你的 CPU 使用最自然的类型,所以使用 ushorts doesn't make necessarily your program faster

【讨论】:

  • +1。引用 Mike Abrash 的话,按原样优化你的慢速算法将导致快速、慢速的代码。重新考虑您的数据结构,以便通过设计使常见操作更快。
  • 我怀疑...顺便说一句,int 真的会比 ushort 快吗?
  • 可能 - 每次加载一个短片时,它都必须做一些额外的移位或与清除高 16 位。它们会随着时间的推移而累积。
  • 您永远无法确定,但在您的分析器没有告诉您之前,我强烈建议您在处理数据类型之前考虑算法。用算法赢得 50% 比调整 2% 性能更好,因为如果它带来那么多,你会使用短裤而不是整数。你有责任在算术等过程中处理溢出(无论如何你都有,但溢出时短裤会更快:-D)。
  • Jdeehan,你是对的!我尝试了这段代码: for (ushort i = 0; i
【解决方案3】:

除了一些人提出的其他建议之外,您可能还想看看是否对这个函数的任何调用是不必要的,并消除这些调用点。例如,如果你看到你在没有机会改变结果的情况下重复调用它。最快的代码是永远不会运行的。

【讨论】:

    【解决方案4】:

    是否可以按状态 UNIT_ON_FRONT 对数据进行排序(或插入排序)?这将使函数变得微不足道。

    【讨论】:

    • 这就是我一直在想的。如果一个单元的状态更改为 UNIT_NOT_ON_FRONT,它将与前面的一个单元交换,最终所有活动单元都位于阵列的开头。但是我也会失去我在其他地方使用的单位的顺序......
    • 如果需要排序,那么可能按最常用的排序或维护 2 个排序列表。双倍的输入时间,但再次获取时间可以忽略不计。
    【解决方案5】:

    单位的状态多久会改变一次?也许您应该保留一份具有适当状态的单位列表,并且仅在状态更改时更新该列表。

    如果有必要将状态更改的成本降至最低,您可以保留一个数组,其中说明前 256 个单元中有多少具有特定状态,接下来的 256 个单元中有多少个等。一个可以扫描数组 256 次尽可能快地扫描单元,直到一个在第 N 个“好”单元的 256 个插槽内。改变一个单元的状态只需要增加或减少一个数组槽位。

    根据不同的使用模式,可以使用其他方法来平衡更改单位状态的成本和寻找单位的成本。

    【讨论】:

    • 谢谢,我会花更多的时间寻找单位而不是改变他们的状态。在最坏的情况下,所有单位都会更改其状态一次,但我将平均查找每个特定单位 100-200 次。
    【解决方案6】:

    其中一个问题可能是该函数可能被过于频繁地调用。假设 UNIT_ON_FRONT 的比例是常数,复杂度是线性的。但是,如果您从循环中调用运算符,则复杂性将上升到 O(N^2)。

    如果您返回类似 boost::filter_iterator 的内容,则可以提高那些需要迭代 UNIT_ON_FRONT 的算法的效率。

    【讨论】:

    • 是的,没错。我想我将添加一些状态标志来指示何时发生了变化。这应该消除相当多的迭代。将看看使用 filter_iterator,谢谢。目前我正在尝试使用矢量,弹出和推送。我们会看看情况如何。
    【解决方案7】:

    我已经完全重新设计了解决方案,使用两个向量,一个用于前面的单元,一个用于其他单元,并更改了所有算法,以便状态更改的单元立即从一个向量移动到另一个向量。因此,我消除了 [] 运算符中的计数,这是主要的瓶颈。

    在使用分析器之前,我的计算时间大约为 5500 到 7000 毫秒。看了这里的答案后, 1) 我将循环变量从 ushort 更改为 int 或 uint,这将持续时间减少了约 10%, 2) 我对二级算法进行了另一次修改,以将持续时间进一步减少 30% 左右, 3)我实现了上面解释的两个向量。这有助于将计算时间从约 3300 毫秒减少到约 700 毫秒,再减少 40%!

    总共减少了 85 - 90%!感谢 SO 和分析器。

    接下来我将实现一个中介者模式,只在需要时调用更新函数,可能会多花几毫秒。 :)

    对应旧sn-p的新代码(现在功能完全不同了):

    UnitBase* Formation::operator[](ushort offset)
    {
        if (offset < numFightingUnits)
            return unitFormation[offset]->getUnit();
        else
            return NULL;
    }
    

    更短,更切中要害。当然,还有许多其他重大修改,最重要的是 unitFormation 现在是 std::vector&lt;UnitFormationElement*&gt; 而不是简单的 UnitBase**UnitFormationElement* 包含 UnitBase* 以及之前在 Formation 类中徘徊的一些其他重要数据。

    【讨论】:

    • 您应该发布代码。关于分析的一个有趣的事情是,当您进行改进以改进事物时,分析器将向您显示可能需要解决的下一个部分。这有时会引发有趣的想法。
    • 当然,刚刚编辑了上面的帖子。是的,现在电子的主要消费者似乎是 ntdll.dll,有趣的是以下行:if ((float) rand() / RAND_MAX &lt;= chanceDefenseBreached。是 cast 还是 rand() 函数?
    • @Kristian:好的,你明白了,正如@EvilTeach 所说,继续这样做。您将获得 10 倍的加速,我敢打赌您可以获得更多,并且在此过程中,您正在简化数据结构
    • @Kristian:您可以通过多次暂停来回答是强制转换还是 rand 函数,并查看它在一个与另一个之间的频率。我猜rand,但我可能错了。
    • @Kristian:这是一种很少教但非常有效的回答性能问题的方法。基本上,您在程序运行时随机暂停程序并检查其状态。您不必做很多次就可以很好地了解时间的去向,按百分比计算。这里有更多的讨论:stackoverflow.com/questions/1777556/alternatives-to-gprof/…
    【解决方案8】:

    这应该不会产生很大的影响,但是您可以检查程序集以查看是否在每次循环迭代时都加载了 itsNumFightingUnitsitsNumUnits 或者是否将它们放入了寄存器中。如果每次都加载它们,请尝试在函数开头添加临时对象。

    【讨论】:

      【解决方案9】:

      对于最后一点,如果定期抛出异常,则可能值得切换为返回错误代码。这是更丑陋的代码,但没有堆栈跳转可能会有很大帮助。关闭异常和 RTTI 在游戏开发中很常见。

      【讨论】:

      • 永远不应该抛出异常,因为代码会处理这个问题。不过感谢您的提示,我认为我不会过多使用异常,除非性能不是问题。
      【解决方案10】:

      你比自己聪明(有时每个人都会这样做)。你做了一个简单的问题 O(N^2)。在重载运算符之前想想你必须做什么。

      为回应评论添加:

      尝试使用更简单的语言,例如 C 或 C++ 的 C 子集。忘掉抽象、集合类,以及所有那些花里胡哨的东西。看看你的程序需要做什么,并以这种方式思考你的算法。然后,如果您可以通过使用容器类和重载来简化它,而不需要让它做更多的工作,那就去做吧。大多数性能问题是由于尝试使用所有花哨的想法而使问题变得复杂而导致的。

      例如,您使用[] 运算符,通常被认为是 O(1),并将其变为 O(N)。然后我假设你在一些 O(N) 循环中使用它,所以你得到 O(N^2)。您真正想要做的是遍历满足特定条件的数组元素。你可以这样做。如果它们很少,并且您以非常高的频率执行此操作,您可能需要为它们建立一个单独的列表。但是保持你的数据结构simplesimplesimple。最好是“浪费”周期,并且仅在确实需要时进行优化。

      【讨论】:

        猜你喜欢
        • 2011-03-05
        • 1970-01-01
        • 1970-01-01
        • 2013-05-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-01-15
        • 2021-09-14
        相关资源
        最近更新 更多