C++ 中的宏会提高性能吗?

Do macros in C++ improve performance?

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

例如,如果我有以下宏:

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

和正常功能:

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

和内联函数:

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

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

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

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

int Square(int x)

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

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

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

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

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

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

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

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

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

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

在没有优化标志的我的编译器 (clang++) 上,平方函数不会被内联。它生成的代码如下所示

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 标志添加到 Inline 中,并且 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    [=12=]xa,%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    [=12=]xa,%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++ 作为您的语言本身就是一种优化。如果你在浪费时间并保持理智,那就试试吧。

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

处理经典变量时,应优先使用(内联)函数形式:

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

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