代码之家  ›  专栏  ›  技术社区  ›  amit kumar

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

  •  30
  • amit kumar  · 技术社区  · 15 年前

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

    • 不要使用智能指针。游戏中没有人这样做。
    • 在游戏编程中不应该(通常也不应该)使用异常来提高内存和速度。

    这些说法中有多少是正确的?C++的特点是为了保持效率而设计的。这种效率不足以进行游戏编程吗?对于97%的游戏编程?

    C思维方式似乎仍然对游戏开发社区有很好的把握。这是真的吗?

    我看了另一个视频,是关于2009年GDC中多核编程的一次谈话。他的演讲几乎完全针对单元编程,在处理之前需要进行DMA传输(简单的指针访问无法与单元的SPE一起工作)。他不鼓励使用多态性作为“重新建立”DMA传输的指针。多伤心啊。就像回到广场一样。我不知道是否有一个优雅的解决方案,在细胞上的C++多态性。DMA传输的主题很深奥,我在这里没有太多的背景。

    我同意C++也不太喜欢程序员,他们希望一个小的语言来破解,而不是阅读书堆。模板也吓坏了调试。你是否同意C++对游戏社区的恐惧太大了?

    15 回复  |  直到 6 年前
        1
  •  57
  •   Charlie Martin    15 年前

    听着,你听到的大部分都是 任何人 说编程的效率是神奇的思维和迷信。智能指针确实有性能成本;特别是如果您在一个内部循环中执行了许多花哨的指针操作,那么它可能会有所不同。

    也许吧。

    但是当人们 像这样的事情,通常是很久以前有人告诉他们x是真的,除了直觉之外,它什么都没有。现在,细胞/多态性问题 声音 我敢打赌,这是对第一个说这句话的人说的。但我还没有证实。

    你会听到关于操作系统C++的同样的事情:它太慢,它做的事情你想做得很好,很差。

    尽管如此,我们还是完全用C++构建了OS/400(从V3R6转发),在裸露的金属上,得到了一个快速、高效、小型的代码库。这需要一些工作;特别是在裸露的金属上工作,有一些引导问题,使用新的放置,诸如此类的事情。

    C++可能是一个问题,因为它太大了:我现在重读Stroustrup的WestRe断路器,它非常吓人。但我认为没有任何内在的东西表明你不能在游戏编程中有效地使用C++。

        2
  •  63
  •   Tim Cooper    12 年前

    我上次玩的游戏是PS3上的天剑,它是用C++编写的,甚至是单元格代码。在此之前,我做了一些PS2游戏和电脑游戏,他们也是C++。非项目使用了智能指针。不是因为效率问题,而是因为它们通常不需要。游戏,特别是控制台游戏,在正常游戏中不使用标准内存管理器进行动态内存分配。如果有动态物体(导弹、敌人等),通常会预先分配,并根据需要重新使用。每种类型的对象对游戏可以处理的实例数量都有一个上限。这些上限将由所需的处理量(太多,游戏速度会变慢)或内存量(太多,您可能会开始频繁分页到磁盘,这会严重降低性能)来定义。

    游戏通常不使用异常,因为,嗯,游戏不应该有错误,因此不能产生异常。这一点尤其适用于由游戏机制造商测试游戏的游戏机游戏,尽管最近的平台(如360和PS3)确实有一些游戏可能崩溃。老实说,我没有在网上看到任何关于启用异常的实际成本的信息。如果只有在抛出异常时才会产生成本,那么没有理由不在游戏中使用它们,但我不确定,这可能取决于所使用的编译器。一般来说,游戏程序员知道什么时候会出现问题,这些问题可以通过在业务应用程序中使用异常(如IO和初始化)来处理,并且不需要使用异常(这是可能的!).

    但是,在全球范围内,C++作为游戏开发语言正在慢慢地减少。Flash和Java可能有更大的市场份额,它们确实有例外和智能指针(以托管对象的形式)。

    对于信元指针访问,当代码被dma以任意基地址插入信元时,就会出现问题。在这种情况下,代码中的任何指针都需要用新的基地址“修复”,这包括v-tables,并且您不希望为加载到单元中的每个对象都这样做。如果代码总是加载在固定的地址,那么就不需要修复指针。但是,由于限制了代码的存储位置,您会失去一些灵活性。在PC上,代码在执行过程中不会移动,因此不需要在运行时修复指针。

    我真的不认为任何人不信任C++特性——而不是信任编译器是另一回事,而且通常是新的,像Cy+这样的深奥体系结构往往在C++之前得到健壮的C编译器,因为C编译器比C++编译器更容易实现。

        3
  •  12
  •   Dan    15 年前

    如果你或你的朋友对性能真的很偏执,那就去阅读英特尔的优化手册。乐趣。

    否则,每次都要追求正确性、可靠性和可维护性。我宁愿玩一个慢一点的游戏,也不想玩撞车的游戏。如果/当您发现您有性能问题时,进行分析,然后进行优化。您可能会发现有一些热点代码,可以通过使用更有效的数据结构或算法来提高效率。当分析显示它们是您获得有价值的加速的唯一方法时,您只需为这些愚蠢的小mico优化而烦恼。

    所以:

    1. 编写清晰正确的代码
    2. 简况
    3. 轮廓
    4. 您能使用更有效的数据结构或算法来加速瓶颈吗?
    5. 将微观优化作为最后的手段,并且仅在分析显示它有帮助的情况下使用。

    PS:许多现代C++编译器提供了一个异常处理机制,它增加了零执行开销,除非抛出异常。也就是说,只有在实际抛出异常时,性能才会降低。只要例外仅用于 特殊情况 那么就没有理由不使用它们了。

        4
  •  7
  •   Community Navdeep Singh    7 年前

    我在StackOverflow上看到了一个帖子(我似乎再也找不到了,所以可能它没有贴在这里),它查看了异常和错误代码的相对成本。人们经常会看到“有例外的代码”和“没有错误处理的代码”,这是不公平的比较。如果您要使用异常,那么通过不使用它们,您必须为相同的功能使用其他的东西,而其他的东西通常是错误返回代码。他们发现,即使在一个简单的例子中,只有一个级别的函数调用(因此不需要将异常传播到调用堆栈的更高层),在错误情况发生的时间为0.1%-0.01%或更少的情况下,异常也比错误代码快,而在相反的情况下,错误代码则更快。

    与上面关于度量异常与不处理错误的抱怨类似,人们在虚拟函数的推理中更经常会出现这种错误。就像不使用异常作为从函数返回动态类型的方法一样(是的,我知道, 全部的 您的代码是例外的),您不会将函数设置为虚拟的,因为您喜欢它在语法中的突出显示方式。将函数设置为虚拟化是因为需要特定类型的行为,因此不能说虚拟化速度很慢,除非将其与具有相同操作的对象进行比较,通常替换为许多switch语句或大量代码重复。它们也有性能和内存命中。

    关于游戏没有bug和其他软件的评论,我只能说我显然没有玩过他们软件公司制作的任何游戏。我在Pokemon的精英4的地板上冲浪,在遗忘中被困在一座山里面,被格洛斯杀死,格洛斯不小心把他们的法力伤害和他们的生命伤害结合在一起,而不是在暗黑破坏神II中单独做它们,然后推着自己穿过一个有大岩石的封闭大门,与一只鸟和一个弹弓在暮色普里与地精战斗。NCESS。软件有缺陷。使用异常并不能使软件无缺陷。

    标准库的异常机制有两种类型的异常: std::runtime_error std::logic_error . 我可以看出我不想用 标准::逻辑错误 (我用它作为一个临时的东西来帮助我进行测试,目的是最终将其移除,我还把它作为一个永久性的检查放在里面)。 std::运行时错误 然而,这不是一个bug。我抛出了一个派生自 std::运行时错误 如果我连接到的服务器向我发送无效数据(安全编程规则1:不信任任何人,甚至是您认为您编写的服务器),例如声称他们向我发送了12字节的消息,然后他们实际向我发送了15个。在这种情况下,只有两种可能性:

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

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

    在这两种情况下,我的响应都是相同的:断开连接(无论我在代码中处于何处,因为我的析构函数都会为我清理东西),等待几秒钟,然后再次尝试连接到服务器。我不能做其他的事。我可以给所有东西一个错误代码(这意味着通过引用传递其他东西,这是一个性能冲击,严重混乱的代码),或者我可以抛出一个异常,我在我的代码中捕捉到一个点,在那里我决定要连接到哪个服务器(这在我的代码中可能非常高)。

    我在代码中提到的任何一个bug?我不这么认为;我认为它接受了我必须与之交互的所有其他代码都是不完美的或恶意的,并且确保我的代码在面对这种含糊不清时仍能保持性能。

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

    在我的个人编码中,我还没有发现很多情况下代码的“C”版本比“C++”版本快。事实上,这通常是相反的。例如,考虑 std::sort VS qsort (bjarne stroustrup使用的一个常见示例)其中 STD::排序 破坏者 快速排序 my recent comparison of std::copy vs. memcpy 在哪里 STD:复制 实际上有一点性能优势。

    太多的“C++特征X太慢”的声明似乎是基于比较它没有功能。在速度和内存方面,性能最好的代码是 int main() {} 但是我们写程序来做事情。如果您需要特定的功能,那么不使用提供该功能的语言的功能将是愚蠢的。然而,你应该从你想让你的程序做什么开始,然后找到最好的方法去做。显然,你不想从“我想写一个使用C++的特征X”的程序开始,你想从“我想写一个程序,使事情冷却”,也许你最终在“…和实现这一点的最佳方式是X”。

        5
  •  6
  •   Adam Jaskiewicz    15 年前

    很多人对事情绝对的陈述,因为他们实际上 认为 . 他们宁愿应用一条规则,使事情变得更乏味,但需要更少的设计和预先考虑。当我做一些毛茸茸的事情时,我宁愿偶尔有点苦思冥想,把无聊的事情抽象掉,但我想不是每个人都这么想的。当然,智能指针有性能开销。例外也是如此。那只是意味着那里 可以 是一些 小的 代码中不应该使用它们的部分。但你应该先分析一下,并确保这就是问题所在。

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

        6
  •  5
  •   Community Navdeep Singh    7 年前

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

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

    这使我们 Skizz's point 关于vtables和指针修正。如果您盲目地在SPE之间复制vtable指针,如果不修复指针,则会导致巨大的性能损失;如果您 修复指针并将虚拟函数代码下载到SPE。

        7
  •  5
  •   Dan Olson    15 年前

    我偶然看到索尼的一个优秀的演讲,叫做“面向对象编程的陷阱”。这一代的控制台硬件真的让很多人对C++的OO方面进行了第二次观察,并开始询问它是否真的是最好的前进方向。

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

        8
  •  4
  •   Steven    15 年前

    过去我用C++编写过小游戏,现在用C++来做其他高性能的应用。不需要在整个代码库中使用每一个C++特性。

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

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

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

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

    我从未编程或使用过一个单元,这样可能会有进一步的限制等。

        9
  •  3
  •   Johan Kotlinski    15 年前

    游戏社区不惧怕C++。我曾经在一个销售数百万美元的开放世界游戏引擎上工作过,我可以说这家公司的员工非常熟练,也非常有知识。

    共享资源没有被广泛使用这一事实部分是因为它有真正的成本,但更重要的是,所有权并不十分清楚。所有权和资源管理是最重要和最难纠正的事情之一。部分原因是控制台上的资源仍然稀缺,但也因为最困难的错误往往与不清楚的资源管理(例如,谁和什么控制对象的生命周期)有关。imho共享资源对这一点毫无帮助。

    异常处理有一个额外的成本,这使得它不值得。在最后一场比赛中,无论如何都不应该抛出异常——与其抛出异常,不如崩溃。另外,在C++中确保异常安全是非常困难的。

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

        10
  •  3
  •   James Anderson    15 年前

    有句老话说将军们已经做好了打最后一场战争而不是下一场战争的充分准备。

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

        11
  •  2
  •   ryeguy    15 年前

    这也取决于游戏的类型。如果它是一个处理器轻游戏(像小行星克隆)或几乎任何二维的东西,你可以摆脱更多。当然,智能指针比普通指针要贵,但如果有些人用C语言写游戏,那么智能指针绝对不会成为问题。游戏中未使用的例外可能是真的,但许多人无论如何都会滥用例外。例外应仅用于例外情况……非预期错误。

        12
  •  2
  •   Luc Hermitte    15 年前

    Kevin Frei写了一份有趣的文件,__ How much does Exception Handling cost, really? 艾斯。

        13
  •  2
  •   Tyler Millican    15 年前

    我在加入游戏行业之前也听说过,但我发现,专门游戏硬件的编译器有时…子部分。(我个人只在主要的游戏机上工作过,但我确信对于手机等设备来说更是如此。)显然,如果你正在为PC开发,这不是一个很大的问题,在PC上,编译器是试用过的,是真的,而且种类繁多,但是如果你想为Wii、PS3或X360开发一款游戏,猜猜有多少种选择?与您选择的Windows/Unix编译器相比,您已经测试了它们,测试得有多好。

    当然,这并不是说这些工具必然是糟糕的,但只有在代码简单的情况下才保证它们可以工作——本质上,如果您使用C语言编程的话。这并不意味着您不能使用类或使用智能指针创建RAII,但与您获得的“有保证”功能相比,对标准B的支持就更不可靠了。生态系统。我亲自编写了一段代码,使用的是一些为一个平台而不是另一个平台编译的模板——其中一个简单地不支持C++标准中的一些边缘情况。

    其中一些无疑是游戏程序员的民间传说,但很有可能是来自某个地方:一些旧的编译器在抛出异常时会奇怪地解开堆栈,因此我们不使用异常;某个平台不能很好地处理模板,因此我们只在小的情况下使用它们;等等。不幸的是,问题案例和它们发生的地方从未发生过。急诊似乎被写在任何地方(而且这些病例经常是深奥的,当它们第一次发生时很难找到),所以没有一个简单的方法来验证它是否仍然是一个问题,除非尝试并希望你不会因此受到伤害。不用说,说起来容易做起来难,所以犹豫还在继续。

        14
  •  1
  •   Alan Chambers    13 年前

    异常处理从来都是免费的,尽管这里有一些相反的声明。无论是内存还是速度,总有成本。如果它的性能成本为零,那么内存成本将很高。不管怎样,所使用的方法完全依赖于编译器,因此超出了开发人员的控制范围。这两种方法都不适合游戏开发,因为a.目标平台的内存有限,通常永远都不够,因此,我们需要完全控制,b.30/60Hz的固定性能约束。对于PC应用程序或工具来说,在处理某些东西的同时,暂时放慢速度是可以的,但在控制台游戏中,这绝对是不可预测的。物理和图形系统等都依赖于一致的帧率,所以任何可能会破坏这一点并且不能被开发者控制的C++“特征”都是被丢弃的好的候选对象。如果C++异常处理如此好,几乎没有或没有性能/内存成本,它将在每个程序中使用,甚至不存在禁用它的选项。事实上,编写可靠的PC应用程序代码可能是一种整洁的方式,但这在游戏开发中是多余的。它扩展了可执行文件,占用了内存和/或性能,完全不可优化。对于拥有巨大指令缓存等的PC开发人员来说,这是很好的,但是游戏机并没有这种奢华。即使他们这样做了,游戏开发团队也几乎肯定会把额外的循环/内存花费在与游戏相关的资源上,而不是浪费在C++上。 需要 .

        15
  •  0
  •   Dragon Energy    6 年前

    其中一些是游戏的民间传说,也许是游戏开发者以非常有限的设备(如移动设备)为目标传递给游戏开发者的咒语。

    然而,要记住的一点是,游戏的性能特征主要由平滑和可预测的帧速率决定。它们不是任务关键型软件,但它们是“FPS关键型”软件。在一个动作游戏中,一个帧速率的小问题可能会导致玩家游戏结束,例如,结果,你可能会在任务关键型软件中发现一些关于不失败的健康程度的偏执,你也可以在游戏中找到一些类似的东西,关于不结巴和滞后。

    我和很多游戏开发人员交谈过,他们甚至不喜欢虚拟内存,我也看到他们尝试应用各种方法来最小化页面错误在不方便的时候发生的可能性。在其他领域,人们可能喜欢虚拟内存,但游戏是“关键的fps”。他们不想在游戏中的某个地方发生任何奇怪的打嗝或口吃。

    因此,如果我们从异常开始,零成本eh的现代实现允许正常的执行路径比在错误条件下执行手动分支更快地执行。但他们付出的代价是,突然抛出一个例外会变得更加昂贵,“停止世界”之类的事件。对于寻求最可预测和最平稳的帧速率的软件来说,这种“停止世界”的做法可能是灾难性的。当然,这只被认为是为真正的特殊路径保留,但一个游戏可能更喜欢寻找原因不面对特殊路径,因为投掷的成本在游戏中间会太高。如果游戏一开始就强烈希望避免面对异常的道路,那么优雅的恢复是一种无意义的概念。

    游戏通常具有这种“启动并运行”的特性,在这种特性下,他们可以潜在地执行所有文件加载和内存分配,以及在加载级别或启动游戏时可能提前失败的操作,而不是在游戏中途失败的操作。因此,它们不一定具有许多分散的代码路径,这些分散的代码路径可能会或应该遇到异常,并且也会降低eh的好处,因为如果只选择几个可能从中受益的最大区域,那么它就不那么方便了。

    由于与eh类似的原因,gamedevs通常不喜欢垃圾收集,因为它也可以有那种“停止世界”的事件,这会导致不可预测的口吃——最简单的口吃,在许多领域很容易被认为是无害的,但对gamedevs来说不是。因此,他们可以直接避免它,或者寻找对象池,只是为了防止GC收集在不适当的时间发生。

    对于我来说,完全避免智能指针似乎有点极端,但是很多游戏可以提前预先分配内存,或者他们可能使用实体组件系统,其中每个组件类型都存储在一个随机访问序列中,这样可以对它们进行索引。智能指针意味着堆分配和在单个对象的粒度级别拥有内存的东西(至少除非使用自定义分配器和自定义删除函数对象),大多数游戏可能会发现避免这种粒度的堆分配是他们最感兴趣的,而是在一个大容器或thrug中同时分配许多东西。h内存池。

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