常量在 C/C + + 中提供了什么样的优化?

我知道,出于可读性的考虑,在通过引用或指针传递参数时,如果可能的话,应该使用 const 关键字。如果我指定一个参数是常量,编译器是否可以进行任何优化?

可能会有一些情况:

功能参数:

经常引用:

void foo(const SomeClass& obj)

Constant Some Class 对象:

void foo(const SomeClass* pObj)

以及指向 SomClass 的常量指针:

void foo(SomeClass* const pObj)

变量声明:

const int i = 1234

函数声明:

const char* foo()

每个编译器都提供了哪种编译器优化(如果有的话) ?

27261 次浏览

SomeClass* const pObj创建指针类型的常量对象。没有安全的方法可以更改这样的对象,因此编译器可以,例如,将其缓存到只有一个内存读取的寄存器中,即使它的地址已被获取。

其他类型不会特别启用任何优化,尽管类型上的 const限定符会影响重载分辨率,并可能导致选择不同且更快的函数。

来源

案例1:

在程序中声明 const时,

int const x = 2;

编译器可以通过不为该变量提供存储空间来优化这个 const; 相反,它可以添加到符号表中。因此,后续的读操作只需要间接进入符号表,而不需要通过指令从内存中获取值。

注意: 如果你这样做:

const int x = 1;
const int* y = &x;

然后,这将强制编译器为 x分配空间。所以,在这种情况下,这种程度的优化是不可能的。

就函数参数而言,const意味着参数在函数中不会被修改。据我所知,使用 const没有实质性的性能提高; 相反,它是确保正确性的一种方法。


案例2:

“将参数和/或返回值声明为 const 是否有助于编译器生成更优的代码?”

const Y& f( const X& x )
{
// ... do something with x and find a Y object ...
return someY;
}

编译器能做得更好吗? 它能避免参数或返回值的副本吗?

不,因为参数已经通过引用传递。

它能把 x 或者 Y 的拷贝放到只读内存里吗?

不,因为 xsomeY都生活在其范围之外,来自和/或给予外部世界。即使 someY是在 f()本身内动态分配的,它及其所有权也是交给调用者的。

那么对出现在 f ()主体内部的代码可能进行的优化呢?由于 const 的存在,编译器能够以某种方式改进它为 f ()体生成的代码吗?

即使在调用 const 成员函数时,编译器也不能假定对象 x或对象 someY的位不会改变。此外,还有一些额外的问题(除非编译器执行全局优化) : 编译器可能也不确定是否有其他代码可能具有与 x和/或 someY别名相同的对象的非常量引用,以及在执行 f();时是否会附带使用任何对同一对象的非常量引用,编译器甚至可能不知道实际对象(xsomeY仅仅是引用)是否首先声明为常量。


案例3:

void f( const Z z )
{
// ...
}

这里面会有什么优化吗?

是的,因为编译器知道 z确实是一个常量对象,所以即使没有全局分析,它也可以执行一些有用的优化。例如,如果 f()的主体包含类似于 g( &z )的调用,编译器可以确保 z的非可变部分在调用 g()期间不会发生变化。

在给出任何答案之前,我想强调的是,使用或不使用 const的原因实际上应该是为了程序的正确性和其他开发人员的清晰度,而不是为了编译器的优化; 也就是说,创建一个参数 const文档,该方法不会修改该参数,并创建一个成员函数 const文档,该成员不会修改其所属的对象(至少不会在逻辑上改变任何其他常数成员函数的输出)。例如,这样做可以让开发人员避免不必要地复制对象(因为他们不必担心原始对象会被破坏或修改) ,或者避免不必要的线程同步(例如,通过知道所有线程只是读取而不会改变有问题的对象)。

至于编译器可以进行的优化,至少在理论上是这样的,尽管优化模式允许编译器做出某些非标准的假设,这些假设可能会破坏标准的 C + + 代码,考虑一下:

for (int i = 0; i < obj.length(); ++i) {
f(obj);
}

假设 length函数被标记为 const,但实际上是一个开销很大的操作(假设它实际上是在 O (n)时间而不是 O (1)时间内操作)。如果函数 f通过 const引用获取它的参数,那么编译器可能会将这个循环优化为:

int cached_length = obj.length();
for (int i = 0; i < cached_length; ++i) {
f(obj);
}

... 因为函数 f不修改参数的事实保证了在对象没有改变的情况下,length函数每次应该返回相同的值。但是,如果 f被声明为通过一个可变引用获取参数,那么在循环的每次迭代中都需要重新计算 length,因为 f可能已经修改了对象,以产生值的变化。

正如注释中指出的,这是假设一些额外的警告,只有当调用编译器在一个非标准模式,允许它作出额外的假设(例如,const方法是严格的一个函数的输入和优化可以假设,代码将永远不会使用 const_cast转换常量引用参数为可变引用)。

功能参数:

const对于引用内存来说并不重要,它就像把一只手绑在优化器的背后一样。

假设您在 foo中调用另一个没有可见定义的函数(例如 void bar())。优化器将受到限制,因为它无法知道 bar是否修改了传递给 foo的函数参数(例如通过访问全局内存)。在外部修改内存和别名的可能性给这个领域的优化器带来了很大的限制。

虽然您没有要求,但是 const 价值观 for 函数参数确实允许优化,因为优化器保证是 const对象。当然,复制该参数的成本可能远远高于优化器的好处。

见: http://www.gotw.ca/gotw/081.htm


变量声明: const int i = 1234

这取决于声明它的位置、创建它的时间和类型。这个类别在很大程度上是存在 const优化的。修改 const对象或已知常量是未定义的,因此编译器允许进行一些优化,它假设您不调用未定义行为,并引入一些保证。

const int A(10);
foo(A);
// compiler can assume A's not been modified by foo

显然,优化器还可以识别不变的变量:

for (int i(0), n(10); i < n; ++i) { // << n is not const
std::cout << i << ' ';
}

函数声明: const char* foo()

不重要。引用的内存可以在外部进行修改。如果 foo返回的引用变量是可见的,那么优化器可以进行优化,但这与函数返回类型上是否存在 const无关。

同样,const值或对象是不同的:

extern const char foo[];

对于使用常量的每个上下文,它的确切效果是不同的。如果在声明变量时使用 const,那么它在物理上就是 const,并且有效地驻留在只读内存中。

const int x = 123;

试图摆脱常规是一种不明确的行为:

即使 const _ cast 可以从任何指针或引用中移除常量或易失性,使用生成的指针或引用写入声明为 const 的对象或访问声明为易失性的对象也会调用未定义行为。Cpferences/const _ cast

因此在这种情况下,编译器可能假设 x的值总是 123。这将打开一些优化潜力(常数传播)

对于函数来说则是另一回事,假设:

void doFancyStuff(const MyObject& o);

我们的函数 doFancyStuff可以用 o做以下任何事情。

  1. 不修改对象。
  2. 抛弃常量,然后修改对象
  3. 修改 MyObject 的 mutable数据成员

请注意,如果您使用一个实例为 声明的 myObject 调用我们的函数,那么您将使用 # 2调用未定义行为。

大师问题: 下面这些人会不会引用未定义行为?

const int x = 1;
auto lam = [x]() mutable {const_cast<int&>(x) = 2;};
lam();