为啥代码会主动尝试阻止尾调用优化?
Posted
技术标签:
【中文标题】为啥代码会主动尝试阻止尾调用优化?【英文标题】:Why would code actively try to prevent tail-call optimization?为什么代码会主动尝试阻止尾调用优化? 【发布时间】:2012-06-03 04:19:27 【问题描述】:问题的标题可能有点奇怪,但问题是,据我所知,根本没有任何内容反对尾调用优化。但是,在浏览开源项目时,我已经遇到了一些积极尝试阻止编译器进行尾调用优化的函数,例如 CFRunLoopRef 的实现,它充满了这样的hacks .例如:
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
我很想知道为什么这看起来如此重要,有没有我作为一个普通开发人员也应该记住这一点的情况?例如。尾调用优化有哪些常见的陷阱?
【问题讨论】:
一个可能的陷阱可能是应用程序在多个平台上运行顺畅,然后在使用不支持尾调用优化的编译器编译时突然停止工作。请记住,这种优化实际上不仅可以提高性能,还可以防止运行时错误(堆栈溢出)。 @NiklasB。但这难道不是不尝试禁用它的理由吗? 系统调用可能是降低 TCO 的可靠方法,但也是一种相当昂贵的方法。 这是一个很好的教学时刻,可以进行适当的评论。 +1 用于部分解释为什么该行存在(以防止尾调用优化),-100 用于不解释为什么需要首先禁用尾调用优化... 由于getpid()
的值没有被使用,它不能被知情的优化器删除(因为 getpid
是一个已知没有副作用的函数),因此允许编译器无论如何都要进行尾调用优化?这似乎是一个真的脆弱的机制。
【参考方案1】:
我的猜测是,这是为了确保__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
在堆栈跟踪中以进行调试。它有 __attribute__((no inline))
支持这个想法。
如果您注意到,该函数无论如何都会跳转到另一个函数,所以它是一种蹦床形式,我只能认为它具有如此冗长的名称来帮助调试。这将特别有用,因为该函数正在调用已从其他地方注册的函数指针,因此该函数可能无法访问调试符号。
还要注意其他类似名称的函数,它们执行类似的操作 - 它看起来确实可以帮助查看回溯中发生的情况。请记住,这是核心 Mac OS X 代码,也会显示在崩溃报告和处理示例报告中。
【讨论】:
是的,这与__attribute__((noinline))
一致。我想你在这里。
是的,确实有道理。但是如果你看看这些函数是从哪里调用的,你会发现它们总是只从一个函数调用,例如我的示例函数只从 __CFRunLoopDoObservers
调用,这肯定会显示在堆栈跟踪中......
当然,但我想这是另一个标记正是观察者回调/块/等正在运行的地方。
我认为这是最好的答案。 +1
@R.. 我只能接受一个答案,而且 Andrew White 还提到了其他可能不需要尾调用优化的情况。请记住,我并没有问为什么这个函数会这样做,而是为什么一般来说可能不需要它,并把这个函数作为现实世界的例子。【参考方案2】:
这只是一个猜测,但可能是为了避免无限循环与堆栈溢出错误的轰炸。
由于所讨论的方法没有将任何内容放入堆栈,因此尾调用递归优化似乎有可能生成进入无限循环的代码,而不是放置返回地址的非优化代码在堆栈上,如果滥用,最终会溢出。
我唯一的其他想法与保留堆栈上的调用以进行调试和堆栈跟踪打印有关。
【讨论】:
我认为堆栈跟踪/调试解释更有可能(我正要发布它)。无限循环并不比崩溃更糟糕,因为用户可以强制应用程序退出。这也可以解释 noinline。 @ughoavgfhw:也许吧,但是当你进入线程之类的时候,无限循环真的很难追踪。我一直认为滥用应该触发异常。由于我从来没有这样做过,所以这仍然只是一个猜测。 同步性,有点...我刚刚遇到了一个错误,导致应用程序不断打开新窗口。这让我想,如果应用程序在尝试使“堆”(我的内存)饱和并阻塞 X 之前崩溃,我就不需要切换到终端来突然杀死这个疯狂的应用程序(因为 X 开始很快成为无反应)。因此,也许,有理由更喜欢可能伴随堆栈溢出且没有优化的“快速失败”方法......?或者,也许这只是另一回事……! @AndrewWhite 嗯,我非常喜欢无限循环——我想不出一个更容易调试的东西,我的意思是你可以附加你的调试器并获得问题的确切位置和状态,而无需任何猜测。但是,如果您想从用户那里获取堆栈跟踪,我同意无限循环是有问题的,所以这似乎是合乎逻辑的 - 您的日志中会出现错误,而无限循环不会。 这假设函数首先是递归的——但事实并非如此;既不直接也不(通过查看函数来自的上下文)间接。我一开始也犯了同样的错误假设。【参考方案3】:一个潜在的原因是使调试和分析更容易(使用 TCO,父堆栈帧消失,这使得堆栈跟踪更难理解。)
【讨论】:
以减慢程序速度为代价让分析变得更容易有点奇怪。这就像在测量你的车能走多远之前稀释你的油一样有意义:x @MatthieuM.:如果添加的调用在循环中执行数百万次,这样的事情就没有意义,但如果它每秒执行几百次或更少,它可能会更好将其留在真实系统中并能够检查真实系统的行为方式,而不是将其取出并冒险让这种移除对系统行为产生微妙但重要的变化。 @MatthieuM。如果稀释你的油是任何测量的先决条件,那么它实际上是非常有意义的。 @DmitryGrigoryev:不,它没有。没有措施是烦人的,但一个错误的措施是从无用到危险(取决于你对它的信任程度)。继续用油的类比:如果它让你慢下来,那么你可能会得到表明重量比空气动力学更重要的测量结果,从而消除重量并恶化空气动力学以优化你所测量的......但是使用真正的油,当你跑得更快时,事实证明空气动力学更重要,你的“改进”比什么都不做更糟糕! @MatthieuM。你熟悉不确定性原理吗?任何测量在某种程度上都是错误的,因为如果不与被测量的对象交互,就无法测量任何东西。因此,即使您在示例中不改变机油,对汽车进行检测也将改变空气动力学。以上是关于为啥代码会主动尝试阻止尾调用优化?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我对 web.api 的请求被长时间运行的控制器代码阻止?
PHP:为啥用括号括起来的函数调用会阻止“通过引用”通知? [复制]