现代 C++ 编译器是不是能够避免在某些情况下两次调用 const 函数?
Posted
技术标签:
【中文标题】现代 C++ 编译器是不是能够避免在某些情况下两次调用 const 函数?【英文标题】:Are modern C++ compilers able to avoid calling a const function twice under some conditions?现代 C++ 编译器是否能够避免在某些情况下两次调用 const 函数? 【发布时间】:2017-07-02 19:47:16 【问题描述】:例如,如果我有这个代码:
class SomeDataProcessor
public:
bool calc(const SomeData & d1, const SomeData & d2) const;
private:
//Some non-mutable, non-static member variables
SomeDataProcessor sdp;
SomeData data1;
SomeData data2;
someObscureFunction(sdp.calc(data1, data2),
sdp.calc(data1, data2));
让我们考虑可能等效的代码:
bool b = sdp.calc(data1, data2);
someObscureFunction(b,b);
要使其有效,calc()
函数应该满足一些要求,例如我将属性称为_pure_const_formula_
_pure_const_formula_
会:
_pure_const_formula_
函数
可能还有一些我没有想到的其他条件
例如,调用随机数生成器不符合这些要求。
是否允许编译器用第二个代码替换第一个代码,即使它需要递归地挖掘被调用的函数?现代编译器能做到这一点吗?
【问题讨论】:
你可以在这里找到更多信息en.wikipedia.org/wiki/Aliasing_(computing) 是的,抱歉,我从更复杂的代码示例中留下了一些代码... 我们是否假设calc
的定义对编译器是可见的,就像整个程序优化(也称为链接时代码生成)可用时一样?这在这里有很大的不同;它完全改变了答案。
如果编译器可以通过内联声明或 LTO 看到实现,那么是的,它可以删除重复调用。否则你需要使用编译器特定的技巧来做到这一点。
@ZanLynx 和 CodyGray:+10 亿。 LTO(或内联定义)使这样的问题变得轻而易举:答案是绝对是:任何优化都是可能的,因为函数的主体可以成为任何调用站点的内联候选,进行所有可能的优化。
【参考方案1】:
不,鉴于显示的代码,编译器不能保证建议的优化不会有明显的差异,并且现代编译器将无法优化第二个函数调用。
一个非常简单的例子:这个类方法可能使用一个随机数生成器,并将结果保存在某个私有缓冲区中,以便稍后代码的其他部分读取。显然,现在消除函数调用会导致放置在缓冲区中的随机生成的值更少。
换句话说,仅仅因为一个类方法是const
并不意味着它在被调用时没有可观察到的副作用。
【讨论】:
Rand() 会改变一个全局变量,OP 会询问编译器是否足够聪明,可以检查全局变量的变化。 是的,我想到了 RNG 的情况,但这至少需要一个静态或全局变量,所以如果编译器可以检查 calc() 中调用的函数来检查它们是否改变了全局状态,编译器能做那种优化吗? OP 已经说过会有限制。你只是重复了它们。如果你想把问题中的代码当作福音,从字面上看,逐字逐句,那么你必须扔掉一半的问题散文并报告程序甚至无法编译,更不用说链接,更不用说优化了。no modern compiler
您是否故意使用编译器 的限制性定义而不包括链接时优化?因为所有主要的现代编译器工具链都支持 LTO,这意味着函数的定义可以考虑在每个调用站点进行优化。 (当然,如果函数在头文件中声明,即使那些不能优化的人也可以看到它没有其他副作用。)
现代编译器(例如 clang)通常可以判断出建议的优化没有明显的差异,并且实际上优化了第二个函数调用。当然,任何在非法的地方进行优化的编译器都会被破坏,但是编译器没有理由不能在合法的地方进行优化。 (有很多人认为编译器永远不够聪明,无法进行特定的优化,然后就被严重烧毁了。)【参考方案2】:
不,在这种情况下不允许编译器这样做。 const
仅表示您不更改方法所属对象的状态。但是,使用相同的输入参数多次调用此方法可能会产生不同的结果。例如,考虑一种产生随机结果的方法。
【讨论】:
“使用相同的输入参数多次调用此方法可能会得到不同的结果” 是的,但编译器通常可以看到情况并非如此。 @LightnessRacesinOrbit 是的,但是在大多数情况下,编译器甚至不需要 const 关键字。 当然,mutable
字段也会把事情搞砸
@MarkKCowan:别忘了volatile
!
在一个巨魔注释中,如果编译器记住 random()
哪个 returns a randomly generated number
,则在执行期间为每次调用提供相同(但在技术上仍然是“随机生成”)结果,但值不同对于每次执行,程序是否仍然满足其规范? XKCD 不久前做了一些类似的事情......【参考方案3】:
GCC 有 pure
attribute(用作__attribute__((pure))
)用于告诉编译器可以消除冗余调用的函数。它用于例如在strlen
。
我不知道有任何编译器会自动执行此操作,特别是考虑到要调用的函数可能无法以源代码形式提供,并且目标文件格式不包含有关函数是否为纯函数的元数据。
【讨论】:
啊哈,感谢这个非常有用的答案。我的脑子里正好有这种想法,即有点昂贵的 .depth() 树迭代器成员函数。 请记住,即使使用pure
属性,gcc 也只会在可以通过静态分析确定它们确实是使用相同参数调用的情况下忽略冗余调用。如果编译器无法做到这一点,您仍然可以通过记忆化消除冗余调用,但 C/C++ 编译器不会自动为您做到这一点。
纯函数确定是一种极其常见的编译器优化,在大多数语言中。在 C 语言中这很困难(主要是由于指针),但即使是 GCC 也会尝试使用编译器标志 -Wsuggest-attribute=pure
来实现(当它认为可以标记函数 pure
时会发出警告)
@nwp 您正在修改字符串,因此它不是同一个参数。指针是一样的,只是它指向的数据被修改了。
@nwp:我认为它的目的是解释为“仅取决于其参数或可通过其参数访问的任何内存(可传递)”。否则它甚至根本无法计算字符串长度。【参考方案4】:
是的,绝对的。
Compilers do this all the time, and more.
例如,如果您的所有函数都返回true
,并且它的定义在调用点对编译器可见,那么整个函数调用可能会被省略,从而导致:
someObscureFunction(true, true);
编译器有足够信息的程序可以从相当复杂的任务链“优化”到可能是一两条指令。现在,实际上对成员变量进行操作在某种程度上将优化器推到了极限,但是如果变量是private
,被赋予一个已知的初始值,并且没有被任何其他成员函数改变,我不明白为什么如果编译器愿意,它不能只内联其已知值。编译器非常非常聪明。
人们认为编译后的程序是源代码中行的一对一映射,但事实并非如此。 C++ 的全部目的是,它是您的计算机在运行程序时实际要做的事情的抽象。
【讨论】:
@cmaster:再次阅读答案。 “如果你的所有函数都返回 true,并且它的定义在调用点对编译器是可见的,那么整个函数调用可能会被忽略”我不是“假设“任何事情! @cmaster 除了 Lightness 的其他观察之外,链接时优化甚至可以跨对象边界内联函数。 @cmaster MSVC、Clang 和 GCC 多年来都支持 LTO,因此从优化的角度来看,“定义内联”之类的概念正在迅速过时。 @KonradRudolph 可以说 LTO 意味着“编译器”在调用站点的链接时检查定义。【参考方案5】:是的,现代 C 编译器可以省略冗余函数调用当且仅当他们可以证明这种优化的行为就像遵循原始程序语义。例如,这意味着它们可以消除对具有相同参数的同一函数的多次调用,如果该函数没有副作用并且其返回值仅取决于参数。
现在,您专门询问了const
- 这对开发人员最有用,而不是编码人员。 const
函数是一个提示,表明该方法不会修改调用它的对象,const
参数是提示,表明参数没有被修改。但是,该函数可能(合法地1)抛弃this
指针或其参数的const
-ness。所以编译器不能依赖它。
此外,即使传递给函数的 const
对象确实从未在该函数中被修改,并且 const
函数从未修改接收器对象,该方法也可以轻松依赖可变的全局数据(并且可以改变此类数据) .例如,考虑一个返回当前时间或递增全局计数器的函数。
所以const
声明帮助程序员,而不是编译器2。
但是,编译器可能能够使用其他技巧来证明调用是多余的:
该函数可能与调用者位于同一编译单元中,从而允许编译器检查它并准确确定它所依赖的内容。其最终形式是内联:函数体可以移动到调用者中,此时优化器可以从以后的调用中删除冗余代码(最多包括完全来自这些调用的所有代码,也可能包括原始调用的全部或端口也)。 工具链可能会使用某种类型的链接时间优化,即使对于不同编译单元中的函数和调用者,它也可以有效地实现上述类型的分析。这可以允许对生成最终可执行文件时出现的任何代码进行优化。 编译器可以允许用户使用一个属性来注释一个函数,该属性通知编译器它可以将该函数视为没有副作用。例如,gcc 提供了pure
和const
函数属性,它们通知gcc
这些函数没有副作用,并且仅依赖于它们的参数(以及全局变量,在pure
的情况下)。
1 通常,只要对象最初没有被定义为const
。
2 const
定义在某种意义上确实对编译器有帮助:它们可以将定义为 const
的全局对象放入可执行文件的只读部分(如果存在这样的特性),并且在它们相等时组合这些对象(例如,相同的字符串常量)。
【讨论】:
以上是关于现代 C++ 编译器是不是能够避免在某些情况下两次调用 const 函数?的主要内容,如果未能解决你的问题,请参考以下文章