【问题标题】:C++ style vs. performance?C++ 风格与性能?
【发布时间】:2011-05-09 11:34:44
【问题描述】:

C++ 风格与性能 - 使用 C 风格的东西,这比一些 C++ 等价物更快,这是不好的做法吗?例如:

  • 不要使用atoi()itoa()atol()等!使用std::stringstream

  • 永远不要使用原始指针,而是使用智能指针 - 好的,它们真的很有用,每个人都知道,我知道,我一直在使用,我知道它们比原始指针好多少,但有时使用原始指针是完全安全的。为什么不呢? “不是 C++ 风格?

  • 不要使用按位运算 - 太 C 风格?什么?为什么不呢,当你确定你在做什么的时候?例如 - 不要按位交换变量 (a ^= b; b ^= a; a ^= b;) - 使用标准的 3 步交换。不要使用左移乘以二。等等等等。(好吧,这不是 C++ 风格与 C 风格的对比,但仍然是“不好的做法”)

  • 最后,最昂贵的——“不要使用 enum-s 来返回代码,它太 C 风格,对不同的错误使用异常”?为什么?好的,当我们谈论深层次的错误处理时——好的,但为什么总是这样呢?这有什么问题,例如 - 当我们谈论一个函数时,它返回不同的错误代码,而当错误处理只在调用第一个函数的函数中实现时?我的意思是 - 不需要在上层传递错误代码。异常是相当缓慢的,它们是exceptions用于特殊情况,而不是为了......美丽。

  • 等等等等等等

好的,我知道良好的编码风格非常非常重要 Compiler optimizations 非常强大。但我也知道exceptions handling 有多贵,(一些)smart_pointers 是如何实现的,并且一直不需要 smart_ptr。我知道,例如,atoi 并不像@ 那样“安全” 987654329@ 是,但仍然......性能如何?


编辑:我不是在谈论一些非常困难的事情,它们只是特定于 C 风格的。我的意思是——不要怀疑使用函数指针或虚拟方法和这类东西,C++ 程序员可能不知道,如果从未使用过这些东西(而 C 程序员一直这样做)。我说的是一些更常见、更简单的东西,比如在例子中。

【问题讨论】:

  • 除非您在硬实时系统上工作,否则程序员的时间比 CPU 时间更宝贵。
  • @Ignacio Vazquez-Abrams :目前,我正在实时系统上工作,每秒处理大量消息/流量,但我仍然一直被告知 - “不要这样做,它是 c 风格的”.. 另外,你对程序员的时间完全正确,我同意你的观点,但是使用 atoi(例如)并不比使用 std::stringstream 花费更多的时间。
  • 即使在最关键的应用程序中,像这样的微优化也很少产生可衡量的差异,但几乎总是使用正确的算法和数据结构才能产生差异。如今,随着多核系统正确有效地使用锁定以及编写缓存友好代码
  • 只是一个适合您的案例研究。我也在实时系统上工作。我的同事经常嘲笑我大量使用“太空时代”C++ 构造,如模板、异常、字符串流等。但是当我的代码与以 C 风格编写的类似代码相比时,我的代码注重效率,我的代码通常更快。即使没有微优化。而且我的代码几乎从未从 Q/A 中返回给我,因为它有效——无论是第一次,还是后来发生变化时。高度“优化”的 C 代码不能这么说。它很脆,不喜欢变化。

标签: c++ c performance coding-style


【解决方案1】:

您的所有问题都是先验的。我的意思是你是在抽象地问他们,而不是在任何你关心性能的特定程序的上下文中。 这就像试图在没有水的情况下游泳一样。

如果您对特定的具体程序进行调优,您会发现性能问题,而且很可能与这些抽象问题几乎没有任何关系。它们很可能都是您事先无法想到的。​​

对于这方面的具体示例,look here

如果我可以根据经验进行概括,那么性能问题的一个主要来源是泛滥的概括。 也就是说,虽然数据结构抽象通常被认为是一件好事,但任何好的东西都可能被大量过度使用,然后它就变成了严重的坏事。这并不罕见。根据我的经验,这是典型的。

【讨论】:

    【解决方案2】:
    1. 具有可变 char* 参数的函数在 C++ 中很糟糕,因为手动处理它们的内存太难了,因为我们有替代方案。它们不是通用的,我们不能像basic_string 允许的那样轻易地从char 切换到wchar_t。此外,lexical_cast 更直接地替代了atoiitoa
    2. 如果您真的不需要智能指针的智能 - 不要使用它。
    3. 交换使用swap。仅对按位运算使用按位运算 - 检查/设置/反转标志等。
    4. 异常很快。它们允许删除错误检查条件分支,因此如果它们真的“从未发生” - 它们会提高性能。

    【讨论】:

      【解决方案3】:

      一般来说,您缺少的是 C 方式通常并不快。它只是看起来更像是一种 hack,而且人们通常认为 hack 更快。

      永远不要使用原始指针,而是使用智能指针 - 好吧,它们真的很有用,每个人都知道,我知道,我一直在使用,我知道它们比原始指针好多少,但有时使用原始指针是完全安全的。为什么不呢?

      让我们把问题转过来。有时使用原始指针是安全的。这仅仅是使用它们的理由吗?有什么关于原始指针实际上优于智能指针?这取决于。 一些智能指针类型比原始指针慢。其他人不是。在std::unique_ptrboost::scoped_ptr 上使用原始指针的性能原理是什么?它们都没有任何开销,它们只是提供更安全的语义。

      这并不是说你永远不应该使用原始指针。只是你不应该仅仅因为你认为你需要性能,或者仅仅因为“它看起来很安全”而这样做。当您需要表示智能指针无法表示的内容时,请执行此操作。根据经验,使用指针指向事物,使用智能指针获取事物的所有权。但这是一个经验法则,而不是一个普遍的规则。使用适合手头任务的任何一种。但是不要盲目地假设原始指针会更快。当您使用智能指针时,请确保您熟悉它们。太多人只是使用shared_ptr 来处理所有事情,这太糟糕了,无论是在性能方面,还是在您最终应用于所有事情的非常模糊的共享所有权语义方面。

      不要使用按位运算 - 太 C 风格?什么?为什么不呢,当你确定你在做什么的时候?例如 - 不要按位交换变量( a ^= b; b ^= a; a ^= b; ) - 使用标准的三步交换。不要使用左移乘以二。等等等等。(好吧,这不是 C++ 风格与 C 风格的对比,但仍然是“不好的做法”)

      那个是对的。原因是“它更快”。按位交换在很多方面都存在问题:

      • 在现代 CPU 上速度较慢
      • 它更微妙,更容易出错
      • 它适用于非常有限的一组类型

      当乘以二时,乘以二。编译器知道这个技巧,并且会应用它如果它更快。再一次,换档也有许多相同的问题。在这种情况下,它可能会更快(这就是编译器会为你做这件事的原因),但它仍然更容易出错,并且它适用于有限的类型集。特别是,它可能与您认为可以安全地使用此技巧的类型编译良好...然后在实践中爆炸。特别是,负值移位是一个雷区。让编译器为您导航。

      顺便说一句,这与“C 风格”无关。完全相同的建议也适用于 C。在 C 中,常规交换仍然比按位 hack 快,并且编译器会仍然执行位移而不是乘法它是有效的,如果它更快。

      但作为一名程序员,您应该只将按位运算用于一件事:对整数进行按位运算。你已经有了一个乘法运算符,所以当你想乘法时使用 that。而且你还有一个std::swap 函数。如果要交换两个值,请使用它。优化时最重要的技巧之一是编写可读的、有意义的代码,这也许令人惊讶。这使您的编译器能够理解代码并对其进行优化。 std::swap 可以专门针对它所使用的特定类型进行最有效的交换。并且编译器知道几种实现乘法的方法,并且会根据情况选择最快的一种……如果你告诉它。如果你告诉它移位,你只是在误导它。告诉它相乘,它会给你它所拥有的最快的相乘。

      最后,最昂贵的——“不要使用 enum-s 来返回代码,它太 C 风格,对不同的错误使用异常”?

      取决于你问谁。我认识的大多数 C++ 程序员都为两者找到了空间。但请记住,返回码的一件不幸的事情是它们很容易被忽略。如果这是不可接受的,那么在这种情况下,也许您应该更喜欢例外。另一点是 RAII 与异常一起工作得更好,C++ 程序员绝对应该尽可能使用 RAII。不幸的是,由于构造函数不能返回错误代码,异常通常是指示错误的唯一方法。

      但仍然......性能呢?

      怎么样?任何体面的 C 程序员都会很乐意告诉你不要过早地优化。

      您的 CPU 每秒可以执行大约 80 亿条指令。如果您在那一秒内给std::stringstream 打了两次电话,是否会显着降低预算?

      您无法预测性能。您无法制定可以快速编写代码的编码指南。即使你从不抛出一个异常,也从不使用stringstream,你的代码仍然不会自动变快。如果您在编写代码时尝试进行优化,那么您将花费 90% 的精力来优化几乎从未执行过的 90% 的代码。为了获得可衡量的改进,您需要关注构成 95% 执行时间的 10% 代码。试图让一切变得快速只会导致浪费大量时间而没有什么可展示的,而且代码库也更丑陋。

      【讨论】:

      • +1 表示“智能指针并非生而平等”,当然还有其他。
      • 非常感谢您的完整回答!接受并注明
      • 一篇好文章。一些评论:std::swap 的交换方式与 boost::lexical_cast 的简单转换方式相同 - 我真诚地相信 Boost Wizards 能够以最有效的方式转换我的字符串,无论是字符串流还是 C 函数或一些 hackage ,我不在乎里面有什么。此外,最后一段绝对应该是第一段。 :)
      • “关于返回码的一个不幸的事情是它们很容易被忽略” - 不幸的是,C++ 异常也很容易被忽略......虽然被忽略的返回码可能会或可能不会产生不良后果,未处理的异常将产生致命的后果。
      • @JonHarrop:是的,ARM CPU 也是如此。至于开销和析构函数,为什么你认为“析构函数调用删除”会比“我手动调用删除”慢?你是对的,如果你早点删除对象,那么这可能会稍微减轻注册压力,因此可能会稍微提高性能。但这与智能指针无关。更改程序的语义总是会影响性能。如果语义(和删除顺序)没有改变,那么使用智能指针和原始指针就没有开销
      【解决方案4】:

      除了@Jerry Coffin 的回答,我认为这非常有用,我想提出一些主观意见。

      • 问题是程序员往往会变得花哨。也就是说,我们大多数人真的很喜欢仅仅为了它而编写花哨的代码。只要您自己做这个项目,这完全没问题。请记住,好的软件是二进制代码按预期工作的软件,而不是源代码干净的软件。然而,当涉及到由很多人开发和维护的大型项目时,编写更简单的代码在经济上会更好,这样团队中的任何人都不会浪费时间来理解你的意思。即使以运行时间为代价(自然成本很小)。这就是为什么包括我自己在内的许多人不鼓励使用异或技巧而不是赋值的原因(您可能会感到惊讶,但那里有非常多的程序员没有听说过异或技巧)。无论如何,异或技巧只适用于整数,而且交换整数的传统方式无论如何都非常快,所以使用异或技巧只是很花哨。

      • 使用 itoa、atoi 等代替流更快。是的。但是快多少呢?不多。除非您的大多数程序只进行从文本到字符串的转换,反之亦然,否则您不会注意到差异。为什么人们使用 itoa、atoi 等?好吧,他们中的一些人这样做,因为他们不知道 c++ 替代方案。另一组这样做是因为它只是一个 LOC。对于前一组 - 你真丢脸,对于后者 - 为什么不 boost::lexical_cast?

      • 异常......啊......是的,它们可能比返回码慢,但在大多数情况下并非如此。返回代码可以包含信息,这不是错误。异常应该用于报告严重的错误,那些不能被忽略。有些人忘记了这一点,并使用异常来模拟一些奇怪的信号/插槽机制(相信我,我已经看到了,呸)。我个人的看法是,使用返回码并没有错,但严重的错误应该报告异常,除非分析器表明不使用它们会大大提高性能

      • 原始指针 - 我自己的观点是:当它与所有权无关时,永远不要使用智能指针。在涉及所有权时始终使用智能指针。当然也有一些例外。

      • 移位而不是乘以 2 的幂。我相信,这是过早优化的典型例子。 x << 3; 我敢打赌,至少 25% 的同事需要一些时间才能理解/意识到这意味着 x * 8;混淆(至少 25%)代码的确切原因是什么?同样,如果分析器告诉您这是瓶颈(我怀疑极少数情况下会出现这种情况),然后开绿灯,继续做(留下评论,实际上这意味着 x * 8

      总结一下。一个好的专业人士承认“好的风格”,理解它们为什么以及何时是好的,并且理所当然地做出例外,因为他知道自己在做什么。一般/差的专业人士分为两类:第一类不承认好的风格,甚至不明白它是什么以及为什么。解雇他们。另一种类型将样式视为教条,这并不总是好的。

      【讨论】:

      • C++ 流真的是我最讨厌的事情之一。关于 atoi 与 stringstreams 的速度。你写“快多少?不多。”通常,C 变体至少快一个数量级。所以相信我,这很重要。另一个缺点是编译时间 - 突然间编译器必须引入并解析数百千字节的模板代码,这在大型项目中可能很重要。雪上加霜的是,等效的 C++ 代码通常也更复杂。
      • @kotlinski:对于那些将 C++ 视为具有一些花哨功能的 C 的人来说,等效的 C++ 代码更加复杂。恕我直言
      • @kotlinski:问题是在将数据从字符串解析为 c++ 类型时,速度的数量级占用了整个程序执行时间多少。在大多数情况下,慢一个数量级甚至不会影响整体性能。在这些情况下,如果使用较慢的版本有助于保证输入是正确的,那么它是值得的。记住:He who puts performance over correctness deserves neither。您宁愿让手术室中的软件能够在旧的奔腾中运行而只发生一些小故障,还是需要顶级 i7 并且是正确的?
      • @Armen:复杂性问题不仅仅是用户代码的语法和语义问题。更大的问题可能在于 C++ 字符串流实现的强制复杂性,这通常会转化为无效的代码。
      • @kotlinsky:我想说non-working C 代码的数量更相关。而且还有很多。因此,正确性论点并不遥远。 (这并不是说 C++ IOStreams 不是一个可笑的怪物。它只是一个让编写错误代码变得更加困难的怪物)
      【解决方案5】:

      移位乘法不会提高 C 语言的性能,编译器会为您做到这一点。请务必乘以或除以 2^n 值以获得性能。

      位域交换也可能会让您的编译器感到困惑。

      我对 C++ 中的字符串处理不是很有经验,但据我所知,很难相信它比 scanf 和 printf 更灵活。

      此外,这些“你永远不应该”的陈述,我通常将它们视为建议。

      【讨论】:

      • 好吧,C++ 中的字符串处理对肉眼来说比printf 行中的数十个格式说明符更具可读性。请不要评判你不知道的东西。 stringstreamscanf 相比同样灵活(如果不是更多,因为可能会继承 streambuf 类),并且可以很好地执行它们的任务。
      • @rubenvb:我将提供一个比较 iostreams 格式化语法与 printf 相比的冗长程度,但在尝试理解它 5 分钟后我放弃了大约 6 行代码。
      • @Matt:我和你在一起。据我所知,如果 iostreams 有优势,那就是它们没有printf 解释格式字符串的可能安全漏洞。
      • @kotlinski:不知道。谢谢。尽管仍有可能直到运行时才知道格式字符串。
      【解决方案6】:

      什么是最佳实践? Wikipedia 的话比我的要好:

      最佳实践是一种技术, 方法、过程、活动、激励、 或奖励传统智慧 认为更有效 交付特定的结果而不是 任何其他技术、方法、过程, 等当应用于特定的 条件或情况。

      [...]

      给定的最佳实践仅 适用于特定条件或 情况,可能必须是 修改或改编为类似 情况。此外,“最佳” 实践可以发展变得更好 随着发现的改进。

      我相信编程中没有普遍真理这样的东西:如果你认为某件事比所谓的“最佳实践”更适合你的情况,那么做你认为正确的事情,但完全知道为什么你做(即:用数字证明)。

      【讨论】:

        【解决方案7】:

        这并不是一个真正的“答案”,但如果您在一个对性能很重要的项目(例如嵌入式/游戏)中工作,人们通常会使用更快的 C 方式而不是您描述的方式中的慢速 C++ 方式。

        例外情况可能是按位运算,其获得的收益并不像您想象的那么多。例如,“不要使用左移乘以 2”。中途编译器将为

        【讨论】:

        • 乘以 2 是 "
        【解决方案8】:
        1. 我建议不要将atoiatol 作为一项规则,但不仅仅是出于风格原因。它们使检测输入错误基本上是不可能的。虽然stringstream 可以做同样的工作,但strtol(例如)是我通常建议的直接替代品。
        2. 我不确定是谁给出了这个建议。在有用时使用智能指针,但在无用时,使用原始指针并没有错。
        3. 真的不知道谁认为在 C++ 中使用按位运算符是“不好的做法”。除非该建议附带某些特定条件,否则我会说这是完全错误的。
        4. 这在很大程度上取决于您在异常输入和(例如)预期但不可用的输入之间划清界限的位置。一般来说,如果您直接从用户那里接受输入,那么您不能(也不应该)将任何东西归类为真正的例外。异常的主要优点(即使在这种情况下)是确保错误不被忽略。 OTOH,我认为这并不总是唯一的标准,所以你不能说这是处理每种情况的正确方法。

        总而言之,在我看来,你得到了一些教条主义到无视现实的建议。它可能最好被忽略或至少被视为关于如何编写 C++ 的一个相当极端的立场,而不一定总是(或永远,必然)应该 编写。

        【讨论】:

        • +1 表示原始指针。 (是的,爱他们,使用他们,我很聪明,我的电脑很聪明,但我喜欢认为我更聪明,即使这可能是错误的)。
        • 我也喜欢原始指针,它们很棒,当您知道自己在做什么时(:
        • 关于第 1 点。在某些情况下,您不需要检测输入错误(错误与 0 相同)。
        • 至于 OP 的第 3 点,我认为对所有按位运算符的推断都是错误的,而不是他提供的 2 个具体案例,因为它们确实有问题。 xor-swap 被延迟,它比使用临时变量慢得多,并且仅适用于 2 个补码。移位 2 而不是乘法/除法是不好的,因为有符号参数的行为不同且不可移植。但这并不意味着应该避免所有位运算符。
        • 鉴于 #3 中使用的具体示例,我想几乎每个人都会同意使用它们不是一个好习惯。
        【解决方案9】:

        为什么异常的昂贵是一个论点?例外是例外,因为它们很少见。他们的表现不影响整体表现。使代码异常安全所必须采取的步骤也不会影响性能。但另一方面,例外是方便和灵活的。

        【讨论】:

        • 是的,它们“方便且灵活”,是的,我说:“它们是例外情况的例外,而不是 .. 美”,但为什么不使用 enum-s 和不同的 return代码,但在可以避免的情况下使用异常的层次结构。这是不是很糟糕的做法。
        • 这真的取决于情况,异常是否使事情变得更容易。对于某些应用程序,应该没有错误和异常行为。与抛出异常相比,拥有一个断言可能更有用,这样您就可以检查调用堆栈。在最终版本发布时,也可以轻松禁用断言。
        • 事实上,在大多数情况下,在一个设计良好的应用程序中应该有断言以及异常处理。在大多数情况下,返回码本身并不是一个坏习惯,只是不够灵活和不方便。在少数情况下,例外是不可接受的。例如,异常不能跨越不同二进制组件的边界。这就是 COM 约定禁止跨 COM 方法边界的异常的原因。
        【解决方案10】:

        我认为您自己回答了大部分问题。我个人更喜欢易于阅读的代码(即使你了解 C 风格,也许下一个阅读你的代码的代码会比较麻烦)和安全的代码(建议使用字符串流、异常、智能指针......)

        如果您确实有考虑按位运算的意义 - 好的。但我经常看到 C 程序员使用 char 而不是几个 bool。我不喜欢这样。

        速度很重要,但大部分时间通常需要在程序中的几个热点进行。因此,除非你测量出某种技术是一个问题(或者你很确定它会成为一个问题),否则我宁愿使用你所谓的 C++ 风格。

        【讨论】:

        • 感谢您的评论。是的,我已经回答了一些问题,但我真的不知道哪个更好,我需要听听其他人的意见。另外,我的意思不是真的非常丑陋和难以理解的东西。无论我之后的程序员是否了解 C 风格的东西,给定的示例都很难理解。例如,我提到的错误处理——每个优秀的程序员都会理解它。另外,如果有人不知道atoi 是什么,那就是google。我不是在谈论一些真正特定于 C 的东西。我会稍微修改一下我的帖子。
        • 嗯,风格是个人喜好的问题,如果 C 和 C++ 都适合解决您的问题,您可以从中选择。但我仍然觉得异常比返回码更安全(返回码经常被忘记检查并且难以传递)。
        • 异常,怎么说呢,异常,返回码是不是它们返回码是异常的反面,都是正常的。它们不应该互换使用。
        • “忘记检查并且难以通过” - 是的,这就是我指定深度级别的原因 - 我的意思是不要将错误代码传递到超过一两个级别(调用函数),因为那时真的很难通过和检查。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-03-20
        • 2011-07-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-20
        • 1970-01-01
        相关资源
        最近更新 更多