假设您已经有了最佳选择算法,那么您可以提供哪些低级解决方案来从C ++代码中压缩出最后几帧呢?
不用说,这些技巧仅适用于您在事件探查器中已突出显示的关键代码部分,但它们应该是低级别的非结构性改进。我已经播了一个例子。
假设您已经有了最佳选择算法,那么您可以提供哪些低级解决方案来从C ++代码中压缩出最后几帧呢?
不用说,这些技巧仅适用于您在事件探查器中已突出显示的关键代码部分,但它们应该是低级别的非结构性改进。我已经播了一个例子。
Answers:
优化您的数据布局!(这不仅适用于C ++,还适用于更多的语言)
您可以深入研究,使其针对数据,处理器,很好地处理多核等进行专门调整。但是基本概念是这样的:
在紧密循环中处理事物时,您希望使每次迭代的数据尽可能小,并在内存中尽可能地紧密。这意味着理想的是对象(不是指针)的数组或向量,其中仅包含计算所需的数据。
这样,当CPU在循环的第一次迭代中获取数据时,接下来的几次迭代中将有价值的数据随之加载到缓存中。
确实,CPU速度很快,编译器也不错。使用更少,更快的指令并不能做太多事情。缓存一致性就在这里(我在Google上浏览过一篇随机文章-它提供了一个获取缓存一致性的好例子,该算法不能简单地线性处理数据。)
一个非常非常低级的技巧,但可以派上用场:
大多数编译器支持某种形式的显式条件提示。GCC具有一个称为__builtin_expect的函数,该函数可让您通知编译器结果的值可能是什么。GCC可以使用这些数据来优化条件,以便在预期情况下尽快执行,在意外情况下执行速度稍慢。
if(__builtin_expect(entity->extremely_unlikely_flag, 0)) {
// code that is rarely run
}
正确使用此功能,可以使速度提高10-20%。
您需要了解的第一件事是运行的硬件。它如何处理分支?缓存呢?它有SIMD指令集吗?它可以使用多少个处理器?是否需要与其他人共享处理器时间?
您可能会以非常不同的方式解决相同的问题-甚至您选择的算法也应取决于硬件。在某些情况下,O(N)的运行速度可能比O(NlogN)慢(取决于实现方式)。
作为优化的粗略概述,我要做的第一件事是准确地查看要解决的问题和数据。然后为此进行优化。如果您想获得出色的性能,那么就不用考虑通用的解决方案了-您可以对所有不常用的情况进行特殊处理。
然后进行配置。个人资料,个人资料,个人资料。查看内存使用情况,查看分支惩罚,查看函数调用开销,查看管道利用率。找出使您的代码变慢的原因。可能是数据访问(我写了一篇名为“ The Latency Elephant”的文章,介绍了数据访问的开销-用谷歌搜索。由于我没有足够的“信誉”,所以我无法在此处发布2个链接),因此请仔细检查并然后优化您的数据布局(漂亮的大平面同质数组很棒)和数据访问(在可能的情况下预取)。
将内存子系统的开销降至最低后,请尝试确定指令现在是否已成为瓶颈(希望它们已成为瓶颈),然后查看算法的SIMD实现-数组结构(SoA)实现可能非常数据化,指令缓存效率高。如果SIMD不能很好地解决您的问题,则可能需要内部函数和汇编程序级别的编码。
如果您仍然需要更高的速度,请平行进行。如果您可以在PS3上运行,那么SPU就是您的朋友。使用它们,爱它们。如果您已经编写了SIMD解决方案,那么使用SPU将会获得巨大的好处。
然后,再配置一些。在游戏场景中进行测试-此代码仍然是瓶颈吗?您能否更改更高级别使用此代码的方式以最大程度地减少其使用(实际上,这应该是您的第一步)?您可以将计算推迟到多个框架吗?
无论您使用哪种平台,都将尽可能多地了解可用的硬件和分析器。不要以为您知道瓶颈是什么-通过分析器找到它。并确保您有试探性的方法来确定您是否确实使游戏运行得更快。
然后再次对其进行配置。
第一步:仔细考虑与算法有关的数据。O(log n)并不总是比O(n)快。一个简单的例子:只有几个键的哈希表通常最好用线性搜索代替。
第二步:查看生成的程序集。C ++为表带来了很多隐式代码生成。有时,它会在您不知情的情况下偷偷溜走。
但是假设这确实是最紧要的时间:Profile。说真的 随机应用“性能技巧”可能会带来伤害,而可能会有所帮助。
然后,一切都取决于您的瓶颈所在。
数据缓存未命中=>优化数据布局。这是一个很好的起点:http : //gamesfromwithin.com/data-oriented-design
代码缓存未命中=>查看虚拟函数调用,过多的调用栈深度等。导致性能下降的常见原因是错误地认为基类必须是虚拟的。
其他常见的C ++性能接收器:
当您查看程序集时,以上所有内容都是显而易见的,因此请参见上文;)
删除不必要的分支
在某些平台和某些编译器上,分支可能会丢弃整个管道,因此即使微不足道的if()块也可能会很昂贵。
PowerPC体系结构(PS3 / x360)提供了浮点选择指令fsel
。如果块是简单分配,则可以在分支位置使用它:
float result = 0;
if (foo > bar) { result = 2.0f; }
else { result = 1.0f; }
成为:
float result = fsel(foo-bar, 2.0f, 1.0f);
当第一个参数大于或等于0时,返回第二个参数,否则返回第三个。
失去分支的代价是将同时执行if {}和else {}块,因此,如果一个操作很昂贵或取消引用NULL指针,则此优化不适合。
有时您的编译器已经完成了这项工作,因此请首先检查您的程序集。
以下是有关分支和fsel的更多信息:
我的基本原则是:不要做任何不必要的事情。
如果发现某个特定功能是瓶颈,则可以优化该功能-或者可以尝试避免首先调用它。
这并不一定意味着您正在使用错误的算法。例如,这可能意味着您正在对可能缓存一小段时间(或完全预先计算)的每个帧进行计算。
在进行任何真正的低级优化之前,我总是尝试这种方法。
最小化依赖链,以更好地利用CPU管线。
在简单的情况下,如果启用循环展开,则编译器可能会为您执行此操作。但是,它通常不会这样做,尤其是当涉及浮点数时,对表达式进行重新排序会改变结果。
例:
float *data = ...;
int length = ...;
// Slow version
float total = 0.0f;
int i;
for (i=0; i < length; i++)
{
total += data[i]
}
// Fast version
float total1, total2, total3, total4;
for (i=0; i < length-3; i += 4)
{
total1 += data[i];
total2 += data[i+1];
total3 += data[i+2];
total4 += data[i+3];
}
for (; i < length; i++)
{
total += data[i]
}
total += (total1 + total2) + (total3 + total4);
不要忽视您的编译器-例如,如果您在Intel上使用gcc,则可以通过切换到Intel C / C ++编译器轻松获得性能提升。如果您以ARM平台为目标,请查看ARM的商业编译器。如果您使用的是iPhone,Apple允许从iOS 4.0 SDK开始使用Clang。
优化(尤其是在x86上)可能会涉及到一个问题,那就是在现代CPU实现上最终会遇到很多直观的问题。不幸的是,对于我们大多数人来说,对编译器进行优化的能力早已消失了。编译器可以根据自身对CPU的内部知识来调度流中的指令。此外,CPU还可以根据自身需求重新安排指令。即使您想到了一种安排方法的最佳方式,也很可能编译器或CPU已经自行提出了建议,并且已经执行了该优化。
我最好的建议是忽略低级优化,而专注于高级优化。无论它们的性能如何,编译器和CPU都无法将您的算法从O(n ^ 2)更改为O(1)算法。这将要求您仔细查看要尝试做的事情,并找到一种更好的方法来做到这一点。让编译器和CPU担心底层,而将精力集中在中高层。
该限制关键字是潜在的方便,尤其是在你需要操纵的对象与指针的情况。它允许编译器假定所指向的对象不会以任何其他方式修改,从而使编译器可以执行更积极的优化,例如将对象的某些部分保留在寄存器中或更有效地对读写进行重新排序。
关于关键字的一个好处是,它是一个提示,您可以一次应用它,而无需重新安排算法就可以从中受益。不利的一面是,如果在错误的地方使用它,则可能会看到数据损坏。但是通常很容易发现在哪里使用它是合法的-这是可以合理地期望程序员知道比编译器可以安全假设的更多信息的少数示例之一,这就是引入关键字的原因。
从技术上讲,“限制”在标准C ++中并不存在,但是大多数C ++编译器都可以使用特定于平台的等效项,因此值得考虑。
另请参阅:http : //cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html
const一切!
您向编译器提供的有关数据的信息越多,优化效果就越好(至少以我的经验而言)。
void foo(Bar * x) {...;}
成为
void foo(const Bar * const x) {...;}
现在,编译器知道指针x不会改变,并且指向的数据也不会改变。
另一个额外的好处是,您可以减少意外错误的数量,阻止自己(或其他人)修改不应该的错误。
const
不能改善编译器优化。的确,如果编译器知道变量不会改变,但const
不能提供足够强的保证,则可以生成更好的代码。
通常,获得性能的最佳方法是更改算法。实施的通用性越差,您就越接近金属。
假设已经完成了...
如果确实是至关重要的代码,请尝试避免读取内存,并避免计算可以预先计算的内容(尽管没有查找表,因为它们违反了规则1)。知道算法的作用,并以编译器也知道的方式编写它。检查组装以确保确实如此。
避免缓存未命中。尽可能多地进行批处理。避免使用虚函数和其他间接方式。
最终,衡量一切。规则一直在变化。3年前用来加速代码的功能现在使其速度变慢。一个很好的例子是“使用双数学函数而不是浮点型”。如果我不读的话,我不会意识到的。
我忘记了-不要让默认的构造函数初始化您的变量,或者,如果您坚持要这么做,至少也要创建不这样做的构造函数。请注意配置文件中未显示的内容。当您在每行代码中丢失一个不必要的周期时,分析器中将不会显示任何内容,但是总体上将损失很多周期。同样,知道您的代码在做什么。使您的核心功能精简而不是万无一失。如果需要,可以调用万无一失的版本,但并非总是需要。多功能性需要付出代价-性能是其中之一。
编辑解释为什么没有默认初始化:很多代码说:Vector3 bla; bla = DoSomething();
构造函数中的初始化浪费时间。同样,在这种情况下,浪费的时间很小(可能清除了向量),但是,如果您的程序员习惯性地这样做,则会加起来。另外,许多函数会创建一个临时(请考虑重载的运算符),将其初始化为零并立即分配。隐藏的丢失周期太小,无法在探查器中看到峰值,但整个代码库中的渗漏周期却很大。另外,有些人在构造函数上做得更多(这显然是不行的)。我已经从一个未使用的变量中获得了数毫秒的收益,其中构造函数恰好在繁重的一面。一旦构造函数产生副作用,编译器将无法对其进行优化,因此,除非您从不使用上述代码,否则我更喜欢非初始化构造函数,或者,正如我所说,
Vector3 bla(noInit); bla = doSomething();
const Vector3 = doSomething()
?然后,可以开始执行返回值优化,并可能省去一两个赋值。
减少布尔表达式评估
这真的很绝望,因为这是对代码的非常微妙但危险的更改。但是,如果您对一个条件进行了无数次的求值,则可以通过使用按位运算符来减少布尔求值的开销。所以:
if ((foo && bar) || blah) { ... }
成为:
if ((foo & bar) | blah) { ... }
而是使用整数算术。如果您的foo和bar是常量或在if()之前求值,则它可能比普通的布尔版本快。
另外,算术版本的分支少于常规布尔版本的分支。这是另一种优化方法。
最大的缺点是您失去了懒惰的评估-整个区块都被评估了,所以您不能这样做foo != NULL & foo->dereference()
。因此,很难维护它是有争议的,因此折衷可能会太大。