代码为什么会主动阻止尾调用优化?

81

这个问题的标题可能有些奇怪,但据我所知,没有任何反对尾递归优化的说法。然而,在浏览开源项目时,我已经遇到过一些旨在阻止编译器进行尾递归优化的函数,例如实现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
但这不是一个试图禁用它的理由吗? - JustSid
4
系统调用可能是阻止尾递归优化的一种可靠方式,但也是一种代价很高的方法。 - Fred Foo
39
这是一个很好的教育时刻,可以教授适当的评论技巧。 +1 部分解释了那行代码存在的原因(为了防止尾调用优化),但-100 没有解释为什么首先需要禁用尾调用优化... - Mark Sowul
16
由于 getpid() 的返回值未被使用,一个明智的优化器是否可以将其移除(因为已知 getpid() 是一个没有副作用的函数),从而允许编译器进行尾调用优化?这似乎是一种非常脆弱的机制。 - luiscubal
显示剩余12条评论
3个回答

83

我猜测这么做是为了确保在调试时,__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__在堆栈跟踪中被记录下来。它有__attribute__((no inline))的属性以支持这个想法。

如果你注意到,这个函数只是跳到另一个函数,所以它是一种类似于跳板的形式,我只能想到这样一个冗长的名称是为了帮助调试。这对于该函数调用已从其他地方注册的函数指针,因此该函数可能没有可访问的调试符号尤其有帮助。

还要注意其他类似命名的函数,它们都做着类似的事情-看起来真的是为了帮助从回溯中查看发生了什么。请记住,这是核心Mac OS X代码,并将出现在崩溃报告和进程采样报告中。


是的,这与__attribute__((noinline))一致。我认为你说得很对。 - Niklas B.
是的,确实有道理。但是如果你看一下这些函数被调用的位置,你会发现它们总是只从一个函数中调用,例如我的示例函数只从__CFRunLoopDoObservers中调用,这在堆栈跟踪中绝对会显示出来... - JustSid
1
当然,但我猜这是另一个标记,用于确定观察者回调/块等确切的运行位置。 - mattjgalloway
2
我认为这是最好的答案。+1 - R.. GitHub STOP HELPING ICE
@R.. 我只能接受一个答案,而且Andrew White也提到了其他不希望使用尾调用优化的情况。请记住,我并没有问为什么函数这样做,而是为什么一般情况下可能不希望这样做,并给出了函数作为现实世界的例子。 - JustSid

34

这只是一个猜测,但可能是为了避免无限循环而不是因栈溢出错误而崩溃。

由于所讨论的方法没有在堆栈上放置任何东西,因此似乎可能进行尾调用递归优化以生成进入无限循环的代码,而非未经优化的代码会将返回地址放在堆栈上,在误用时最终会溢出。

我唯一的另一个想法与保留调用堆栈以进行调试和堆栈跟踪打印有关。


8
我认为堆栈跟踪/调试解释更可能是正确的(我正要发布它)。无限循环不比崩溃更糟糕,因为用户可以强制结束应用程序。这也可以解释为什么会出现 noinline。 - ughoavgfhw
3
可能吧,但是当你涉及到线程等内容时,无限循环真的很难追踪。我一直认为滥用应该引发异常。既然我从来没有做过这个,那么这只是一个猜测。 - Andrew White
1
同步性,有点像……我刚遇到了一个糟糕的错误,导致应用程序不断打开新窗口。这让我想到,如果应用程序在尝试饱和“堆”(我的内存)并使X崩溃之前就崩溃了,我就不需要切换到终端来突然杀死疯狂的应用程序(因为X很快就会变得无响应)。所以也许,选择“快速失败”的方法可能更好,这可能会导致堆栈溢出而没有优化……?或者也许只是另一回事……! - ShinTakezou
2
@AndrewWhite 嗯,我非常喜欢无限循环 - 我想不出有什么比这更容易调试的了。我的意思是,你只需附加调试器,就可以获得问题的确切位置和状态,而不需要猜测。但是,如果你想从用户那里获取堆栈跟踪,我同意无限循环会带来问题,所以这似乎是合理的 - 错误将出现在日志中,而无限循环则不会。 - Voo
保留调用堆栈是我在实践中使用它的唯一原因。使用尾递归优化(TCO)时,如果int A()调用B()调用C(),并且B->C()是一个尾调用,那么如果C()内部发生了错误,调用堆栈将显示A直接调用了C,而没有经过B。这可能会令人困惑。 - Crashworks
1
这假设函数本来就是递归的 - 但它不是; 直接或(通过查看函数来自的上下文)间接地都不是。我最初也犯了同样的错误假设。 - Konrad Rudolph

21

其中一个可能的原因是为了使调试和分析更容易(使用尾递归优化,父级堆栈帧消失,这使得堆栈跟踪更难以理解)。


2
让程序变慢以方便分析性能有点奇怪。这就好比在测量汽车行驶距离之前稀释油一样没有意义 :x - Matthieu M.
1
@MatthieuM.:如果在循环中执行了数百万次,则添加的调用就没有意义,但是如果每秒执行几百次或更少,则最好将其保留在真实系统中,并能够检查真实系统的行为,而不是将其删除并冒着这种删除会对系统行为产生微妙但重要变化的风险。 - supercat
@MatthieuM。如果稀释您的油是任何测量的先决条件,那么这实际上是非常合理的。 - Dmitry Grigoryev
@DmitryGrigoryev:不,它并不会。没有任何措施是令人讨厌的,但错误的措施从无用到危险都有可能(取决于您对其的信任程度)。继续使用油类比:如果它减慢了您的速度,那么您可能会得到一些指示重量比空气动力学更重要的措施,因此去除重量并恶化空气动力学以优化您所测量的内容... 但是在实际的油中,当您加速时,事实证明空气动力学更为重要,而您的“改进”比不做任何事情还要糟糕! - Matthieu M.
@MatthieuM。你熟悉不确定性原理吗?任何测量都会有一定的误差,因为没有办法在不与被测物体相互作用的情况下进行测量。因此,即使您不更换示例中的油,仪器化汽车也会改变空气动力学。 - Dmitry Grigoryev
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接