"静态const" vs" #define"提高效率

时间:2014-11-21 18:56:52

标签: c performance constants c-preprocessor

我最近想知道#definestatic const之间的区别在于 C ,以及为什么有两种方法可以做同样的事情。我发现有些人在这里有类似的问题:

很多人谈论最佳实践和惯例,并提供使用其中一个的实际原因,例如需要将指针传递给常量,我可以使用static const但不能使用#define。但是我还没有找到任何人谈论两者效率的比较。

根据我对 C 预处理器的理解,如果我有这样的声明:

#define CONSTANT 6

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

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

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

5 个答案:

答案 0 :(得分:33)

考虑以下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视为相同。

答案 1 :(得分:3)

如果常量的定义对于翻译是可见的,那么编译器肯定能够将其用作优化。

  

这将创建一个名为constant的变量,它将存在于堆栈中,我假设它会带来比#define更多的行李。

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

  

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

这取决于编译器和架构。我觉得有些人认为#define有很大的优势。它没有。显而易见的情况是复杂的评估或函数调用(比如sin(4.8)。考虑在循环中使用的常量。一个适当的作用域常量可以被评估一次。一个定义可以在每次迭代时进行评估。

  

我将如何自行测试?

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

如果你想要一个经验法则,我会说&#34;使用常数,除非#define为你提供了一个可衡量的改进方案&#34;。

GCC文档中有一篇关于此的文章。也许有人会记得它究竟在哪里。

答案 2 :(得分:3)

测试简单优化问题的快捷方法是使用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 ) ;
}

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

movl    $6, %esi    #,

答案 3 :(得分:1)

static const变量不是(至少不应该)在堆栈上创建的;加载程序时,它们的空间被放在一边,因此不应该存在与它们的创建相关的运行时损失。

可能是与其初始化相关的运行时惩罚。虽然gcc的版本我在编译时使用初始化常量;我不知道这种行为有多常见。如果存在这样的运行时惩罚,则仅在程序启动时发生一次。

除此之外,静态const - 限定对象和文字 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的字节多一个字节,并且指令必须有效地取消引用%rip寄存器:

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.如果代码给出了错误的答案或做错了什么,那么代码的速度并不重要。如果没有人可以修复或升级代码,那么代码的速度并不重要,因为他们无法理解代码是如何工作的。如果代码在第一次输入错误时死亡,那么代码的速度并不重要。如果它打开恶意软件的大门,那么代码的速度并不重要

答案 4 :(得分:-2)

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

因为我们正在谈论C,并且假设你在堆栈上声明数组,答案实际上非常有趣。在这种情况下,两者之间不可能存在任何差异。 &#34; 6&#34;实际上并没有在运行时使用!因为您只是使用它来调整堆栈上的数组大小,所以编译器只是使用它来计算变量需要多少堆栈空间。

假设你有一个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))

所以你看,价值&#34; 6&#34;在运行时不使用。实际上,无论你想要什么,你都可以写入索引6,7,8。运行时不知道你是否溢出了数组的末尾。 (但根据您编写代码的方式,编译器可能会在编译时捕获错误)

我在那里掩饰了一些细节(毫无疑问,我甚至都不知道),但这是它的要点。 (我欢迎你的评论)

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

但是,尽管如此,从不担心节省一个字节的空间。代码可维护性更重要。引入单个小错误或使代码可读性稍差的风险,这些风险比额外的几个字节或额外的处理器周期的成本高出一万亿亿倍。使用常量和枚举来利用列出的所有好处here