游戏编程的C ++ - 爱还是不信任?

时间:2009-05-06 13:48:39

标签: c++ performance

以游戏编程的效率为名,一些程序员不信任几个C ++特性。我的一位朋友声称了解游戏行业的运作方式,并会提出以下意见:

  • 不要使用智能指针。游戏中没有人会这样做。
  • 游戏编程中不应使用(通常不会)例外记忆和速度。

这些陈述中有多少是真的?设计C ++功能时要牢记效率。这种效率不足以进行游戏编程吗? 97%的游戏编程?

C-way-of-thinking仍然似乎对游戏开发社区有很好的把握。这是真的吗?

我观看了GDC 2009中关于多核编程的另一个视频。他的演讲几乎完全针对单元编程,在处理之前需要进行DMA传输(简单的指针访问不适用于Cell的SPE) 。他不鼓励使用多态,因为指针必须“重新基于”DMA传输。多么悲伤。这就像回到广场一样。我不知道是否有一个优雅的解决方案来编程Cell上的C ++多态。 DMA传输的主题是深奥的,我在这里没有太多背景。

我同意C ++对于那些希望使用小语言进行破解而不是阅读书籍堆栈的程序员来说也不是很好。模板也吓坏了调试。你是否同意游戏社区过分担心C ++?

15 个答案:

答案 0 :(得分:63)

我参与的最后一场比赛是PS3上的Heavenly Sword,它是用C ++编写的,甚至是单元格代码。在此之前,我做了一些PS2游戏和PC游戏,他们也是C ++。非项目使用智能指针。不是因为任何效率问题,而是因为它们通常不需要。游戏,尤其是控制台游戏,在正常播放期间不使用标准内存管理器进行动态内存分配。如果有动态物体(导弹,敌人等),那么它们通常是预先分配的,并根据需要重新使用。每种类型的对象都会对游戏可以处理的实例数量设置上限。这些上限将由所需的处理量(太多而游戏速度慢到爬行)或RAM存在量(太多而且您可能经常开始寻呼到磁盘,这会严重降低性能)来定义。

游戏通常不使用例外,因为游戏不应该有错误,因此无法产生异常。对于控制台制造商测试游戏的控制台游戏尤其如此,尽管最近的平台如360和PS3似乎确实有一些游戏可能会崩溃。说实话,我没有在网上看到任何启用例外的实际成本是什么。如果仅在抛出异常时产生成本,那么没有理由不在游戏中使用它们,但我不确定并且它可能取决于所使用的编译器。通常,游戏程序员知道何时可以使用业务应用程序中的异常(IO和初始化等)处理问题并在不使用异常的情况下处理它们(可能!)。

然而,在全球范围内,C ++正在逐渐减少作为游戏开发的语言。 Flash和Java可能拥有更大的市场份额,它们确实有异常和智能指针(以托管对象的形式)。

对于Cell指针访问,当代码在任意基地址被DMA进入Cell时会出现问题。在这种情况下,代码中的任何指针都需要使用新的基地址“固定”,这包括v表,并且您并不真正想要为加载到Cell中的每个对象执行此操作。如果代码总是在固定地址加载,那么就不需要修复指针。虽然因为限制了代码存储的位置,但您会失去一点灵活性。在PC上,代码在执行期间永远不会移动,因此永远不需要在运行时修复指针。

我真的不认为任何人'不信任'C ++特性 - 不相信编译器完全是另外一些新的,像Cell这样的深奥体系结构往往会在C ++之前获得强大的C编译器,因为C编译器更容易制作而不是C ++。

答案 1 :(得分:57)

看,你听到的大多数任何人关于编程效率的一切都是神奇的思考和迷信。智能指针确实有性能成本;特别是如果你在内循环中做了很多奇特的指针操作,它可能会有所作为。

可能。

但是当人们这样的事情时,通常是很久以前就告诉他们X是真的,没有任何东西,只有直觉的结果。现在,细胞/多态性问题听起来似乎是合理的 - 我打赌它对第一个说出来的人做了。但我还没有证实。

你会听到关于操作系统的C ++的相同内容:它太慢了,它会做你想要做得好的事情。

然而,我们完全用C ++构建OS / 400(来自v3r6),裸机启动,并且获得了快速,高效和小型的代码库。这需要一些工作;特别是在裸机上工作,有一些引导问题,使用新的布局,这类事情。

C ++可能是一个问题只是因为它太大了:我现在正在重读Stroustrup的腕带,而且它非常令人生畏。但我不认为有任何固有的东西表明你不能在游戏编程中有效地使用C ++。

答案 2 :(得分:12)

如果您或您的朋友对性能非常偏执,那么请阅读英特尔有关优化的手册。乐趣。

否则,每次都要求正确性,可靠性和可维护性。我宁愿玩一个比崩溃的游戏慢一点的游戏。如果/当您注意到性能问题时,PROFILE然后进行优化。您可能会发现有一些热点代码可以通过使用更高效的数据结构或算法来提高效率。只有在分析表明它们是获得有价值的加速的唯一方式时,才会对这些愚蠢的小微博优化感到烦恼。

所以:

  1. 编写清晰明确的代码
  2. 资料
  3. 配置文件
  4. 您可以使用更有效的数据结构或算法来加速瓶颈吗?
  5. 使用微优化作为最后的手段,只有在分析显示它会有帮助的地方
  6. PS:许多现代C ++编译器提供了一种异常处理机制,除了抛出异常外,它还增加了零执行开销。也就是说,只有在实际抛出异常时才会降低性能。只要异常仅用于特殊情况,那么没有充分的理由不使用它们。

答案 3 :(得分:7)

我在StackOverflow上看到了一篇文章(我似乎找不到了,所以也许它没有发布在这里),它查看了异常与错误代码的相对成本。很多时候人们会看到#34;代码有异常" vs."没有错误处理的代码",这不是公平的比较。如果您要使用异常,那么不使用它们就必须使用其他东西来实现相同的功能,而其他东西通常是错误返回代码。他们发现,即使在一个简单的示例中,单个函数调用级别(因此不需要在调用堆栈中传播异常),在错误情况发生的情况下,异常比错误代码快0.1% - 0.01%的时间或更少,而错误代码在相反的情况下更快。

与上述关于测量异常与无错误处理的投诉类似,人们在虚拟函数方面更经常地进行推理时会出现这种错误。就像你不使用异常作为从函数中返回动态类型的方法(是的,我知道,所有你的代码都是例外的),你不能做出功能虚拟,因为你喜欢它在语法高亮显示中的外观。您将函数设置为虚拟,因为您需要特定类型的行为,因此您不能说虚拟化很慢,除非您将其与具有相同操作的内容进行比较,并且通常替换是大量的switch语句或批量代码重复。那些也有性能和内存命中率。

关于游戏没有错误和其他软件的评论,我可以说的是,我显然没有玩过任何软件公司制作的游戏。我在Pokemon的精英4的地板上冲浪,被困在Oblivion的一座山内,被Gloams杀死,意外地将他们的法力伤害与他们的hp伤害结合起来,而不是在暗黑破坏神II中单独进行,并推我自己穿过一个带有大石头的封闭大门,在暮光公主中与一只鸟和一个弹弓对抗哥布林。软件有bug。使用例外并没有使无错误的软件错误。

标准库的异常机制有两种类型的例外:std::runtime_errorstd::logic_error。我可以看到不想使用std::logic_error(我已经将它用作临时性的东西来帮助我测试,目标是最终将其删除,并且我还将其作为永久性保留校验)。但是,std::runtime_error不是一个错误。如果我连接的服务器向我发送无效数据(安全编程的规则#1:不信任任何人,甚至是您认为自己编写的服务器),我会抛出从std::runtime_error派生的异常,例如声称它们是向我发送一个12字节的消息,然后他们实际上发送给我15.在这种情况下,只有两种可能性:

1)我连接到恶意服务器或

2)我与服务器的连接已损坏。

在这两种情况下,我的回答都是一样的:断开连接(无论我在代码中的哪个位置,因为我的析构函数会为我清理),等待几秒钟,然后尝试再次连接到服务器。我做不了什么。我绝对可以给出所有错误代码(这意味着通过引用传递其他所有东西,这是一个性能损失,并严重混乱代码),或者我可以抛出一个异常,我在我的代码中找到哪个我确定哪些服务器连接到(在我的代码中可能会非常高)。

我在代码中提到的是否有任何错误?我不这么认为;我认为它接受我必须与之交互的所有其他代码都是不完美或恶意的,并且确保我的代码在面对这种模糊性时仍然保持高效。

对于智能指针,再次,您尝试实现的功能是什么?如果您需要智能指针的功能,那么不使用智能指针意味着手动重写其功能。我认为为什么这是一个坏主意很明显。但是,我很少在自己的代码中使用智能指针。我真正做的唯一一次是,如果我需要在标准容器中存储一些多态类(比如,std::map<BattleIds, Battles>其中Battles是基于战斗类型派生的某个基类),在哪种情况下,我使用了std::unique_ptr。我相信有一次我在类中使用std::unique_ptr来处理一些库代码。我使用std::unique_ptr的大部分时间都是使不可复制的,不可移动的类型可移动。但是,在许多使用智能指针的情况下,在堆栈上创建对象并从方程中完全删除指针似乎更好。

在我的个人编码中,我还没有真正找到许多情况,其中&#34; C&#34;代码的版本比&#34; C ++&#34;更快。版。事实上,它通常是相反的。例如,考虑std::sortqsort(Bjarne Stroustrup使用的常见示例)的许多示例,其中std::sort clobbers qsortmy recent comparison of std::copy vs. memcpy,其中std::copy实际上具有轻微的性能优势。

太多的&#34; C ++特征X太慢了#34;声称似乎是基于比较它没有功能。最高性能(在速度和内存方面)和无错误代码是int main() {},但我们编写程序来做事情。如果您需要特定的功能,那么不使用为您提供该功能的语言功能将是愚蠢的。但是,您应该首先考虑您希望程序执行的操作,然后找到执行此操作的最佳方法。显然你不想开始&#34;我想编写一个使用C ++特征X的程序&#34;,你想以#34开头;我想写一个程序,很酷的事情Z&#34;也许你最终会在......并且最好的方法就是实现X&#34;。

答案 4 :(得分:6)

许多人对事物做出绝对陈述,因为他们实际上并没有思考。他们宁愿只是应用规则,使事情变得更乏味,但需要更少的设计和预见。我现在宁愿做一些艰难的思考,然后当我做一些毛茸茸的事情,并抽象掉那些乏味的东西,但我想不是每个人都这么想。当然,智能指针具有性能成本。做例外。这只是意味着可能是您的代码的一些部分,您不应该使用它们。但是你应该首先剖析并确保问题实际上是什么。

免责声明:我从未做过任何游戏编程。

答案 5 :(得分:5)

关于Cell架构:它有一个不连贯缓存。每个SPE都有自己的256 KB本地存储。 SPE只能访问这个内存;必须使用DMA访问任何其他内存,例如512 MB主内存或另一个SPE的本地存储。您可以手动执行DMA,并通过显式启动DMA传输将内存复制到本地存储中。这使同步成为一个巨大的痛苦。

或者,您实际上可以访问其他内存。主内存和每个SPE的本地存储都映射到64位虚拟地址空间的某个部分。如果通过正确的指针访问数据,DMA就会在幕后发生,而且它们看起来都像是一个巨大的共享内存空间。问题?巨大的表现受到打击。每次访问其中一个指针时,SPE都会在DMA发生时停止。这很慢,而且你不想在性能关键代码(即游戏)中做些什么。

这将我们带到Skizz's point关于vtable和指针修复。如果你盲目地复制SPE之间的vtable指针,如果你没有修改你的指针,你将会受到巨大的性能影响,如果你做的话,你也会受到巨大的性能影响。 修复指针并将虚拟功能代码下载到SPE。

答案 6 :(得分:5)

我在索尼的一次精彩演讲中称之为“面向对象编程的陷阱”。这一代控制台硬件确实让很多人重新审视了C ++的OO方面,并开始询问它是否真的是最好的前进方式。

您可以找到演示文稿here(直接链接here)。也许你会发现这个例子有点做作,但希望你会发现这种对高度抽象的面向对象设计的厌恶并不总是基于神话和迷信。

答案 7 :(得分:4)

我过去用C ++编写了小游戏,目前正在使用C ++用于其他高性能应用程序。在整个代码库中不需要使用每个C ++特性。

因为C ++是(几乎,减去一些东西)C的超集,所以你可以在需要时编写C样式代码,同时在适当的时候利用额外的C ++特性。

考虑到一个不错的编译器,C ++可以和C一样快,因为你可以在C ++中编写“C”代码。

和往常一样,分析代码。与使用某些C ++功能相比,算法和内存管理通常会对性能产生更大的影响。

许多游戏还将Lua或其他一些脚本语言嵌入到游戏引擎中,因此显然每一行代码都不需要最高性能。

我从未编程或使用过Cell,因此可能会有进一步的限制等。

答案 8 :(得分:3)

游戏社区并不担心C ++。在开发销售数百万的开放式游戏引擎的过程中,我可以说业务人员非常熟练且知识渊博。

shared_ptr未被广泛使用的事实部分是因为它有实际成本,但更重要的是因为所有权不是很清楚。所有权和资源管理是最重要和最难实现的事情之一。部分原因是因为资源在控制台上仍然很少,但也因为大多数困难的错误往往与不明确的资源管理有关(例如,谁和什么控制对象的生命周期)。 IMHO shared_ptr至少没有帮助。

异常处理会增加成本,这使得它不值得。在最后的游戏中,无论如何都不应抛出异常 - 最好是崩溃而不是抛出异常。另外,无论如何,确保C ++中的异常安全真的很难。

但是C ++的许多其他部分在游戏业务中被广泛使用。在EA内部,EASTL是一款令人惊叹的STL翻版,非常适合高性能和稀缺资源。

答案 9 :(得分:3)

有一句古老的谚语说将军们已经做好充分准备去战胜上一场战争而不是下一场战争。

关于性能的大多数建议都是类似的。它通常与五年前的软件和硬件有关。

答案 10 :(得分:2)

这也取决于游戏的类型。如果它是一个处理器轻的游戏(如小行星克隆)或2d中的几乎任何东西,你可以逃脱更多。当然,智能指针比常规指针花费更多,但如果有人用C#编写游戏,那么智能指针肯定不会成为问题。在游戏中没有使用的例外可能是正确的,但许多人反正滥用例外。例外情况只应用于特殊情况......不是预期的错误。

答案 11 :(得分:2)

凯文·弗雷写了一篇有趣的文件,“How much does Exception Handling cost, really?”。

答案 12 :(得分:2)

在我加入游戏行业之前,我也听说过它,但我发现有些专业游戏硬件的编译器有时会......低于标准。 (我个人只与主要的游戏机一起工作,但我确信它对于手机之类的设备来说更是如此。)显然,如果你正在为PC开发,这不是一个真正的大问题。编译器经过验证,真实且丰富多样,但如果您想为Wii,PS3或X360开发游戏,请猜测您拥有多少选项以及它们与您选择的Windows / Unix编译器相比的测试结果。< / p>

这并不是说这些工具一定很糟糕,但是如果你的代码很简单,它们只能保证工作 - 实质上,如果用C语言编程,这并不意味着你不能使用类或创建使用RAII的智能指针,但是从您获得的“保证”功能越远,对标准的支持就越不稳定。我个人使用一些模板编写了一行代码,这些模板为一个平台而不是另一个平台编译 - 其中一个模块根本不支持C ++标准中的一些边缘情况。

其中一些无疑是游戏程序员的民间传说,但它很可能来自某个地方:一些旧的编译器在抛出异常时奇怪地解开堆栈,所以我们不使用异常;某个平台没有很好地使用模板,所以我们只在琐碎的情况下使用它们;不幸的是,问题案例和它们发生的地方似乎从来没有被写下来(而且案件往往是深奥的,并且在第一次发生时很难追查),因此没有简单的方法来验证它是否仍然是一个问题或除了试图并希望你不会因此受到伤害。毋庸置疑,这说起来容易做起来难,所以犹豫不决。

答案 13 :(得分:1)

异常处理从来都不是免费的,尽管在这里有一些相反的说法。无论是记忆还是速度,总是有成本。如果性能成本为零,则内存成本会很高。无论哪种方式,使用的方法完全依赖于编译器,因此,不受开发人员的控制。这两种方法都不适合游戏开发。目标平台具有有限的内存量,这通常是永远不够的,因此,我们需要完全控制,并且b。固定的性能约束为30 / 60Hz。 PC应用程序或工具可以在某些事情得到处理时暂时放慢速度,但这在控制台游戏中绝对无法容忍。有物理和图形系统等依赖于一致的帧速率,因此任何可能破坏它的C ++“特征” - 并且不能由开发人员控制 - 是被丢弃的良好候选者。如果C ++异常处理非常好,性能/内存成本很低或没有,它将在每个程序中使用,甚至没有选项来禁用它。事实上,编写可靠的PC应用程序代码可能是一种干净整洁的方式,但在游戏开发中需要过剩。它会耗尽可执行文件,耗费内存和/或性能,并且完全不可优化。这对于具有巨大指令缓存等的PC开发者来说很好,但游戏控制台没有这种奢侈品。即使他们这样做,游戏开发社区几乎肯定会花费额外的周期/内存在游戏相关资源上,而不是浪费在我们不需要的C ++功能上。

答案 14 :(得分:0)

其中一些是游戏民间传说,也许游戏开发人员将咒语从非常有限的设备(例如移动设备)转移到不属于游戏设备的游戏开发者身上传下来。

然而,关于游戏要记住的一点是,它们的性能特征主要是平滑且可预测的帧速率。他们不是关键任务软件,但他们是&#34; FPS-critical&#34;软件。帧速率的打嗝可能导致玩家在动作游戏中进行游戏,例如因此,尽管你可能会发现任务关键型软件中有一些关于不失败的健康程度的偏执,你也可以在游戏中找到类似的东西,而不是口吃和滞后。

我曾经谈过的许多游戏设备也不喜欢虚拟内存,而且我看到他们试图采用各种方法来最大程度地减少在不方便的时候发生页面错误的可能性。在其他领域,人们可能喜欢虚拟记忆,但游戏是&#34; FPS-critical&#34;。他们不希望在游戏过程中发生任何奇怪的打嗝或口吃。

因此,如果我们从异常开始,零成本EH的现代实现允许正常执行路径执行得比在错误条件下执行手动分支更快。但他们的代价是抛出异常突然变得更加昂贵,并且#34;停止世界&#34;一种事件。那种&#34;阻止世界&#34;对于寻求最可预测和平滑帧速率的软件来说,事情可能是灾难性的。当然,这只应该被保留用于真正特殊的路径,但是游戏可能更愿意找到不面对特殊路径的理由,因为在游戏过程中投掷的成本太高了。如果游戏强烈希望首先避免面对特殊的路径,那么优雅的恢复就是一种没有实际意义的概念。

游戏经常有这种&#34;启动和去#34;特性,他们可以潜在地执行所有文件加载和内存分配,以及在加载关卡或开始游戏时可能提前失败的事情,而不是在游戏过程中可能失败的事情。因此,他们不一定有那么多分散的代码路径可以或者应该遇到异常,这也会降低EH的好处,因为如果只有少数几个区域最大化,它就不会变得如此方便可能会从中受益。

由于类似的原因,EH,gamedevs经常不喜欢垃圾收集,因为它也可以有这样的&#34;停止世界&#34;可能导致不可预测的口吃的事件 - 在许多领域中可能容易被忽视的最简单的口吃是无害的,但不是游戏性的。因此,他们可能会完全避免它或寻求对象池,以防止GC收集在不合适的时间发生。

对我来说,避免使用智能指针似乎有点极端,但很多游戏可以提前预先分配内存,也可以使用实体组件系统,其中每个组件类型都存储在随机访问序列中,允许它们被索引。智能指针意味着堆分配和在单个对象的粒度级别拥有内存的东西(至少除非你使用自定义分配器和自定义删除函数对象),并且大多数游戏可能会发现它符合它们的最佳利益以避免这种粒度堆分配而是在大容器中或通过内存池一次分配许多东西。

这里可能有一些迷信,但我认为其中一些至少是合理的。