【问题标题】:Avoiding gcc function prologue overhead?避免 gcc 函数序言开销?
【发布时间】:2011-07-25 13:29:00
【问题描述】:

我最近遇到了很多 gcc 在 x86 上生成非常糟糕的代码的函数。它们都符合以下模式:

if (some_condition) {
    /* do something really simple and return */
} else {
    /* something complex that needs lots of registers */
}

将简单的情况想象成如此之小,以至于一半或更多的工作都花在了压入和弹出根本不会被修改的寄存器上。如果我手动编写 asm,我会在复杂情况下保存和恢复保存的跨调用寄存器,并在简单情况下完全避免接触堆栈指针。

有什么方法可以让 gcc 变得更聪明一点,并且自己来做这件事吗?最好使用命令行选项,而不是源代码中的丑陋黑客......

编辑:具体来说,这里有一些非常接近我正在处理的一些功能:

if (buf->pos < buf->end) {
    return *buf->pos++;
} else {
    /* fill buffer */
}

还有一个:

if (!initialized) {
    /* complex initialization procedure */
}
return &initialized_object;

还有一个:

if (mutex->type == SIMPLE) {
    return atomic_swap(&mutex->lock, 1);
} else {
    /* deal with ownership, etc. */
}

编辑 2:我应该首先提到:这些函数不能内联。它们具有外部链接,它们是库代码。允许它们在应用程序中内联会导致各种问题。

【问题讨论】:

  • 只是好奇,如果你反转 if 语句会发生什么?
  • 无论哪种方式,gcc 都将函数序言/尾声(保存寄存器、调整堆栈对齐等)置于两种情况之外,因此两种情况都会产生成本。
  • 您的样本缺少任何复杂的部分。您是否建议即使使用空的 else 块,编译器也会将其弄乱?
  • 好的,将printf("hello, world\n"); 添加到空块中......说真的,那里有什么并不重要。如果它进行一个或多个函数调用,您将导致堆栈对齐序言,如果它使用大量寄存器,您将导致保存/恢复一个或多个 ebx/esi/edi/ebp。跨度>
  • 另请注意:我试过__builtin_expect 并没有什么区别。

标签: c gcc code-generation x86


【解决方案1】:

我会这样做:

static void complex_function() {}

void foo()
{
    if(simple_case) {
        // do whatever
        return;
    } else {
        complex_function();
    }
}

编译器我坚持内联complex_function(),在这种情况下你可以使用它的noinline属性。

【讨论】:

    【解决方案2】:

    也许升级你的 gcc 版本? 4.6 刚刚发布。据我了解,它具有“部分内联”的可能性。也就是说,函数的易于集成的外部部分被内联,而昂贵的部分被转换为调用。但我不得不承认我自己还没有尝试过。

    编辑:我在 ChangeLog 中引用的声明:

    现在支持部分内联,并且 默认情况下在 -O2 或更高时启用。 该功能可以通过控制 -fpartial-inlining.

    部分内联将函数拆分为 返回的热路径短。这允许 更积极的热内联 路径导致更好的性能和 经常减少代码大小(因为 功能的冷部分不是 重复)。

    ...

    优化大小时的内联 (在程序的寒冷地区 或使用 -Os 编译时)是 改进以更好地处理 C++ 程序 具有更大的抽象惩罚, 导致代码更小更快。

    【讨论】:

    • @Jens:感谢您的想法,但内联不是一种选择。这是库中的一个外部函数,将其中一半内联在调用者中会在调用者中创建一个讨厌的实现/版本依赖。
    • @R.. 在任何情况下,inline 似乎新版本的 gcc 都可以因此拆分昂贵的函数的一部分。也许通过查看 gcc 最近的优化改进,您会发现一些有用的东西。
    • 我有什么特别的选择吗?
    • 那(“热路径”拆分)看起来像我需要的一般优化类型,但在不内联时使用。我想知道 gcc 是否可以将其应用于不适合内联的函数。我可能会得到 4.6 并尝试使用它...但我的猜测是我们必须等待 4.7 或 4.8 才能使此功能对我描述的情况有用...:-(
    • @R..,您总是可以在每个编译单元中只编译一个函数。告诉编译器inline 只是该单元中的那个函数,但在那里强制生成符号。如果您的头文件仅包含原型而没有定义,则编译器根本无法真正内联。我的猜测是 gcc 会使用“热路径”功能构造函数,从而将其一分为二。
    【解决方案3】:

    鉴于这些是外部调用,gcc 可能会将它们视为不安全并保留函数调用的寄存器(如果没有看到它保留的寄存器很难知道,包括你说的“未使用的寄存器” ')。出于好奇,这种过度的寄存器溢出是否仍然会在禁用所有优化的情况下发生?

    【讨论】:

    • 在 x86 ABI 上,ebx、ebp、esi 和 edi 需要被调用方保存,如果它破坏了它们。这是对的。问题是 gcc 在函数进入和退出时无条件地完成了保存和恢复它们的所有工作,即使简单的情况永远不会破坏它们。如果将保存和恢复移动到复杂分支中,简单分支将是指令数量的一半(或更少),并且速度明显更快。
    • @R..:你可以稍微调整一下这种行为,但没有太多细粒度的控制——如果这些函数是 extern 并且不受你的控制,你怎么知道有未使用的寄存器?
    • @geekosaur:这个问题与调用者的代码生成无关,它当然不知道被调用者使用什么寄存器。整个主题都是关于被调用者的代码生成,它绝对知道它使用什么寄存器。
    • 不,只有绝对知道您是用汇编程序编写它还是坚持使用特定的编译器版本和优化级别,以便您确定生成的代码。无论如何,我已经完成了这个问题;你显然陷入了一种世界观,其中唯一可能的答案是“你不能”。
    • 而且您显然只对按照“将其他更重要的考虑因素抛在窗外并按照我说的做”的方式给我“建议”而不是试图回答问题.
    【解决方案4】:

    更新

    要明确禁止 gcc 中单个函数的内联,请使用:

    void foo() __attribute__ ((noinline))
    {
      ...
    }
    

    另见How can I tell gcc not to inline a function?


    除非编译为 -O0(禁用优化),否则此类函数将定期自动内联。

    在 C++ 中,您可以使用 inline 关键字提示编译器

    如果编译器不接受您的提示,您可能在函数内使用了太多寄存器/分支。通过将“复杂”块提取到它自己的函数中,几乎可以肯定地解决了这种情况。


    更新我注意到您添加了它们是外部符号的事实。 (请使用该关键信息更新问题)。好吧,从某种意义上说,有了外部功能,所有的赌注都没有了。我真的不敢相信 gcc 会根据定义将所有复杂函数内联到一个微小的调用者中 simply 因为它只是从那里调用的。也许您可以提供一些示例代码来演示该行为,我们可以找到适当的优化标志来解决这个问题?

    还有,这是 C 还是 C++?在 C++ 中,我知道将琐碎的决策函数内联(主要作为类声明中定义的成员)是很常见的。这不会像简单的(外部)C 函数那样产生链接冲突。

    您还可以定义模板函数,在所有编译模块中完美内联,而不会导致链接冲突。

    我希望你使用 C++,因为它会给你很多选择。

    【讨论】:

    • 不,他们不会,因为这部分很复杂。
    • 因为它们是外部的。
    • 另外请注意,即使我将“复杂”分支移到它自己的函数中,(1) gcc 也会将其移回原处,因为 gcc 内联仅从一个位置调用的函数,以及 (2)即使 gcc 没有将其移回,为函数调用设置是丑陋的序言代码的原因之一,它使琐碎的情况变慢,并且 gcc 不会将设置放在其中一个分支中。跨度>
    • @R.. 你并没有真正鼓励我们帮助你和思考。请提供示例代码,以便我们进行实际分析,而不是仅仅做“我认为”/“我想”/“不,它会”显示。我开始认为你真的是在发泄意见而不是提出问题
    • 附言。我现在才注意到您提到该功能是外部的。那是一种痛苦。更新我的答案
    【解决方案5】:

    我可能会重构代码以鼓励简单案例的内联。也就是说,您可以使用-finline-limitgcc 考虑内联更大的函数,或者使用-fomit-frame-pointer -fno-exceptions 来最小化堆栈帧。 (请注意,后者可能会破坏调试并导致 C++ 异常行为不端。)

    不过,您可能无法从调整编译器选项中获得太多好处,并且必须进行重构。

    【讨论】:

    • 哇,我有很多答案建议做一些事情来让 gcc 更好地内联,当问题指出这是一个外部函数和内联是不可能的......
    • 我所说的内联是在外部、包装函数甚至宏的情况下。内联捕获简单案例并调用外部案例。这确实涉及两次测试条件,但只有在这种情况下无论如何都是昂贵的,所以它在噪音中下降。这不是火箭科学。
    • 同样,这在库代码中是不可能的,除非您想编写那些库中的一个,其中每个版本都不兼容,因为所有所谓不透明的库结构的内部被内联到调用应用程序中。如果 gcc 没有搞砸简单的情况,我的外部函数将比宏或内联函数好 80-90%,但它们只有 50-70%,因为 gcc 增加了很多不必要的开销。
    • 你在这里忙着制造矛盾。如果可以更改函数链接,则可以插入宏。如果不能插入宏,那么更改函数链接为时已晚。
    • 我根本不想使用宏或内联函数。我正在尝试在一个库中编写一个普通的extern-linkage 函数,该函数具有一个短而快速的“热路径”(感谢 Jens 的这个词),具有最小的进入/退出开销和一个只执行一次的更大的分支或很少。
    猜你喜欢
    • 2011-10-09
    • 1970-01-01
    • 1970-01-01
    • 2013-01-08
    • 2015-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-10
    相关资源
    最近更新 更多