2014-11-21 41 views
17

我最近想知道#definestatic const之间有什么区别是C以及为什么有两种方法可以做同样的事情。我发现有些人在这里也有类似的问题:“static const”vs“#define”为了提高效率C

很多人谈论的最佳实践和惯例,以及给予使用其中一个的实际原因,比如需要传递指针t一个常数,我可以用static const来做,但不能用#define。然而,我还没有找到任何人谈论这两者的效率比较。

从我的理解对Ç预处理,如果我有这样的语句:

#define CONSTANT 6 

我创建可以这样

char[CONSTANT]使用一个恒定值实际上是在实际编译之前用此语句替换为char[6]

这对我来说似乎是它会比使用 static const constant = 6;,因为这将创建一个名为常数变量,将生活我以为会来一些比#define行李多堆栈上更有效率。假设我需要一个常量,在这种情况下,我可以选择使用预处理器#definestatic const语句,而没有明显的原因可以选择一个,而效率更高?我该如何去测试它自己呢?

+4

我不明白为什么编译器不应该代替'静态的,我们可以用下面的程序(see it live)看到这const int foo = 6'在编译时带有文字“6”。 – Philipp 2014-11-21 18:59:14

+2

#define'd常量实际上只存在于源代码和编译器中。它的价值将会被放置在任何使用它的地方,例如'FOO(恒定)'。一个'静态常量常量'将为变量本身分配一些永久存储空间,并且它的每个地方都会引用这个值。对于一个简单的字符,这是一个微不足道的节省 - 内部的变量指针/引用将比“char”更大。但是如果这个价值更长,那么常数会更便宜。 – 2014-11-21 19:01:56

+3

你正在做一个假设,“恒定”会在栈上生活。一个优化编译器可以(通常会)只是用'6'替换名字'constant',就像使用宏一样。所以不,你不会通过使用宏来获得任何速度(加上,即使它确实有所作为,你正在谈论一个微型优化,这对你的代码性能影响可能微乎其微)。 – Cornstalks 2014-11-21 19:03:13

回答

30

考虑以下2个测试文件

Test1.c:采用静态常量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 

在这两种情况下,都不使用外部存储器。但区别在于,#define取代foo的值,因此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当它优化相同。

+0

您是否在优化测试? – Cornstalks 2014-11-21 20:05:42

+0

@Cornstalks我已经通过优化测试过,结果相同。 – Venkatesh 2014-11-21 20:16:40

+2

我认为在优化关闭的情况下做这个测试并不合理。我刚刚在我的机器上编译了'-O2',两个程序都产生**完全相同的程序集。所以不,我想我们可以说宏观并不一定更好。 – Cornstalks 2014-11-21 22:37:49

3

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

这将创建一个名为常量的变量,它将生活在栈上,我认为它会带来比#define更多的行李。

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

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

这取决于编译器与架构。我觉得有人认为#define有很大的优势。它没有。最明显的案例是一个复杂的评估或函数调用(比如sin(4.8)。考虑一个循环内使用恒定的。一个正常范围的常数可以计算一次。定义在每次迭代可以评估。

我究竟会去这个测试自己?

阅读每次使用编译器,并测量产生的装配。

如果你想要一个经验法则,我会说:“用一个恒定的,除非#define为您提供该方案中的可衡量的改进“

在GCC文档中有关于此的良好写法。也许有人记得它到底是什么。不(至少不应当)在堆栈上创建

1

static const变量;当程序被加载时,它们的空间被放置在一边,所以不应该存在与它们的创建相关的运行时间损失。

可能与他们的初始化相关联的运行时间延长。虽然我使用的gcc版本在编译时初始化常量,我不知道这种行为有多普遍。如果有这样的运行时间损失,它只会在程序启动时出现一次。

除此之外,静态const -qualified对象和之间的运行时性能差异文字(这是一个宏最终会扩大到)应该是可以忽略不计,以不存在的,根据字面的类型和涉及的操作。

笨示例(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部分:

[[email protected]]~/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,所以没有runtim初始化惩罚。

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 <[email protected]> 
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 <[email protected]> 

这会转化为可衡量的运行时间性能差异吗?也许,在正确的环境下。如果你在一个紧密的循环中CPU约束数十万次,那么是的,使用一个宏(解析为文字)在static const变量可能可测量得更快。如果这是在程序的整个生命周期中发生的一次,那么差异太小而无法测量,并且没有强制性的理由使用宏来替代static const变量。

一如既往,正确性和可维护性比性能更重要。使用static const代替宏,你不太可能犯错。考虑以下情形:

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

什么答案,你会期望,什么答案,你会得到?相比之下,与

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

是的,你可以通过使用括号解决宏观情况 - (1 + 2)。问题是,如果您使用static const对象,则此场景不是问题。这是一个少开一枪的方式。


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

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

-2

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

因为我们在谈论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。运行时不会知道你正在溢出数组的末尾。 (但取决于你如何编写代码,编译器可能会在编译时捕获错误)

我掩盖了一些细节(毫无疑问,我甚至没有意识到),但这是它的要点。 (我欢迎您的评论)

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

但是,说了这么多,从来没有担心保存一个字节的空间。代码可维护性更重要。引入单个微小错误的风险,或者使代码的可读性略差,这些风险比额外的几个字节或额外的处理器周期的代价要高出万亿亿倍。使用常量和枚举来利用列出的所有优点here

3

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

对于您的特定问题,现代优化编译器应该能够为这两种情况生成相同的代码,并且事实上只是将它们优化为常量。

#include <stdio.h> 

#define CONSTANT 6 
static const int constant = 6; 

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

在这两种情况下都访问降低到以下:

movl $6, %esi #,