const-correctness是否为编译器提供了更多的优化空间?


77

我知道它可以提高可读性并使程序不易出错,但是它可以在多大程度上提高性能呢?

另外,引用和const指针之间的主要区别是什么?我假设它们以不同的方式存储在内存中,但是怎么办呢?


8
不,但是它可以给您自己的代码空间进行更多优化。一个不可变的字符串类就是一个例子,它提供了恒定时间的子字符串操作,并且可以在不进行动态分配的情况下从文字中进行创建。其中有很多功能const,但大多数是人类的功能,以及我们编写的代码。编译器无法信任任何东西,因此无法以相同的方式合理地受益。
干杯和健康。-Alf

Answers:


70

[编辑:好的,所以这个问题比起初我想的要微妙。]

声明指向const或const引用的指针永远不会帮助任何编译器优化任何内容。(尽管请参见此答案底部的“更新”。)

const声明仅指示如何的标识符将内使用范围其声明的; 它并不表示基础对象不能更改。

例:

int foo(const int *p) {
    int x = *p;
    bar(x);
    x = *p;
    return x;
}

编译器不能假定对*p的调用未对其进行修改bar(),因为它p可能是(例如)指向全局int的指针,并且bar()可能会对其进行修改。

如果编译器对调用方有足够的了解,foo()并且它的内容bar()可以证明bar()没有被修改*p,那么它也可以在没有const声明的情况下执行该证明

但这通常是正确的。因为const仅在声明的范围内起作用,所以编译器已经可以看到您如何处理该范围内的指针或引用。它已经知道您没有在修改基础对象。

简而言之,const在这种情况下,所有要做的就是防止您犯错误。它不会告诉编译器它尚不知道的任何内容,因此与优化无关。

那调用的函数foo()呢?喜欢:

int x = 37;
foo(&x);
printf("%d\n", x);

编译器可以证明此打印37,因为foo()需要const int *

不。即使foo()使用指向const的指针,也可能会舍弃const-ness并修改int。(这不是未定义的行为。)再次,编译器通常无法做出任何假设;如果知道足够多的信息foo()可以进行这样的优化,即使没有,它也会知道const

唯一const可能允许优化的情况是这样的情况:

const int x = 37;
foo(&x);
printf("%d\n", x);

在这里,要x通过任何机制进行修改(例如,通过获取指向它的指针并丢弃const),都将调用未定义行为。因此,编译器可以自由地假定您不这样做,并且可以将常量37传播到printf()中。对于您声明的任何对象,这种优化都是合法的const。(实际上,您永远不会引用的局部变量将不会受益,因为编译器已经可以查看您是否在其范围内对其进行了修改。)

要回答您的“附带说明”问题,(a)const指针是指针;(b)const指针可以等于NULL。您是正确的,内部表示形式(即地址)很可能是相同的。

[更新]

正如克里斯多夫在评论中指出的那样,我的回答是不完整的,因为它没有提及restrict

C99标准的6.7.3.1(4)节规定:

在每次执行B的过程中,令L为具有基于P的&L的任何左值。如果使用L来访问它指定的对象X的值,并且还对X进行了修改(通过任何方式),则以下要求适用:T不得为const限定。...

(这里B是一个基本块,作用域T的限制指针P在其上。)

因此,如果这样foo()声明C函数:

foo(const int * restrict p)

...然后,编译器可能会假设*pp-的执行期间(即在-的执行期间)不会进行任何修改,foo()因为否则行为将是未定义的。

因此,原则上,restrict与指向常量的指针结合使用可以实现上述两种优化。我想知道是否有任何编译器实际上实现了这种优化?(至少不是GCC 4.5.2。)

请注意restrict,除了作为编译器特定的扩展之外,它仅存在于C中,而不存在于C ++中(甚至不存在于C ++ 0x中)。


10
“所以,在全局变量的情况下-您不应该使用它。”对不起,但这是一个糟糕的建议-在很多情况下,可以并且应该使用全局变量。
亚当·罗森菲尔德

4
通过调用UB修改任何 const变量const_cast,而不仅是全局变量const...
ildjarn 2011年

1
@Billy:答案可能是尽可能简单的指针-TO-正确const有关,但它至少不完全-尼莫忘了restrict:声明foo()int foo(const int *restrict p)确实会告诉编译器将指向的值p不会被调用来改变作者foo()
Christoph

6
@Billy:重新阅读C99第6.7.3.1节:restrict实际的意思是(1)如果restrict修改了-qualified指针指向的对象,则所有访问(包括触发该修改的访问)必须基于该访问指针,以及(2)-与此处相关的部分-如果发生任何修改,则该指针一定不能被const限定;底线:在该指针的有效期内修改由restrict-qualified-to-指向的对象是非法的const
Christoph

1
@AnishRamaswamy:我毫不怀疑“限制”在某些情况下会有所不同,因为它有助于别名分析...它没有做的就是启用这种在调用中传播常量的特殊优化。我知道是因为我尝试了一下,然后查看了汇编代码。(我只是再次使用GCC 4.8.1;结果相同。)
Nemo

6

constC ++存在两个问题(就优化而言):

  • const_cast
  • mutable

const_cast 这意味着即使您通过const引用或const指针传递对象,该函数也可能会舍弃const-ness并修改该对象(如果该对象不是const开头,则允许)。

mutable表示即使一个对象是const,它的某些部分也可能被修改(缓存行为)。另外,const即使在逻辑上是对象状态的一部分,也可以在方法中修改指向(而不是被拥有)的对象。最后,全局变量也可以修改...

const 是为了帮助开发人员尽早发现逻辑错误。


如果函数使用抛弃const const_cast,然后修改该对象,则它是未定义的行为。编译器可以放心地假设这种情况永远不会发生。
James Picone

1
@JamesPicone:是和否。修改const对象是未定义的行为,但这const_cast是丢弃const引用或指针的属性来修改非const对象的原因。因此,编译器可以从中获得一些性能提升const,而没有这种性能提升的只是一小部分const_cast
Matthieu M.

6

const不禁想到了两种情况,适当的-qualification允许进行其他优化(在无法进行整个程序分析的情况下):

const int foo = 42;
bar(&foo);
printf("%i", foo);

在这里,编译器知道打印42而不必检查主体bar()(主体在当前翻译单元中可能不可见),因为对它的所有修改foo都是非法的(这与Nemo的示例相同)。

但是,这也有可能无标记fooconst通过声明bar()

extern void bar(const int *restrict p);

在许多情况下,程序员实际上希望将restrict合格的指向对象的指针const而不是普通的指向对象的指针const作为函数参数,因为只有前者才能保证所指向对象的可变性。

关于问题的第二部分:出于所有实际目的,可以将C ++引用视为具有自动间接指向的常量指针(而不是指向常量值的指针!)-它不是“更安全”或“更快”的比指针要方便。


如果const引用传递了非常量对象的值,则编译器可以传播该值:由于§5.2.2/ 5,它可以静默创建所传递对象的临时副本。
Ruslan

4

常量正确性通常对性能没有帮助;大多数编译器甚至不费心跟踪前端之外的constness。根据情况将变量标记为const会有所帮助。

引用和指针在内存中的存储方式完全相同。


2
您能否提供一个有助于(或应该)帮助的示例?在同一段中说“不帮助”,“可以帮助”和“取决于情况”并不能真正弄清情况。
安德烈·卡隆

const可以帮助构建如下(以及更复杂的变体):int x = 10; int f(){return x; } vs. const int x = 10; int f(){return x; }
2011年

2

这实际上取决于编译器/平台(它可能有助于对尚未编写的某些编译器或从未使用过的平台进行优化)。除了给出某些功能的复杂性要求外,C和C ++标准没有提及任何性能。

指向const的指针和引用通常无助于优化,因为在某些情况下可以合法地删除const限定,并且有可能可以通过其他非const引用来修改对象。另一方面,将对象声明为const可能会有所帮助,因为它可以保证不能修改该对象(即使将其传递给编译器不知道其定义的函数)。这允许编译器将const对象存储在只读存储器中,或将其值缓存在集中位置,从而减少了复制和检查修改的需要。

指针和引用通常以完全相同的方式实现,但是同样,这完全取决于平台。如果你真正感兴趣的,那么你应该看看在生成的机器码你的平台和编译器在你的程序(如果你确实使用的是生成机器代码的编译器)。


1

一件事是,如果声明全局变量const,则可以将其放在库或可执行文件的只读部分中,从而可以通过只读mmap在多个进程之间共享它。至少在全局变量中声明了许多数据的情况下,这在Linux上可能是一个巨大的内存胜利。

另一种情况,如果您声明一个常量全局整数,float或enum,则编译器也许能够将常量内联而不使用变量引用。尽管我不是编译器专家,但我认为这样做的速度要快一些。

引用只是在实现方面位于其下方的指针。


与我-1回答的其他问题相同-如果确实从未在整个程序中写入它,则无论是否被标记,编译器都将能够执行这类优化const
Billy ONeal

1
@比利:不是extern变量。
Matthieu M.

@Matthieu:他专门指的是整个程序的优化,在链接时extern停止有意义时执行。
2011年

@Dennis:是的,对不起,我习惯于处理DLL,WPO失去了很多功能。
Matthieu M.

@Billy也许是理论上的,但至少在Linux上实际上是要使用共享库(或编写共享库),而编译器无法做到这一点。Linux上哪种程序不使用共享库?基本上没有。我不确定gcc在任何情况下都可以执行此操作,无论是共享库还是不执行-如果这样做,这是一个相当新的功能。
Havoc P

0

它可以帮助提高性能,但前提是您直接通过其声明访问该对象。引用参数等无法优化,因为可能存在指向对象的其他路径,而该路径最初并未声明为const,并且除非您使用的是声明,否则编译器通常无法确定您所引用的对象是否实际上已声明为const。

如果使用的是const声明,则编译器将知道外部编译的函数体等无法对其进行修改,因此您会从中受益。当然,像const int之类的东西会在编译时传播,所以这是一个巨大的胜利(相比于int)。

引用和指针的存储方式完全相同,只是语法上的行为不同。引用基本上是重命名的,因此是相对安全的,而指针可以指向许多不同的事物,因此更强大且更容易出错。

我猜想const指针在结构上将与引用相同,因此机器代码和效率将相同。真正的区别是语法-引用是一种更简洁,更易于阅读的语法,并且由于不需要指针提供的额外机制,因此从风格上考虑,首选引用。


如果该项目没有被修改,那么如今的体面编译器将知道它const很好,而无需您告知。(谈到最近的MSVC ++和GCC,它们都具有链接时间代码生成)
Billy ONeal

1
@Billy:链接时优化并不是万灵药,因为所有库都必须在启用lto的情况下进行编译,更不用说共享库了–编译器无法推理从未见过的代码...
Christoph

@Christoph:编译器永远不能相信const它也看不到的变量的限定条件。该内存可能被完全const不存在的语言所困扰,甚至不存在,甚至可能是用原始汇编语言编写的。在这样的情况下,const该库可以访问变量,则编译器必须忽略const限定符。因此,我不认为这与这个问题有什么关系。
Billy ONeal
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.