Linux内核中的possible()和appliant()宏如何工作,它们的好处是什么?

时间:2020-03-06 14:29:43  来源:igfitidea点击:

我一直在研究Linux内核的某些部分,并找到了如下调用:

if (unlikely(fd < 0))
{
    /* Do something */
}

或者

if (likely(!err))
{
    /* Do something */
}

我找到了它们的定义:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)

我知道它们是为了优化,但是它们如何工作?使用它们会导致多少性能/尺寸下降?至少在瓶颈代码中(当然在用户空间中)值得麻烦(并且可能会失去可移植性)。

解决方案

它们是对编译器的提示,以在分支上生成提示前缀。在x86 / x64上,它们占用一个字节,因此每个分支最多最多增加一个字节。至于性能,它完全取决于应用程序-如今,在大多数情况下,处理器上的分支预测器将忽略它们。

编辑:忘记了他们实际上可以真正提供帮助的地方。它可以允许编译器对控制流图进行重新排序,以减少为"可能"路径采取的分支数量。在检查多个退出案例的循环中,这可以显着改善。

这些是GCC函数,供程序员使用,以向编译器提示有关给定表达式中最有可能发生分支条件的情况。这使编译器可以构建分支指令,以便最常见的情况是执行最少数量的指令。

分支指令的构建方式取决于处理器体系结构。

它们提示编译器发出指令,这些指令将导致分支预测偏向跳转指令的"可能"一侧。如果预测正确,这将是一个巨大的胜利,这意味着跳转指令基本上是免费的,将花费零个周期。另一方面,如果预测错误,则意味着需要清除处理器管道,这可能会花费多个周期。只要预测在大多数时间都是正确的,这将对性能有好处。

像所有这样的性能优化一样,我们应该仅在进行广泛的性能分析后才能执行此操作,以确保代码确实处于瓶颈,并且可能考虑到微观性质,因此代码正在紧密循环中运行。通常,Linux开发人员非常有经验,所以我想他们会做到的。他们并不十分在乎可移植性,因为它们只针对gcc,并且对要生成的程序集有着非常密切的了解。

它们使编译器在硬件支持它们的地方发出适当的分支提示。这通常仅意味着在指令操作码中旋转几位,因此代码大小不会改变。 CPU将开始从预测的位置获取指令,并刷新管道并在到达分支时发现错误的情况下重新开始;否则,CPU将重新启动管道。在提示正确的情况下,这将使分支的运行速度更快,具体取决于硬件的运行速度;以及这对代码性能的影响程度将取决于时间提示的正确比例。

例如,在PowerPC CPU上,无提示的分支可能需要16个周期,正确提示的分支为8,而错误提示的分支为24. 在最内层的循环中,良好的提示可能会产生巨大的影响。

可移植性并不是一个真正的问题,大概是每个平台的头文件中都有定义。对于不支持静态分支提示的平台,我们可以简单地将"可能"和"不太可能"定义为空。

这些是宏,可向编译器提示分支可能的运行方式。如果可用,宏会扩展为GCC特定的扩展。

GCC使用这些来优化分支预测。例如,如果我们有如下所示的内容

if (unlikely(x)) {
  dosomething();
}

return x;

然后,可以将这段代码重组为类似以下内容的代码:

if (!x) {
  return x;
}

dosomething();
return x;

这样做的好处是,当处理器第一次执行分支时,会产生很大的开销,因为它可能是在推测性地加载和执行代码。当它确定将进入分支时,则必须使其无效,然后从分支目标开始。

现在,大多数现代处理器都具有某种形式的分支预测,但是只有在我们之前浏览过分支并且分支仍位于分支预测缓存中时,该功能才有帮助。

在这些情况下,编译器和处理器可以使用许多其他策略。我们可以在Wikipedia上找到有关分支预测变量的更多详细信息:http://en.wikipedia.org/wiki/Branch_predictor

(一般评论其他答案涵盖细节)

没有理由不应该失去使用它们的可移植性。

我们始终可以选择创建一个简单的nil效果"内联"或者宏,使我们可以使用其他编译器在其他平台上进行编译。

如果我们在其他平台上,我们将无法获得优化的好处。