【问题标题】:Inlineness guarantee of one-liner function templates c++单行函数模板c ++的内联性保证
【发布时间】:2019-03-08 04:25:25
【问题描述】:

在 C++ 标准库中,有很多单行函数模板。例如。 std::move 本质上只是一个转换,一个实现可能是:

template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

我知道实际上,不会从 std::move 生成任何机器代码,因为它只是一个演员表。我的问题是:标准中是否有任何保证说,对于像 std::move 或 std::forward 之类的函数(只不过是强制转换),它们必须总是被内联(所以不生成机器代码)?换句话说,(迂腐的)编译器是否可以将它们视为普通函数(即,将参数放在堆栈上,并生成 call 和 ret 指令) ?

【问题讨论】:

  • 该标准没有指定没有可观察到的副作用的优化。如果您询问的是启用了优化的体面的编译器,那么这将不会生成函数调用。
  • 为什么重要。只要程序按预期进行。
  • 该标准不承诺int i = 0; i++; 不会花费 5 分钟和 117 次机器级函数调用。您依靠编译器来合理地行事。

标签: c++


【解决方案1】:

我的问题是:标准中是否有任何保证说对于像 std::move 或 std::forward 这样的函数(只不过是强制转换),它们必须始终是内联的(因此不会生成机器代码)?

没有。该标准只描述抽象机器的可观察行为。代码生成是标准一无所知的实现细节。

话虽如此,std::forward 和 std::move 都是真正只影响 C++ 类型系统的操作,而不是实际数据,所以我会非常惊讶地看到为它们生成的任何机器代码优化构建。

另一方面,在未优化的构建中,保留 std::move 的轮廓(与几乎任何其他函数调用一样)可能是简化调试的好主意。您可以轻松地对此进行测试 (live on gcc.godbolt):

#include <utility>

struct Foo {
    int i;
    Foo(Foo &&other) :i(other.i) {};
};

Foo with_move(Foo f) {
    return std::move(f);
}

在 gcc 中,带有 -O0 std::move 的函数作为实际函数生成(除了设置/拆除堆栈帧并返回它接收到的指针参数之外什么都不做)

Foo::Foo(Foo&&):
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     QWORD PTR [rbp-16], rsi
        mov     rax, QWORD PTR [rbp-16]
        mov     edx, DWORD PTR [rax]
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], edx
        nop
        pop     rbp
        ret
with_move(Foo):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     QWORD PTR [rbp-16], rsi
        mov     rax, QWORD PTR [rbp-16]
        mov     rdi, rax
        call    std::remove_reference<Foo&>::type&& std::move<Foo&>(Foo&)
        mov     rdx, rax
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rdx
        mov     rdi, rax
        call    Foo::Foo(Foo&&)
        mov     rax, QWORD PTR [rbp-8]
        leave
        ret
std::remove_reference<Foo&>::type&& std::move<Foo&>(Foo&):
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        pop     rbp
        ret

即使在 -O1 时,所有内容都会内联:

with_move(Foo):
        mov     rax, rdi
        mov     edx, DWORD PTR [rsi]
        mov     DWORD PTR [rdi], edx
        ret

【讨论】:

  • 我明白了。从带有 -O0 的代码中可以清楚地看出 std::move 等可以被视为普通函数;出于性能原因,任何合理的实现通常都会内联它。谢谢。
【解决方案2】:

这不是来自标准,而是来自 Scott Meyer 的有效 CPP。摘自
第 30 项:了解内联的来龙去脉。

编译器优化是 通常设计用于缺少函数调用的代码段,因此 当你内联一个函数时,你可以让编译器执行上下文- 对函数体的具体优化。大多数编译器 永远不要对“概述”的函数调用进行此类优化。
...
在内存有限的机器上,过分热心 内联可能会导致程序太大而无法使用 空间。即使使用虚拟内存,内联引起的代码膨胀也会导致 额外的分页,降低指令缓存命中率,以及 伴随这些事情的性能损失。
...
另一方面,如果内联函数体很短,代码 为函数体生成的代码可能小于生成的代码 用于函数调用。如果是这种情况,内联函数可能 实际上导致更小的目标代码和更高的指令缓存命中 率!
...
请记住,内联是对编译器的请求,而不是命令。 ...
模板实例化独立于内联。如果你正在写一个 模板并且您相信所有从 模板应该是内联的,声明模板内联;
...
但是,如果您正在为没有理由想要内联的函数编写模板,请避免将模板声明为内联(显式或隐式)。内联 有成本,您不想在没有事先考虑的情况下产生这些成本。
...
... 让我们结束观察 inline 是一个请求 编译器可能会忽略。大多数编译器拒绝内联函数 他们认为太复杂(例如,那些包含循环或递归的), 除了对虚函数的最微不足道的调用之外,所有的调用都无法进行内联。

这一切加起来就是:给定的内联函数是否真的是 内联取决于你使用的构建环境——主要是 编译器。幸运的是,大多数编译器都有一个诊断级别, 如果他们未能内联函数,将导致警告 你已经要求他们这样做了。

有时编译器会为内联函数生成函数体 即使他们完全愿意内联函数。例如, 如果您的程序采用内联函数的地址,则编译器 通常必须为其生成一个概述的函数体。

如果你能掌握这本书,阅读整本书,它会消除你的许多疑惑。

【讨论】:

    猜你喜欢
    • 2019-02-11
    • 1970-01-01
    • 2013-07-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-11
    • 1970-01-01
    • 2014-02-08
    相关资源
    最近更新 更多