[英]How to optimize this algorithm
我需要帮助才能使这段代码更快:
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
对象的指针数组,称为UnitFormation
。 UnitBase*
数组具有相同大小的数字数组,用于指示每个对应的UnitBase对象的状态,称为UnitSetup
。
我重载了[]
运算符,只返回指向那些具有特定状态的UnitBase对象的指针,所以如果我要求它的itsFormation[5]
,该函数不一定返回UnitFormation[5]
,而是UnitFormation的第5个元素其状态为UNIT_ON_FRONT
。
我尝试过使用上面的代码,但根据我的分析器,这花费了太多时间。 这是有道理的,因为算法必须在返回请求的指针之前计算所有元素。
我是否需要完全重新思考整个问题,还是可以以某种方式更快地进行?
提前致谢。
一个快速优化将是在您找到它时立即返回单元,而不是继续迭代所有其余单元,例如
if (j == offset)
unit = unitFormation[i];
变
if (j == offset)
return unitFormation[i];
当然,这只有在您正在寻找的单元朝向unitFormation序列的前面时才会有所帮助,但这样做很简单,并且有时会有所帮助。
对于每种状态,更加有效但更有效的方法是使其更快,以构建和维护具有该状态的单元的链接列表。 您可以与主单元数组并行执行此操作,链接列表的内容将指向主单元数组,因此您不会复制单元数据。 然后,要在状态中找到给定的偏移量,您可以只遍历链接列表的offset
节点,而不是遍历每个单元。
使它成为一个双向链表并保持一个尾指针可以让你找到具有高偏移的元素,就像低偏移一样快(从结束开始然后向后)。
但是,如果有很多具有相同状态的单位并且您正在寻找偏移量接近中间的单位,那么这仍然会很慢。
那么重新设计你的代码以保持一个“前面的单位”表无论那意味着什么,听起来很有趣:-)。 如果真的要查询那部分并且经常不进行修改,那么你将节省一些时间。 您无需检查整个或部分完整的单元列表,而是立即获得结果。
PS: int
应该为你的CPU使用最自然的类型,因此使用ushorts 并不一定能使你的程序更快 。
除了一些人提出的其他建议之外,您可能希望查看是否对这个函数的任何调用都是不必要的,并消除了这些调用点。 例如,如果您发现在结果无法改变的情况下重复调用此选项。 最快的代码是永不运行的代码。
是否可以按状态UNIT_ON_FRONT对数据进行排序(或插入排序)? 这将使功能变得微不足道。
一个单位的状态多久会改变一次? 也许您应该保留一个具有正确状态的单元列表,并且只在状态更改时更新该列表。
如果需要最小化状态更改的成本,您可以保留一个数组,其中显示前256个单元中有多少具有特定状态,接下来256个单元中有多少等等。一个可以扫描数组的速度是256倍一个人可以扫描单位,直到一个在第N个“好”单位的256个插槽内。 更改单元的状态只需要递增或递减一个阵列槽。
在给定各种使用模式的情况下,可以使用其他方法来平衡改变单元状态的成本与查找单元的成本。
其中一个问题可能是太频繁地调用此函数。 假设UNIT_ON_FRONT的比例是常数,则复杂度是线性的。 但是,如果从循环中调用运算符,那么复杂性将上升到O(N ^ 2)。
相反,如果你返回类似boost::filter_iterator
东西,你可以提高那些需要迭代UNIT_ON_FRONT的算法的效率。
我已经完全重新设计了解决方案,使用了两个向量,一个用于前面的单元,一个用于其他单元,并且更改了所有算法,以便具有已更改状态的单元立即从一个向量移动到另一个向量。 因此我消除了[]运算符中的计数,这是主要的瓶颈。
在使用分析器之前,我的计算时间大约为5500到7000毫秒。 在看了这里的答案之后,1)我将循环变量从ushort更改为int或uint,这将持续时间缩短了~10%,2)我在辅助算法中进行了另一次修改,将持续时间再减少了30%左右,3)我实现了如上所述的两个向量。 这有助于将计算时间从~3300 ms减少到~700 ms,另外40%!
总之,这减少了85-90%! 感谢SO和探查器。
接下来,我将实现一个中介模式,并且只在需要时调用更新函数,可能会渗出几个ms。 :)
与旧代码段对应的新代码(功能现在完全不同):
UnitBase* Formation::operator[](ushort offset)
{
if (offset < numFightingUnits)
return unitFormation[offset]->getUnit();
else
return NULL;
}
更短,更重要。 当然,还有许多其他重大修改,最重要的是unitFormation现在是std::vector<UnitFormationElement*>
而不是简单的UnitBase**
。 UnitFormationElement*
包含UnitBase*
以及之前在Formation
类中UnitBase*
一些其他重要数据。
这不应该产生很大的影响,但你可以检查程序集,看看是否每次循环迭代都加载了itsNumFightingUnits
和itsNumUnits
,或者它们是否被放入寄存器。 如果每次都加载它们,请尝试在函数开头添加临时值。
对于最后一点果汁,如果定期抛出异常,可能需要切换到返回错误代码。 这是更丑陋的代码,但缺乏堆栈跳跃可能是一个很大的帮助。 关闭异常和RTTI在游戏开发中很常见。
你是超越自己(每个人都有时会这样做)。 你做了一个简单的问题O(N ^ 2)。 想想在你超载运营商之前你必须做些什么。
添加以回应评论:
尝试退回到更简单的语言,如C或C ++的C子集。 忘记抽象,收集课程,以及所有那些hao-haw。 看看你的程序需要做什么,并以这种方式考虑你的算法。 然后,如果你可以通过使用容器类和重载来简化它,而不需要再做任何工作,那么就去做吧。 大多数性能问题都是由于通过尝试使用所有奇特的想法来解决简单的问题并使它们变得复杂。
例如,您正在使用[]
运算符,通常将其视为O(1),并将其设为O(N)。 然后我假设你在一些O(N)循环中使用它,所以得到O(N ^ 2)。 你真正想要做的是遍历满足特定条件的数组元素。 你可以这样做。 如果它们非常少,而且您的频率非常高,那么您可能需要构建一个单独的列表。 但要保持数据结构简单 , 简单 , 简单 。 最好“浪费”循环,只有在你真正需要时才进行优化。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.