【问题标题】:Do macros in C++ improve performance?C++ 中的宏能提高性能吗?
【发布时间】:2016-07-12 18:25:28
【问题描述】:

我是 C++ 的初学者,我刚刚了解到宏通过在需要时替换文本来工作。在这种情况下,这是否意味着它使 .exe 运行得更快?这与内联函数有何不同?

例如,如果我有以下宏:

#define SQUARE(x) ((x) * (x))

和正常功能:

int Square(const int& x)
{
    return x*x;
}

和内联函数:

inline int Square(const int& x)
{
    return x*x;
}

这三者之间,尤其是内联函数和宏之间的主要区别是什么?谢谢。

【问题讨论】:

  • 先让它工作。然后,让它快速工作。如果正确性无关紧要,那么什么都不做的函数是最快的。通常,宏比函数更难正确。除非分析另有说明,否则宏与内联函数的微优化通常是浪费精力。
  • “宏通过替换文本工作”和“宏使您的程序更快”是两个几乎不相关的语句。

标签: c++ c macros inline preprocessor


【解决方案1】:

如果可能,您应该避免使用宏。内联函数总是更好的选择,因为它们是类型安全的。内联函数应该和宏一样快(如果它确实被编译器内联;请注意,inline 关键字没有绑定,而只是对编译器的提示,如果不能进行内联,编译器可能会忽略它)。

PS:作为一种风格,避免将const Type& 用于fundamental 的参数类型,例如intdouble。只需使用类型本身,换句话说,使用

int Square(int x)

因为副本不会影响(甚至更糟)性能,请参阅例如this question了解更多详情。

【讨论】:

  • 感谢您的快速回答和提示。我是这样写的(const Type&),因为在我读过的一门课程中,它说在性能方面复制了“成本”。它没有提到任何关于基本类型的内容,但现在我想我只会将它用于用户定义的类型。再次感谢!
  • @GeorgeGabriel 不客气。课程中的解释是否正确,如果您遵循它,您不会失去任何东西。但是,对于基本类型,通过引用传递与复制一样昂贵,因为在内部传递了指向数据的指针的副本。从风格上讲,C++ 程序员倾向于按值传递基本类型。
  • 好的,我明白了。再次感谢。在编写代码时,了解并养成良好的习惯是件好事。
  • 在极少数情况下,可以使用宏来确保代码是内联的。这绝对是一种强硬的方法,您需要进行性能测试以证明它实际上是有帮助的,而不是像普通人一样编写代码。
  • @vsoftco:建议是正确的,原因是错误的。基本类型通常不使用指针传递;它们在 CPU 寄存器中传递。这意味着它们可以直接被操作,而且不会比这更快。
【解决方案2】:

宏翻译为:用模式 B 愚蠢地替换模式 A。这意味着:一切都发生在编译器启动之前。有时它们会派上用场;但总的来说,应避免使用它们。因为你可以做很多事情,然后在调试器中,你不知道发生了什么。

此外:你的表现方法很好,很天真,可以说是友好的。首先你要学习这门语言(这对于现代 C++ 来说,因为有很多重要的概念和事情是绝对需要知道和理解的)。然后你练习,练习,练习。然后,当您真正遇到现有应用程序存在性能问题的地步时;然后进行分析以了解真正的问题

换句话说:如果您对性能感兴趣,那么您就问错了问题。您应该更多地担心架构(例如:潜在的瓶颈)、配置(系统中不同节点之间的延迟)等等。当然,你应该运用常识;不要编写明显浪费内存或 CPU 周期的代码。但有时一段运行速度慢 50% 的代码……可能更容易阅读和维护 500%。如果执行时间是 500 毫秒,而不是 250 毫秒;这可能完全没问题(除非该特定部分每分钟调用一千次)。

【讨论】:

  • 我很清楚我根本不会掌握这门语言,我想尽可能多地学习。这个问题的目的只是为了了解有什么区别,因为我不打算真正改进我现在正在编写的任何代码,因为它非常简单。感谢您的回答和提示。我意识到在我真正考虑提高我的代码性能之前,这将是 C++ 世界的漫长旅程。
【解决方案3】:

宏和内联函数的区别在于宏是在编译器看到之前处理的。

在没有优化标志的编译器 (clang++) 上,square 函数不会被内联。它生成的代码是这样的

4009f0:       55                      push   %rbp
4009f1:       48 89 e5                mov    %rsp,%rbp
4009f4:       89 7d fc                mov    %edi,-0x4(%rbp)
4009f7:       8b 7d fc                mov    -0x4(%rbp),%edi
4009fa:       0f af 7d fc             imul   -0x4(%rbp),%edi
4009fe:       89 f8                   mov    %edi,%eax
400a00:       5d                      pop    %rbp
400a01:       c3                      retq   

imul 是完成工作的汇编指令,其余的是移动数据。 调用它的代码看起来像

  400969:       e8 82 00 00 00          callq  4009f0 <_Z6squarei>

我将 -O3 标志添加到内联它,并且 imul 显示在 C++ 代码中调用该函数的主函数中

0000000000400a10 <main>:
400a10:       41 56                   push   %r14
400a12:       53                      push   %rbx
400a13:       50                      push   %rax
400a14:       48 8b 7e 08             mov    0x8(%rsi),%rdi
400a18:       31 f6                   xor    %esi,%esi
400a1a:       ba 0a 00 00 00          mov    $0xa,%edx
400a1f:       e8 9c fe ff ff          callq  4008c0 <strtol@plt>
400a24:       48 89 c3                mov    %rax,%rbx
400a27:       0f af db                imul   %ebx,%ebx

为您的机器获取汇编语言的基本句柄并在源代码上使用 gcc -S 或在二进制文件上使用 objdump -D(就像我在这里所做的那样)来查看到底发生了什么是合理的做法。

使用宏而不是内联函数会得到非常相似的结果

0000000000400a10 <main>:
400a10:       41 56                   push   %r14
400a12:       53                      push   %rbx
400a13:       50                      push   %rax
400a14:       48 8b 7e 08             mov    0x8(%rsi),%rdi
400a18:       31 f6                   xor    %esi,%esi
400a1a:       ba 0a 00 00 00          mov    $0xa,%edx
400a1f:       e8 9c fe ff ff          callq  4008c0 <strtol@plt>
400a24:       48 89 c3                mov    %rax,%rbx
400a27:       0f af db                imul   %ebx,%ebx

注意宏的许多危险之一:它有什么作用?

x = 5; std::cout << SQUARE(++x) << std::endl; 

36?不,42。它变成了

std::cout << ++x * ++x << std::endl; 

变成 6 * 7

不要被那些告诉你不要关心优化的人推迟。使用 C 或 C++ 作为您的语言本身就是一种优化。如果您在浪费时间,请尝试解决并保持理智。

【讨论】:

  • SQUARE(++x) 的真正危险不是它产生42 而不是36。危险在于它在单个语句中两次修改变量,这会产生未定义的行为。产生42 只是一种可能的结果。它可能会产生42,它可能会产生36,它可能会产生49,或者它可能会重新格式化您的硬盘。
  • 人们不会告诉你不要担心优化。他们会告诉你不要沉迷于过早的优化。这包括在您知道有实际需要之前不要沉迷于优化。
  • 是的,无论如何。当重点是“它不会按照你的意图做”时,这里不是很重要你是对的,当然,带有 -O3 的 gcc 给出 49,clang 给出 42。两者都用 -Wall 给出了一个很好的警告,所以肯定用 -Wall 编译可能是更有用的注意事项。
  • “我们应该忘记小的效率,比如说大约 97% 的时间:过早的优化是万恶之源。但我们不应该放弃那关键的 3% 的机会。” ——克努特。不要错过这 3%,要懂得分析和去做。当一个真正的关于理解优化的问题出现时,人们会不断地从引用的中间跳出 7 个单词。这个问题就是其中之一,值得明智地对待。
  • 问题是你在没有上下文的情况下给出了关于优化的评论。你也错过了很多关于优化的问题在他们对需求进行任何分析之前询问如何去做(或要求比较 X 与 Y 的性能)。无论如何,你已经阐明了你的态度,所以享受你的反对票吧。
【解决方案4】:

宏只是执行文本替换来修改源代码。

因此,宏本身不会影响代码的性能。您用于设计和编码的技术显然会影响性能。因此,宏对性能的唯一影响是基于宏的作用(即您编写宏要发出的代码)。

宏的最大危险是它们不尊重范围。他们所做的改变是无条件的、跨职能的,诸如此类。编写宏以使它们按预期运行有很多微妙之处(避免代码中的意外副作用,避免未定义的行为等)。这意味着使用宏的代码更难理解,也更难正确。

在最好的情况下,使用现代编译器,使用宏可以获得的性能提升与使用内联函数可以获得的性能提升相同 - 以增加代码行为不正确的机会为代价。因此,您最好使用内联函数 - 与宏不同,它们是类型安全的并且可以与其他代码一致地工作。

现代编译器可能选择不内联函数,即使您已将其指定为内联。如果发生这种情况,您通常无需担心 - 现代编译器在决定是否应内联函数方面比大多数现代程序员做得更好。

【讨论】:

    【解决方案5】:

    只有当它的参数本身是一个#define'd 常量时,使用这样的宏才有意义,因为计算将由预处理器执行。即便如此,请仔细检查结果是否符合预期。

    在处理经典变量时,应该首选(内联)函数形式:

    • 它是类型安全的;
    • 它将以一致的方式处理用作参数的表达式。这不仅包括 Peter 引用的 per/post 增量的情况,而且当参数本身是一些计算密集型表达式时,使用宏形式会强制对该参数进行两次评估(顺便说一下,这可能不一定会评估为相同的值) 而函数只有一次。

    我不得不承认,我曾经编写过这样的宏来快速制作看似简单的函数的原型,但是那些让我多年来失去的时间最终改变了我的想法!

    【讨论】:

      猜你喜欢
      • 2014-05-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-12
      • 1970-01-01
      • 2021-07-19
      • 2016-11-09
      • 2014-06-11
      相关资源
      最近更新 更多