【问题标题】:"Static const" vs "#define" for efficiency in C在 C 中提高效率的“静态 const”与“#define”
【发布时间】:2015-01-20 00:17:34
【问题描述】:

我最近想知道#definestatic constC 中的区别是什么,以及为什么存在两种方法来做同样的事情。我在这里找到了一些有类似问题的人:

很多人都在谈论最佳实践和约定,并给出使用其中一个而不是另一个的实际理由,例如需要传递一个指向常量的指针,我可以使用 static const 但不能使用#define。但是我还没有找到任何人谈论两者的效率比较。

根据我对 C 预处理器的了解,如果我有这样的陈述:

#define CONSTANT 6

我创建一个可以这样使用的常量值

char[CONSTANT] 在实际编译之前将实际替换为该语句char[6]

在我看来,这似乎比使用 static const constant = 6; 因为这将创建一个名为常量的变量,该变量将存在于堆栈中,我认为它会比#define 带来更多的包袱。假设在我可以选择使用预处理器#definestatic const 语句而没有明显理由选择另一个的情况下,我需要一个常量,哪个更有效?我将如何自己进行测试?

【问题讨论】:

  • 我看不出为什么编译器不应该在编译时用文字 6 替换 static const int foo = 6
  • #define'd 常量实际上只存在于源代码和编译器中。它的值将被替换在它使用的任何地方,例如foo(constant)static const constant 会为变量本身分配一些永久存储空间,并且每个使用它的地方都会引用该值。对于一个简单的字符,这是一个微不足道的节省 - 内部的变量指针/引用将大于“字符”。但如果这个值更长,那么 const 会更便宜。
  • 您假设constant 将存在于堆栈中。优化编译器可以(并且通常会)将名称 constant 替换为值 6,就像使用宏一样。所以不,你不会通过使用宏来获得任何速度(另外,即使它确实有所作为,你所说的微优化很可能对你的代码性能的影响可以忽略不计)。跨度>
  • @Philipp int const *p = &foo; 是一个原因,但我想我们永远不会知道,因为我们没有任何 代码
  • 如果值恰好是int类型,你可以使用枚举破解:enum { CONSTANT = 6 };

标签: c performance constants c-preprocessor


【解决方案1】:

考虑以下 2 个测试文件

Test1.c:使用静态 const foo。

// Test1.c uses static const..

#include <stdio.h>

static const foo = 6;

int main() {
    printf("%d", foo);
    return 0;
}

Test2.c:使用宏。

// Test2.c uses macro..

#include <stdio.h>

#define foo 6

int main() {
    printf("%d", foo);
    return 0;
}

使用gcc -O0(默认)时对应的汇编等价如下,

Test1.c 的组装:

  0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 20             sub    rsp,0x20
   8:   e8 00 00 00 00          call   d <main+0xd>
   d:   b8 06 00 00 00          mov    eax,0x6
  12:   89 c2                   mov    edx,eax
  14:   48 8d 0d 04 00 00 00    lea    rcx,[rip+0x4]        # 1f <main+0x1f>
  1b:   e8 00 00 00 00          call   20 <main+0x20>
  20:   b8 00 00 00 00          mov    eax,0x0
  25:   48 83 c4 20             add    rsp,0x20
  29:   5d                      pop    rbp
  2a:   c3                      ret
  2b:   90                      nop

Test2.c 的组装:

  0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 20             sub    rsp,0x20
   8:   e8 00 00 00 00          call   d <main+0xd>
   d:   ba 06 00 00 00          mov    edx,0x6
  12:   48 8d 0d 00 00 00 00    lea    rcx,[rip+0x0]        # 19 <main+0x19>
  19:   e8 00 00 00 00          call   1e <main+0x1e>
  1e:   b8 00 00 00 00          mov    eax,0x0
  23:   48 83 c4 20             add    rsp,0x20
  27:   5d                      pop    rbp
  28:   c3                      ret
  29:   90                      nop

在这两种情况下,它都没有使用外部存储器。但不同的是,#definefoo 替换为值,static const 是一条指令,因此它将指令指针递增到下一条指令,并使用 1 个额外的寄存器来存储该值。

由此,我们可以说宏比静态常量好,但差别很小。

编辑:当使用-O3 编译选项(即优化时)时,test1.c 和 test2.c 的计算结果相同。

0000000000000000 <main>:
   0:   48 83 ec 28             sub    rsp,0x28
   4:   e8 00 00 00 00          call   9 <main+0x9>
   9:   48 8d 0d 00 00 00 00    lea    rcx,[rip+0x0]        # 10 <main+0x10>
  10:   ba 06 00 00 00          mov    edx,0x6
  15:   e8 00 00 00 00          call   1a <main+0x1a>
  1a:   31 c0                   xor    eax,eax
  1c:   48 83 c4 28             add    rsp,0x28
  20:   c3                      ret
  21:   90                      nop

因此,gcc 在优化时将static const#define 视为相同。

【讨论】:

  • 您是否在进行优化测试?
  • @Cornstalks 我在关闭优化的情况下对其进行了测试,结果相同。
  • 我认为在关闭优化的情况下进行此测试没有多大意义。我刚刚在我的机器上用-O2 编译,两个程序都生成完全相同的程序集。所以不,我认为我们可以说宏不一定更好。
  • 这个答案不正确,请看我的答案,优化后的两种情况应该是一样的。
  • 好吧。让我们尝试多次引用预处理器 foo 并考虑代码大小。
【解决方案2】:

测试简单优化问题的快速方法是使用godbolt

对于您的特定问题,现代优化编译器应该能够为这两种情况生成相同的代码,并且实际上会将它们优化为常数。我们可以通过以下程序 (see it live) 看到这一点:

#include <stdio.h>

#define CONSTANT 6
static const int  constant = 6;

void func()
{
  printf( "%d\n", constant ) ;
  printf( "%d\n", CONSTANT ) ;
}

在这两种情况下都访问 reduce 到以下内容:

movl    $6, %esi    #,

【讨论】:

    【解决方案3】:

    如果常量的定义对翻译可见,编译器当然可以利用它作为优化。

    这将创建一个名为常量的变量,该变量将存在于堆栈中,我认为它会带来比#define 更多的包袱。

    它可以“生活”在多个地方。编译器当然可以替换引用的常量,而不需要静态或堆栈存储。

    假设我需要一个常量,在这种情况下我可以选择使用预处理器#define 或静态 const 语句而没有明显的理由选择另一个,哪个更有效?

    这取决于编译器和架构。我的印象是有些人认为#define 有很大的优势。它没有。最明显的情况是复杂的求值或函数调用(比如sin(4.8)。考虑在循环中使用的常量。适当范围的常量可以求值一次。定义可以在每次迭代时求值。

    而我自己将如何进行测试?

    读取您使用的每个编译器生成的程序集,并进行测量。

    如果您需要经验法则,我会说“使用常量,除非 #define 为您提供了可衡量的改进方案”。

    在 GCC 文档中有一篇很好的文章。也许有人记得它到底在哪里。

    【讨论】:

      【解决方案4】:

      static const 变量没有(至少不应该)在堆栈上创建;加载程序时会为它们留出空间,因此不应存在与创建它们相关的运行时损失。

      可能与它们的初始化相关联的运行时惩罚。尽管我使用的 gcc 版本在编译时初始化了常量;我不知道这种行为有多普遍。如果存在这样的运行时惩罚,它只会在程序启动时发生一次。

      除此之外,静态const-qualified 对象和字面量1(宏最终将扩展为)之间的任何运行时性能差异应该可以忽略不计,具体取决于关于字面量的类型和所涉及的操作。

      愚蠢的例子(gcc version 4.1.2 20070115 (SUSE Linux)):

      #include <stdio.h>
      
      #define FOO_MACRO 5
      
      static const int foo_const = 5;
      
      int main( void )
      {
        printf( "sizeof FOO_MACRO = %zu\n", sizeof FOO_MACRO );
        printf( "sizeof foo_const = %zu\n", sizeof foo_const );
        printf( "      &foo_const = %p\n",  ( void * ) &foo_const );
      
        printf( "FOO_MACRO = %d\n", FOO_MACRO );
        printf( "foo_const = %d\n", foo_const );
      
        return 0;
      }
      

      输出:

      sizeof FOO_MACRO = 4
      sizeof foo_const = 4
            &foo_const = 0x400660
      FOO_MACRO = 5
      foo_const = 5
      

      foo_const 的地址在二进制文件的 .rodata 部分中:

      [fbgo448@n9dvap997]~/prototypes/static: objdump -s -j .rodata static
      
      static:     file format elf64-x86-64
      
      Contents of section .rodata:
       40065c 01000200 05000000 73697a65 6f662046  ........sizeof F
                       ^^^^^^^^
       40066c 4f4f5f4d 4143524f 203d2025 7a750a00  OO_MACRO = %zu..
       40067c 73697a65 6f662066 6f6f5f63 6f6e7374  sizeof foo_const
       40068c 203d2025 7a750a00 20202020 20202666   = %zu..      &f
       40069c 6f6f5f63 6f6e7374 203d2025 700a0046  oo_const = %p..F
       4006ac 4f4f5f4d 4143524f 203d2025 640a0066  OO_MACRO = %d..f
       4006bc 6f6f5f63 6f6e7374 203d2025 640a00    oo_const = %d..
      

      请注意,对象已经初始化为 5,因此没有运行时初始化惩罚。

      printf 语句中,将foo_const 的值加载到%esi 的指令比加载文字值0x5 的指令需要多一个字节,并且该指令必须有效地取消引用@987654333 @注册:

      400538:       be 05 00 00 00          mov    $0x5,%esi
                    ^^^^^^^^^^^^^^
      40053d:       bf ab 06 40 00          mov    $0x4006ab,%edi
      400542:       b8 00 00 00 00          mov    $0x0,%eax
      400547:       e8 e4 fe ff ff          callq  400430 <printf@plt>
      40054c:       8b 35 0e 01 00 00       mov    270(%rip),%esi        # 400660 <foo_const>
                    ^^^^^^^^^^^^^^^^^
      400552:       bf bb 06 40 00          mov    $0x4006bb,%edi
      400557:       b8 00 00 00 00          mov    $0x0,%eax
      40055c:       e8 cf fe ff ff          callq  400430 <printf@plt>
      

      这会转化为可衡量的运行时性能差异吗?也许,在合适的情况下。如果您在紧密循环中执行数十万次 CPU 绑定的操作,那么是的,在 static const 变量上使用宏(解析为文字)可能会明显更快。如果这是在程序的生命周期内发生一次的事情,那么差异太小而无法衡量,并且没有令人信服的理由在 static const 变量上使用宏。

      与往常一样,正确性和可维护性比性能更重要2。使用static const 而不是宏,您不太可能出错。考虑以下场景:

      #define FOO 1+2
      ...
      x = FOO * 3;
      

      你会期待什么答案,你会得到什么答案?比较一下

      static const int foo = 1+2;
      ...
      x = foo * 3;
      

      是的,您可以通过使用括号 - (1 + 2) 来修复宏大小写。关键是,如果您使用 static const 对象,这种情况就不是问题。这是一种更少的射击自己的方式。


      1. 现在,我只讨论简单的标量文字(整数或浮点数),而不是复合文字;尚未调查他们的行为。

      2. 如果你的代码给出了错误的答案或做错了事,你的代码有多快并不重要。如果没有人可以修复或升级它,因为他们无法理解它是如何工作的,那么你的代码有多快并不重要。如果您的代码在输入错误的第一个提示时死亡,那么它的速度有多快并不重要。如果您的代码打开了恶意软件的大门,那么它的速度并不重要。

      【讨论】:

        【解决方案5】:

        你已经完全改变了你的问题。 这是我对您的新问题的回答:

        因为我们在谈论C,并且假设您在堆栈上声明数组,所以答案实际上很有趣。在这种情况下,两者之间不可能有任何区别。 “6”实际上并没有在运行时使用!因为您只是使用它来确定堆栈上数组的大小,所以编译器只是使用它来计算变量需要多少堆栈空间。

        假设您有一个 32 位地址空间,并且您的本地函数包含这个 6 字节数组 (myArray) 和一个无符号 32 位整数 (myInt)。编译器创建以下指令用于输入此函数: - 将 4 字节的返回地址写入堆栈 - 将堆栈指针向前移动 10 个字节
        在执行函数时,运行时不知道任何变量的名称或大小。如果您的代码显示

        myInt = 5;
        myArray[myInt] = 25;
        

        那么编译器将生成这些指令:

        - write 00000000 00000000 00000000 00000101 starting at address (StackPointer - 4)
        - write 00001101 starting at (StackPointer - 10 + (value at Stackpointer - 4))
        

        所以你看,值“6”在运行时没有使用。事实上,您可以随意写入索引 6、7、8。运行时不会知道您溢出了数组的末尾。 (但取决于您编写代码的方式,编译器可能会在编译时捕获错误)

        我在那里掩盖了一些细节(毫无疑问,有些我什至不知道),但这就是它的要点。 (欢迎你们的cmets)

        将 6 定义为“const”实际上可能会导致该值存储到 4 个字节的无用空间中,但这不会影响执行。显然它会被优化掉,因为它从未被使用过。

        但是,说了这么多,永远不要担心节省一个字节的空间。代码可维护性更为重要。引入单个小错误或使您的代码可读性降低的风险,这些风险比额外几个字节或额外处理器周期的成本高出一万亿倍。使用常量和枚举来利用列出的所有好处here

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-03-23
          • 2010-12-21
          • 1970-01-01
          • 2019-03-25
          • 2010-12-02
          • 1970-01-01
          • 2018-04-18
          • 2020-09-19
          相关资源
          最近更新 更多