const 在 C/C++ 中提供啥样的优化?
Posted
技术标签:
【中文标题】const 在 C/C++ 中提供啥样的优化?【英文标题】:What kind of optimization does const offer in C/C++?const 在 C/C++ 中提供什么样的优化? 【发布时间】:2015-02-12 12:31:22 【问题描述】:我知道,出于可读性原因,您应该在通过引用或指针传递参数时尽可能使用 const 关键字。如果我指定一个参数是常量,编译器是否可以做任何优化?
可能有几种情况:
功能参数:
常量引用:
void foo(const SomeClass& obj)
常量 SomeClass 对象:
void foo(const SomeClass* pObj)
还有指向 SomeClass 的常量指针:
void foo(SomeClass* const pObj)
变量声明:
const int i = 1234
函数声明:
const char* foo()
每个都提供什么样的编译器优化(如果有的话)?
【问题讨论】:
const
在指针或引用的目标上并不意味着常量。这意味着这是一个对象的只读视图。通过其他方式访问同一对象可能会改变它。只有对象定义上的 const
实际上使它不可变。
使用const
的主要目的不是帮助编译器进行优化,而是保护自己免受错误的影响。实际上,我怀疑任何编译器都依赖const
-qualification 来优化代码。
@BenVoigt 我知道它不会使对象保持不变,而只会使其在此函数的范围内显得不变。也许我没有使用正确的术语来表达我的意思。
简短的回答是const
对优化没有影响;这是为了帮助在编译时捕获错误。
C 和 C++ 是两种具有共同传统的不同语言。虽然关于不存在的混合“C/C++”的 cmets 可能会提供一些见解,但专注于其中一个会更有意义。
【参考方案1】:
Source
案例 - 1:
当你在你的程序中声明一个常量时,
int const x = 2;
编译器可以优化掉这个常量,不为这个变量提供存储空间,而是将它添加到符号表中。因此,后续读取只需要间接到符号表中,而不是从内存中获取值的指令。
注意:如果您执行以下操作:
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 或 someY 的副本放入只读内存吗?
不,因为x
和someY
都存在于其范围之外,并且来自和/或被提供给外部世界。即使someY
是在f()
自身内部动态分配的,它和它的所有权都交给调用者。
对于出现在 f() 主体中的代码可能进行哪些优化?由于 const,编译器能否以某种方式改进它为 f() 的主体生成的代码?
即使调用 const 成员函数,编译器也不能假定对象 x
或对象 someY
的位不会改变。此外,还有其他问题(除非编译器执行全局优化):编译器也可能不确定没有其他代码可能有一个非常量引用,该引用将同一对象作为x
和/或someY
的别名,以及在执行f();
期间是否会偶然使用对同一对象的任何此类非常量引用,编译器甚至可能不知道x
和someY
只是引用的真实对象是否实际上是首先声明为 const。
案例 - 3:
void f( const Z z )
// ...
这里面会有优化吗?
是的,因为编译器知道 z
确实是一个 const 对象,即使没有全局分析,它也可以执行一些有用的优化。例如,如果f()
的主体包含像g( &z )
这样的调用,编译器可以确保z
的非可变部分在调用g()
期间不会改变。
【讨论】:
优化还是可以的。语言规则禁止更改x
,因此它的值1
仍然可以在任何需要值 的地方替换。你是对的,需要有内存来获取地址,但是可以跳过通过名称 x
访问该内存。
@BenVoigt:只有当编译器能够证明每次引用指针时确实是对内存地址的访问时,才能跳过对该内存地址的访问。例如,考虑foo(&y); bar(*y);
,其中foo
在另一个翻译单元中定义。由于y
本身不是const
,因此编译器无法知道foo
是否改变了y
,因此它无法优化掉*y
,因为在那个地方它无法分辨y
指向的位置。但是由于y
可能仍然指向x
,所以x
的内存地址必须存在并且包含值1
。
因抄袭 Sutter 而未注明出处而被否决:gotw.ca/gotw/081.htm
@ravi 为了不抄袭,你真的不需要政策。只是不要把别人的工作归功于他人,不仅在 *** 上,而且在任何地方。不好看。
@all_blaming_plagiarism 我相信这篇文章完全在 Herb 的 Sutter 书中。所以,这意味着将他的知识转移到世界上。我想我在这里也做同样的事情,除了不包括信息来源。无论如何,下次会处理归因。【参考方案2】:
在给出任何答案之前,我想强调一下,使用或不使用const
的原因实际上应该是为了程序的正确性和其他开发人员的清晰度,而不是编译器优化;也就是说,创建一个参数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
可能已经修改了对象以产生变化价值。
正如 cmets 中所指出的,这是假设了一些额外的警告,并且只有在允许它做出额外假设的非标准模式下调用编译器时才有可能(例如 const
方法严格它们输入的函数,并且优化可以假设代码永远不会使用const_cast
将 const 引用参数转换为可变引用)。
【讨论】:
这取决于obj
的动态类型是否已知为const
,f
是否通过复制或引用获取其参数,以及f
的主体是否可见.它不依赖于f
带引用参数是否为const
-qualified。
您的新编辑(说const
参考而不是“const 参数”)更加清晰。现在明显错了。您提到的转换只有在 obj
被创建 const
时才有可能,或者编译器可以看到 length()
成员函数的内部。
@MichaelAaronSafyan:const
函数仍然可以在每次调用时返回不同的结果。这种优化实际上要求函数被标记为幂等的,或者编译器可以通过检查函数体来推断出这一点。参数类型没有帮助。
@MichaelAaronSafyan “不过,真的,在这种情况下它不应该被标记为 const” -- 为什么不呢? const
在方法上传达的唯一含义是,在调用它时它不会改变调用它的对象。它不意味着每次调用它时返回值都相同,即使对象没有改变。方法上的const
承诺不更改对象;它并不意味着幂等性。
@MichaelAaronSafyan 最终,它与我们认为const
对成员函数的暗示以及我们认为应该使用它的时间无关。标准就是权威,as-if 规则支配着所有的优化。如果编译器可以证明const
方法是幂等的,那么它可以在未修改对象的代码块中省略调用。但是如果它不能证明这一点并且它仍然进行了优化,那么 as-if 规则就被打破了,它并不是一个真正的 C++ 编译器,只是一个可以解析 C++ 代码但解释不正确的编译器。【参考方案3】:
功能参数:
const
对于引用的内存并不重要。这就像把一只手绑在优化器的背后。
假设您在 foo
中调用另一个没有可见定义的函数(例如 void bar()
)。优化器会有一个限制,因为它无法知道bar
是否修改了传递给foo
的函数参数(例如,通过访问全局内存)。外部修改内存的可能性和别名在这方面给优化器带来了很大的限制。
虽然您没有问,但函数参数的const
值 确实允许优化,因为优化器保证了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[];
【讨论】:
【参考方案4】:SomeClass* const pObj
创建一个指针类型的常量对象。不存在更改此类对象的安全方法,因此编译器可以例如将其缓存到仅读取一次内存的寄存器中,即使其地址已被占用。
尽管类型上的const
限定符会影响重载分辨率,并可能导致选择不同且更快的函数,但其他的并不专门启用任何优化。
【讨论】:
您能否详细说明过载部分?也许是一个例子? @UnTraDe:我正在尝试考虑标准库中的一个案例,其中const
版本的函数做了一些完全不同的事情,但我做不到。然而,用户代码和其他库可能会这样做。
@BenVoigt,我记得过去 C++ 标准库的实现者尝试使用引用计数 std::string
s。打电话,例如。 g.,非常量字符串上的begin()
导致其分离;也就是说,如果std::string
对象与另一个std::string
对象共享该字符串,则此时将其复制并标记为不可引用计数。在 const 字符串上调用 begin()
不会改变其内部状态。
@Andrey:是的,我所说的通过重载选择进行改进的类型。但 std::string
这样做并不符合标准,而且我不知道标准中的任何其他类有这种行为。【参考方案5】:
const 的确切效果因使用它的每个上下文而异。如果在声明变量时使用 const,它实际上是 const 并且可能驻留在只读内存中。
const int x = 123;
试图摆脱 const 是未定义的行为:
即使 const_cast 可以从任何指针或引用中删除 const 或易失性,使用生成的指针或引用来写入声明为 const 的对象或访问声明为 volatile 的对象会调用未定义的行为。 cppreference/const_cast
所以在这种情况下,编译器可能会假设x
的值始终是123
。这打开了一些优化潜力(常量传播)
对于函数,这是另一回事。假设:
void doFancyStuff(const MyObject& o);
我们的函数doFancyStuff
可以用o
做以下任何事情。
-
不修改对象。
去掉常量,然后修改对象
修改 MyObject 的
mutable
数据成员
请注意,如果您使用被声明为 const 的 MyObject 实例调用我们的函数,您将使用 #2 调用未定义的行为。
大师问题:以下是否会调用未定义的行为?
const int x = 1;
auto lam = [x]() mutable const_cast<int&>(x) = 2;;
lam();
【讨论】:
你可能是对的,修改 const 对象的可变数据成员定义了行为。我会编辑那部分。但我认为你把我的大师问题弄错了。按值 lambda 捕获保留了捕获的常量性。所以在 lambda 内部我们有一个变量x
,它被声明为const int
。修改那个是未定义的行为。
@Deduplicator 我希望是这样,但事实并非如此。 16:40 见 youtube.com/watch?v=48kP_Ssg2eY。
小心,未明确标记为 mutable
的 lambda 的 const
语义已更改。以上是关于const 在 C/C++ 中提供啥样的优化?的主要内容,如果未能解决你的问题,请参考以下文章