【问题标题】:Do global variables mean faster code?全局变量是否意味着更快的代码?
【发布时间】:2011-04-26 13:03:45
【问题描述】:

我最近在 1996 年写的 article on game programming 中读到,使用全局变量比传递参数更快。

这曾经是真的吗?如果是的话,今天仍然如此吗?

【问题讨论】:

  • 微优化的问题在于它们依赖于硬件。昨天的优化可能不会成为明天的优化。 15 年前的优化几乎可以保证在今天要么不太有效,要么就是无关紧要。更高级别的优化是永恒的,但低级别的细节会随着每一代硬件而改变。
  • 我认为优化是在传递额外参数与不传递参数之间进行的(因为它已经作为全局可用),这意味着您避免为每个函数调用复制一个指针(涉及屏幕);不是因为 global 天生就比什么都快。
  • 您应该对大多数游戏文章持保留态度,尤其是那些超过几年的游戏文章。游戏行业一直充满了糟糕的程序员,因为最早的游戏程序员是“C in C++”程序员,而现在他们是教新开发者的人。这很可悲。你最好先精通 C++,然后是一般的软件开发,然后游戏编程。这样你就知道什么时候该忽略所有的废话了。 (即使是 Game Programming Gems 8 中的至少一篇文章,这是本评论系列中的最新文章,也说了不正确的 C++ 内容。可悲!)
  • 不敢相信这才14-15岁。模式 13 小时?将屏幕宽度硬编码为 320?那是80年代中期的东西。 1996 年,3D 加速开始受到关注。

标签: c++ c optimization


【解决方案1】:

但是当你经常使用全局变量时,你有“意大利面条代码”。

【讨论】:

  • 我认为“意大利面条代码”与使用 goto 相关,而不是全局变量。
  • 使用全局数据的一段代码的耦合图确实看起来像意大利面条——即使程序流程不是。
【解决方案2】:

每个人都已经给出了关于这是特定于平台和程序的适当警告答案,需要实际测量时间等。所以,说了这么多,让我直接针对 x86 上游戏编程的具体案例回答你的问题和 PowerPC。

在 1996 年,在某些情况下,将参数压入堆栈需要额外的指令,并可能导致 Intel CPU 管道内的短暂停止。在这些情况下,完全避免参数传递和从文字地址读取数据可能会有非常小的加速。

这在 x86 或大多数游戏机中使用的 PowerPC 上不再适用。使用全局变量通常比传递参数,原因有两个:

  • 现在更好地实现了参数传递。现代 CPU 在寄存器中传递参数,因此从函数的参数列表中读取值比内存加载操作更快。 x86 使用寄存器映射和存储转发,因此看起来像将数据混洗到堆栈上并返回实际上可以是一个简单的寄存器移动。
  • Data cache latency far outweighs CPU clock speed in most performance considerations。堆栈被大量使用,几乎总是在缓存中。从任意全局地址加载可能会导致缓存未命中,这是一个巨大的损失,因为内存控制器必须从主 RAM 中获取数据。 (这里的“巨大”是 600 个循环或更多。)

【讨论】:

  • +1 用于将缓存局部性识别为使用全局变量时的主要罪魁祸首。
  • 在 PlayStation 2 上进行优化时,我几乎不需要做更多的事情,只需修复缓存未命中即可让我的功能运行得足够快。与缓存未命中的延迟相比,指令计数是备用变化。
【解决方案3】:

在某些情况下,它可能仍然是正确的。 全局变量可能与指向变量的指针一样快,其指针仅存储在寄存器中/通过寄存器传递。所以,这是一个关于寄存器数量的问题,你可以使用。

要加速优化函数调用,您可以执行其他几项操作,使用 global-variable-hacks 可能会更好:

  • 将函数中的局部变量数量减少到几个(显式)寄存器变量。
  • 尽量减少函数的参数计数,即使用指向结构的指针,而不是在相互调用的函数中使用相同的参数组合。
  • 使函数“裸”,这意味着它根本不使用堆栈。
  • 使用“proper-tail-calls”(不适用于 java/-bytecode 和 java-/ecma-script)
  • 如果没有更好的方法,请像 TABLES_NEXT_TO_CODE 那样破解自己,它将全局变量定位在函数代码旁边。在函数式语言中,这是一种后端优化,它也使用函数指针作为数据指针;但只要您不使用函数式语言进行编程,您只需要将这些变量定位在函数使用的变量旁边。再说一次,您只希望它从您的函数中删除堆栈处理。如果您的编译器生成处理堆栈的汇编代码,那么这样做没有意义,您可以使用指针来代替。

我发现了这个“gcc 属性概述”: http://www.ohse.de/uwe/articles/gcc-attributes.html

我可以给你这些标签用于谷歌搜索: - 正确的尾调用(主要与函数式语言的命令式后端相关) - TABLES_NEXT_TO_CODE(主要与 Haskell 和 LLVM 相关)

【讨论】:

    【解决方案4】:

    像其他一切一样:是和否。没有一个答案,因为它取决于上下文。

    对位:

    • 想象一下在拥有数百个寄存器的 Itanium 上进行编程。您可以将相当多的全局变量放入其中,这将比在 C 中实现全局变量的典型方式更快(一些静态地址(尽管如果它们是字长,它们可能只是将全局变量硬编码为指令))。即使全局变量一直在缓存中,寄存器可能仍然更快。

    • 在 Java 中,由于必须执行初始化锁,过度使用全局变量(静态变量)会降低性能。如果 10 个类想要访问某个静态类,他们都必须等待该类完成其静态字段的初始化,这可能需要任何时间,甚至是永远。

    在任何情况下,全局状态都是不好的做法,它会增加代码的复杂性。设计良好的代码自然足够快 99.9% 的时间。似乎较新的语言正在一起删除全局状态。 E 删除全局状态,因为它违反了他们的安全模型。 Haskell 一起删除状态。 Haskell 存在并且其实现优于大多数其他语言的事实足以证明我再也不会使用全局变量了。

    此外,在不久的将来,当我们都有数百个内核时,全局状态并不会真正有多大帮助。

    【讨论】:

      【解决方案5】:

      我看到了很多理论上的答案,但没有针对您的场景的实际建议。我的猜测是,您有大量参数要通过许多函数调用传递,并且您担心来自多个调用帧级别和每个级别的许多参数的累积开销。否则你的担心是完全没有根据的。

      如果这是您的场景,您可能应该将所有参数放在“上下文”结构中,并传递一个指向该结构的指针。这将确保数据的局部性,并使您不必在每次函数调用时传递多个参数(指针)。

      以这种方式访问​​的参数比真正的函数参数访问成本略高(您需要一个额外的寄存器来保存指向结构基址的指针,而不是使用函数参数用于此目的的帧指针),并且单独地(但可能不考虑缓存影响)比普通的非 PIC 代码中的全局变量访问成本更高。但是,如果您的代码位于使用position independent code 的共享库/DLL 中,由于 GOT 和 GOT 相对寻址,访问通过指针传递给 struct 的参数的成本比访问全局变量便宜,并且与访问静态变量相同。这是从不使用全局变量进行参数传递的另一个原因:如果您最终可能将代码放入共享库/DLL 中,那么任何可能的性能优势都会突然适得其反!

      【讨论】:

        【解决方案6】:

        简短的回答 - 不,优秀的程序员通过了解和使用适合工作的工具,然后在他们的代码不符合他们的要求时以有条不紊的方式进行优化,从而使代码运行得更快。

        更长的答案 - 在我看来,这篇文章写得不是特别好,无论如何都不是关于程序加速的一般建议,而是“15 种更快的 blits 方法”。将此推断到一般情况下,无论您如何看待文章的优点,都没有抓住作者的观点。

        如果我正在寻找性能建议,我将零信任放在一篇文章中测量代码可能是个好主意。如果你不打算展示如何让代码变得更好,为什么要包含它?

        有些建议已经过时了 - 很久以前,FAR 指针就不再是 PC 上的问题了。

        认真的游戏开发人员(或任何其他专业程序员,就此而言)会对这样的建议大笑:

        您可以取出断言的 完全,或者你可以添加一个 #define NDEBUG 编译最终版本时。

        我给你的建议是,如果你真的想评估这 15 个技巧中的任何一个的优点,并且由于这篇文章已有 14 年的历史,那就是在现代编译器(比如 Visual C++ 10)中编译代码并尝试确定使用全局变量(或任何其他技巧)可以使其更快的任何区域。

        [开个玩笑——我真正的建议是完全忽略这篇文章,并在您遇到无法解决的工作问题时就 Stack Overflow 提出具体性能问题。这样,您得到的答案将经过同行评审,由示例代码或良好的外部证据支持,并且是最新的。]

        【讨论】:

        • +1 表示不厌其烦地阅读和分析引用的文章,并以迄今为止最具体、最有条理的方式解决问题。
        • 您有一个奇怪的假设,即现在的每台计算机和编译器都是“现代的”。由于堆栈大小和代码空间非常小,通常需要在嵌入式系统上使用全局变量。在速度方面,它还有助于尝试实时处理数据(这是大多数嵌入式的目标)。
        • @Nick T,好吧,也许我应该在我的问题中提到这一点,我正在为 Android 智能手机开发。
        • @Nick T - 关于该主题有一个有趣且非常不同的讨论。此处的文章是 Windows 特定的,c.1996,我的回复旨在适用于这种情况。
        【解决方案7】:

        当您从参数切换到全局变量时,可能会发生以下三种情况之一:

        • 运行速度更快
        • 运行方式相同
        • 运行速度较慢

        您将不得不衡量性能,以了解在不平凡的具体案例中什么是更快的。 1996 年如此,今天如此,明天亦如此。

        暂时不考虑性能,大型项目中的全局变量会引入依赖项,这几乎总是会使维护和测试变得更加困难。

        今天,当出于性能原因尝试寻找全局变量的合法用途时,我非常同意examples in Preet's answer:微控制器程序或设备驱动程序中经常需要的变量。极端情况是专用于全局变量的处理器寄存器。

        在推理全局变量与参数传递的性能时,编译器实现它们的方式是相关的。全局变量通常存储在固定位置。有时编译器会生成直接寻址来访问全局变量。然而,有时编译器会使用更多的间接方式,并为全局变量使用一种符号表。 AIX 的 IIRC gcc 是 15 年前做的。在这种环境下,小类型的全局变量总是比局部变量和参数传递慢。

        另一方面,编译器可以通过将参数压入堆栈、在寄存器中传递参数或两者的混合来传递参数。

        【讨论】:

        • 我要补充一点,这个开关对维护很危险。
        • +1 任何“X 会更快”的问题只有一个有用的答案:measure
        • 哇。这是怎么得到赞成的?它完全没用。 “巴黎下雨了吗?” ——“嗯,要么在下雨,要么不下雨,要么下点小雨。如果您必须知道,请前往巴黎。暂时离开巴黎,我更喜欢阳光明媚的天气。”
        • @Konrad 显然,我认为它非常有用。它有助于在调整性能时建立必要的心态。你能提出一些建设性的批评吗?
        • (续)如果不是缓存局部性,参数传递确实总是不如全局,性能方面。这是一个重要的观察结果,不需要任何基准。考虑到缓存局部性使这变得更加有趣,现在我们可能已经达到了一个有趣的基准点。但是如何编写这样的基准?需要注意哪些变量?我对几乎所有简单地喊“衡量”的与性能相关的答案都很不满意。他们只是躲避子弹。提供一个基准。那么你就有了答案。
        【解决方案8】:

        也许是一个微优化,并且可能会被您的编译器在不诉诸此类做法的情况下生成的优化所消灭。事实上,使用全局变量甚至可能会抑制一些编译器优化。可靠且可维护的代码通常具有更大的价值,而全局变量则不利于这一点。

        使用全局变量替换函数参数会导致所有此类函数不可重入,如果使用多线程,这可能会出现问题 - 在 1996 年的游戏开发中不是常见做法,但随着多核处理器的出现更为常见.它还排除了递归,尽管这可能不是一个问题,因为递归有其自身的问题。

        在任何重要的代码体中,算法和数据结构的更高级别优化都可能有更多的里程数。此外,除了避免参数传递的全局变量之外,您还可以选择其他选项,尤其是 C++ 类成员变量。

        如果在您的代码中习惯性地使用全局变量对其性能产生可衡量或有用的差异,我会首先质疑设计。

        有关全局变量固有问题的讨论以及避免这些问题的一些方法,请参阅 Jack Gannsle 的A Pox on Globals。该文章涉及嵌入式系统开发,但普遍适用;只是一些嵌入式系统开发人员认为他们有充分的理由使用全局变量,这可能是出于在游戏开发中用来证明其合理性的所有相同的误导性原因。

        【讨论】:

          【解决方案9】:

          好吧,如果您正在考虑使用全局参数而不是参数传递,那可能意味着您有很长的方法/函数链,您必须向下传递该参数。在这种情况下,你真的会通过从参数切换到全局变量来节省 CPU 周期。

          所以,那些说这要看情况的人,我想他们是完全错误的。即使使用 REGISTER 参数传递,仍然会有更多的 cpu 周期和更多的开销来将参数下推到被调用者。

          但是 - 我从不这样做。 CPU 现在更胜一筹,有时 12Mhz 8086 可能是问题所在。如今,如果您不编写嵌入式或超级涡轮增压性能代码,请坚持使用看起来不错的代码,不会破坏代码逻辑,并且会蓬勃发展成为模块化。

          最后,将机器语言代码的生成留给编译器 - 设计它的人最擅长了解他们的宝宝的表现,并使您的代码以最佳状态运行。

          【讨论】:

            【解决方案10】:

            当我们有

            在 3mhz 处理器上编程的人说。是的,你没看错,64k 还不够。

            【讨论】:

            • 64k 不会... 640k 可能会。
            • @John Nicholas:是的。我听说过一些 640k 还不够的情况,但是如果你不包括附加的可读介质(如磁带或 floopy)。在那种情况下,一切都很好
            【解决方案11】:

            撇开可维护性和正确性问题不谈,基本上有两个因素会控制全局变量与参数的性能。

            当你创建一个全局时,你会避免复制。这稍微快一点。当您按值传递参数时,必须复制它,以便函数可以在它的本地副本上工作,而不会损坏调用者的数据副本。至少在理论上。如果某些现代优化器确定您的代码表现良好,它们会做一些非常棘手的事情。一个函数可能会自动内联,编译器可能会注意到该函数对参数没有做任何事情,只是优化了任何复制。

            当你创建一个全局变量时,你就是在对缓存撒谎。当您将所有变量整齐地包含在您的函数中时,以及一些参数,数据往往都在一个地方。有些变量将在寄存器中,有些可能会立即在缓存中,因为它们彼此“相邻”。使用大量全局变量基本上是缓存的病态行为。不能保证相同的函数会使用各种全局变量。位置与使用没有明显的相关性。也许你有一个足够小的工作集,任何东西在哪里都没有区别,而且它都在缓存中结束。

            所有这些只是我上面的海报所提出的观点:

            当你从参数切换到 全局变量,三件事之一 可能发生:

            * it runs faster
            * it runs the same
            * it runs slower
            

            您必须衡量绩效 看看什么是不平凡的更快 具体案例。 1996 年确实如此, 今天是真的,明天也是真的。

            根据您的确切编译器的具体行为,以及您用于运行代码的硬件的具体细节,在某些情况下,全局变量可能会带来非常轻微的性能提升。这种可能性可能值得在一些运行速度太慢的代码上进行尝试。这可能不值得全身心投入,因为明天你的实验答案可能会改变。因此,正确的答案几乎总是采用“正确”的设计模式并避免丑陋的设计。寻找更好的算法、更高效的数据结构等,然后再故意尝试使您的项目变得意大利面。从长远来看,回报要好得多。

            而且,除了开发时间与用户时间的争论之外,我将添加开发时间与摩尔的时间争论。如果您假设摩尔定律将使计算机的速度每年提高一半,那么为了一个简单的整数,我们可以假设进步发生在每周稳定 1% 的进步中。如果您正在研究一种可能会提高 1% 之类的微优化,并且它会为项目增加一周的时间来避免使事情变得复杂,那么只需休息一周就会对您的用户的平均运行时间产生相同的影响。

            【讨论】:

              【解决方案12】:

              通常(但它可能很大程度上取决于编译器和平台实现),传递参数意味着将它们写入堆栈,而全局变量则不需要。

              也就是说,全局变量可能意味着在 MMU 或内存控制器中包含页面刷新,而堆栈可能位于处理器可用的更快内存中...

              抱歉,这样的一般问题没有好的答案,只需测量它(并尝试不同的场景)

              【讨论】:

              • +1,即使有一些调用约定/架构参数甚至不必写入堆栈。
              【解决方案13】:

              在某种程度上任何避免处理器指令的代码(即更短的代码)都会更快。然而快多少? 不是很!还要注意编译器优化策略可能会导致更小的代码。

              如今,这只是对非常特殊的应用程序的优化,通常在超时间关键驱动程序或微控制代码中。

              【讨论】:

              • 没错。如果全局变量在大多数情况下更快,优化编译器会根据需要将参数转换为全局变量。以自动化的方式似乎很容易做到。
              • 我厌倦了说“为了优化得到一些实数”。
              • 不正确。更长的代码也可以更快。尤其是在这种情况下,全局变量通常会导致缓存未命中,而使用非全局数据可以避免这种情况,全局变量很有可能会更慢。跨度>
              • @dbkk:无法将参数转换为全局变量。不可重入函数不等同于可重入函数,编译器在编译一个翻译单元时无法判断代码是否需要从另一个翻译单元重入调用。
              【解决方案14】:

              “更快”是什么意思?

              我知道一个事实,理解一个带有全局变量的程序比不理解一个程序要花费我更多的时间。

              如果程序员花费的额外时间少于用户在使用全局变量运行程序时获得的时间,那么我会说使用全局变量更快。

              但考虑到该计划将由 10 个人每天运行一次,持续 2 年。没有全局变量需要 2.84632 秒,使用全局变量需要 2.84217 秒(增加了 0.00415 秒)。 TOTAL 运行时间减少了 727 秒。就程序员时间而言,获得 10 分钟的运行时间并不值得引入一个全局变量。

              【讨论】:

              • +1 此类问题的唯一正确答案。其余的人认为差异很小或可能不存在,你证明了为什么它不值得;)
              • +1 特别是如果全局变量意味着引入 3 小时来解决一个问题,否则该问题可以在 10 分钟内解决。
              • 大部分是真的。例如,如果您正在编写一个游戏,而您只有 0.02 秒的时间来处理输入、计时游戏时钟、进行碰撞检测、计算场景内容、构建场景图并将其渲染到屏幕上,您'将采取任何一点加速你能得到!正如其他人所说,衡量,不要像这样不必要地进行微观优化。
              猜你喜欢
              • 1970-01-01
              • 2012-08-14
              • 2012-08-30
              • 2022-10-04
              • 2017-11-06
              • 1970-01-01
              • 2016-05-13
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多