为什么代码会积极尝试阻止尾调用优化?


80

问题的标题可能有点奇怪,但据我所知,根本没有什么可以反对尾部调用优化。但是,在浏览开源项目时,我已经遇到了一些主动尝试阻止编译器进行尾部调用优化的功能,例如CFRunLoopRef的实现,其中充斥着此类黑客。例如:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (func) {
        func(observer, activity, info);
    }
    getpid(); // thwart tail-call optimization
}

我很想知道为什么这看起来如此重要,并且在任何情况下我作为普通开发人员都应该牢记这一点吗?例如。尾部调用优化存在常见的陷阱吗?


10
一个可能的陷阱可能是应用程序可以在多个平台上顺利运行,然后在使用不支持尾部调用优化的编译器进行编译时突然停止工作。请记住,这种优化实际上不仅可以提高性能,而且还可以防止运行时错误(堆栈溢出)。
Niklas B.

5
@NiklasB。但这不是尝试禁用它的原因吗?
JustSid 2012年

4
系统调用可能是提高TCO的肯定方法,但也是相当昂贵的方法。
弗雷德·富

39
这是进行适当评论的绝佳时机。+1代表部分解释为什么存在那条线(以防止尾部呼叫优化),-100代表未解释为什么首先需要禁用尾部呼叫优化...
Mark Sowul,2012年

16
由于getpid()未使用的值,因此不能由有见识的优化器删除它(因为getpid该函数没有副作用),因此允许编译器进行尾部调用优化吗?这似乎是一个非常脆弱的机制。
luiscubal 2012年

Answers:


82

我的猜测是,要确保该消息__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__位于堆栈跟踪中以进行调试。它__attribute__((no inline))支持了这个想法。

如果您注意到了,该函数无论如何都会跳到另一个函数,因此它是蹦床的一种形式,我只能认为它的名称如此冗长,以帮助调试。鉴于该函数正在调用已从其他位置注册的函数指针,因此该函数可能无法访问调试符号,这将特别有用。

还请注意其他具有类似功能的名称相似的函数-确实看起来像是在帮助查看回溯发生了什么。请记住,这是Mac OS X的核心代码,它也将显示在崩溃报告和处理示例报告中。


是的,与保持一致__attribute__((noinline))。我想你在这里。
Niklas B.

是的,确实有道理。但是,如果您查看从何处调用这些函数,就会发现它们始终仅是从一个函数中调用的,例如,我的示例函数仅是从__CFRunLoopDoObservers其中调用的,因此肯定会显示在堆栈跟踪中……
JustSid 2012年

1
可以,但是我想这是观察者回调/阻止/运行的确切位置的另一个标记。
mattjgalloway 2012年

2
我认为这是最好的答案。+1
R .. GitHub停止帮助ICE

@R ..虽然我只能接受一个答案,但安德鲁·怀特(Andrew White)还列举了其他可能不需要尾部调用优化的情况。记住,我不是在问函数为什么要这么做,而是为什么通常可能并不需要它,并以实际示例为例。
JustSid 2012年

34

这只是一个猜测,但也许是为了避免无限循环与使用堆栈溢出错误进行轰炸。

由于所讨论的方法没有在堆栈上放置任何内容,因此尾调用递归优化似乎有可能产生进入无限循环的代码,而不是将返回地址放入堆栈的未优化的代码如果滥用,最终会溢出。

我唯一想到的是与将调用保留在堆栈上以进行调试和堆栈跟踪打印有关。


8
我认为stacktrace / debugging解释的可能性更大(我正要发布它)。无限循环并不比崩溃更糟糕,因为用户可以强制应用程序退出。这也将解释noinline。
ughoavgfhw 2012年

3
@ughoavgfhw:也许吧,但是当您进入线程之类的时候,无限循环真的很难追踪。我一直都以为滥用会引发异常。既然我从来没有这样做,那仍然只是一个猜测。
安德鲁·怀特

1
同步性……我刚刚遇到了一个严重的错误,该错误使应用程序打开了新窗口。这让我想到,如果应用程序在试图使“堆”(我的内存)饱和并阻塞X之前崩溃了,我将无需切换到终端以突然杀死该疯狂的应用程序(因为X很快就开始成为无反应)。因此,也许这是一个更喜欢“失败快速”方法的原因,这种方法可能会伴随堆栈溢出而没有优化……?或者,也许这是另一回事……!
ShinTakezou 2012年

2
@AndrewWhite Hmm我完全喜欢无限循环-我想不出一件更容易调试的东西,我的意思是,您可以只附加调试器并获得问题的确切位置和状态,而无需任何猜测。但是,如果您想从用户那里获取堆栈跟踪信息,则我同意无限循环是有问题的,因此这似乎合乎逻辑-错误将出现在您的日志中,而无限循环则不会。
Voo

1
假定函数首先是递归的,但是不是。既不直接也不间接(通过查看函数的来源上下文)。最初我做同样的错误假设。
康拉德·鲁道夫2012年

21

一个潜在的原因是使调试和配置更加容易(使用TCO,父堆栈框架消失了,这使得堆栈跟踪更难以理解。)


2
但是,以降低程序速度为代价,简化配置过程有点奇怪。在测量您的汽车可以行驶多远之前,先稀释您的机油是很有意义的:x
Matthieu M.

1
@MatthieuM .:如果添加的调用在一个循环中执行了数百万次,那么这种事情就没有意义,但是如果每秒执行数百次或更短的次数,最好将其保留在实际系统中,能够检查实际系统的行为,而不是将其取出,否则可能会被删除而造成系统行为的微妙但重要的变化。
supercat 2015年

@MatthieuM。如果稀释油是进行任何测量的前提,那么这实际上是很合理的。
德米特里·格里戈里耶夫

@DmitryGrigoryev:不,不是。没有任何措施会令人讨厌,但是错误的措施会从无用到危险(取决于您对它的信任程度)。继续用油类比:如果它减慢了您的速度,那么您可能会得到一些表明重量比空气动力学更重要的指标,从而减轻了重量并恶化了空气动力学,从而针对您所测量的结果进行了优化……但是对于真油,当您走得更快时,事实证明,空气动力学更重要,而您的“改进”比什么都不做更糟糕!
Matthieu M.

@MatthieuM。您熟悉不确定性原理吗?任何测量在某种程度上都是错误的,因为如果不与被测对象进行交互,就无法进行任何测量。因此,即使您不更换示例中的机油,对汽车进行仪表安装也将改变空气动力学。
德米特里·格里戈里耶夫
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.