如果函数包含静态变量,为什么编译器不执行内联?

Why does the compiler not perform inlining, if the function contains static variables?

我从以下网站了解到,当函数具有静态变量时,编译器可能不会执行内联。这是什么原因?

参考:Inline Functions in C++

Remember, inlining is only a request to the compiler, not a command. Compiler can ignore the request for inlining. Compiler may not perform inlining in such circumstances like:

  1. If a function contains a loop. (for, while, do-while)
  2. If a function contains static variables.
  3. If a function is recursive.
  4. If a function return type is other than void, and the return statement doesn’t exist in function body.
  5. If a function contains switch or goto statement.

一般来说,编译器会决定内联什么而不内联什么。 inline 关键字的用途已更改为允许某些内容有多个定义。

引用自cppreference

The original intent of the inline keyword was to serve as an indicator to the optimizer that inline substitution of a function is preferred over function call, that is, instead of executing the function call CPU instruction to transfer control to the function body, a copy of the function body is executed without generating the call. This avoids overhead created by the function call (passing the arguments and retrieving the result) but it may result in a larger executable as the code for the function has to be repeated multiple times.

Since this meaning of the keyword inline is non-binding, compilers are free to use inline substitution for any function that's not marked inline, and are free to generate function calls to any function marked inline. Those optimization choices do not change the rules regarding multiple definitions and shared statics listed above.

Because the meaning of the keyword inline for functions came to mean "multiple definitions are permitted" rather than "inlining is preferred", that meaning was extended to variables.

编译器可以完全内联带有循环静态变量、switch 语句甚至递归函数的函数。

这是一个例子:

#include <iostream>

inline int foo(int* a, int n)
{
    int r = 0;
    static int b;
    for (int i = 0; i < n; i++)
    {
        r += a[i];
    }
    switch (n)
    {
    case 42:
        std::cout << "???\n";
    }
    return r;
}

inline int foo2(int n)
{
    return n == 0 ? 0 : 1 + foo2(n - 1);
}

int main()
{
    int bar[3];
    for (int i = 0; i < 3; i++)
    {
        std::cin >> bar[i];
    }
    std::cout << foo(bar, 3) << '\n';
    std::cout << foo2(bar[0]) << '\n';
}

这里是编译器generated:

的汇编代码
main:
        sub     rsp, 24
        mov     edi, OFFSET FLAT:_ZSt3cin
        lea     rsi, [rsp+4]
        call    std::basic_istream<char, std::char_traits<char> >::operator>>(int&)
        lea     rsi, [rsp+8]
        mov     edi, OFFSET FLAT:_ZSt3cin
        call    std::basic_istream<char, std::char_traits<char> >::operator>>(int&)
        lea     rsi, [rsp+12]
        mov     edi, OFFSET FLAT:_ZSt3cin
        call    std::basic_istream<char, std::char_traits<char> >::operator>>(int&)
        mov     esi, DWORD PTR [rsp+8]
        mov     edi, OFFSET FLAT:_ZSt4cout
        add     esi, DWORD PTR [rsp+4]
        add     esi, DWORD PTR [rsp+12]
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     edx, 1
        lea     rsi, [rsp+3]
        mov     BYTE PTR [rsp+3], 10
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     esi, DWORD PTR [rsp+4]
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        lea     rsi, [rsp+3]
        mov     edx, 1
        mov     BYTE PTR [rsp+3], 10
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        xor     eax, eax
        add     rsp, 24
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

请注意,在汇编代码中,main 函数中没有对 foofoo2 的调用。数组元素的相加是由 main 函数中间的指令 mov esi, DWORD PTR [rsp+8]add esi, DWORD PTR [rsp+4]add esi, DWORD PTR [rsp+12] 执行的。该文章要么是错误的,要么是它使用 "may not" 表示 "might not" 而不是 "isn't allowed to"。后一种情况是有意义的,因为编译器不太可能内联更大和更复杂的函数。

此外,如其他答案中所述,编译器可以在没有 inline 关键字的情况下内联函数。如果您从上面的示例中删除 inline 关键字,编译器仍将内联该函数。

当文章说“may not perform”时,我认为它的意思是“might not perform”,看看这个短语是如何紧跟在“can ignore”之后的。如果是这种情况,编译器实际上不需要不内联函数的理由。编译器自行决定内联。

不过,编译器做什么和不做什么通常都是有原因的。较新的编译器比旧的编译器更擅长内联函数。文章的作者可能试验了一个编译器,该编译器根本不支持带有静态变量的内联函数。我希望较新的编译器没有此限制。

在评估编译器的局限性时,请记住信息的年龄。我没有看到那篇文章的日期。不过,它的评论可以追溯到三年前(而且它的信息似乎比这更早)。三年可以发生很多事情。编译器不断发展并变得更好。不可能的事情可能已经变得司空见惯。

更不用说那篇文章的主题是离题了。至少从 C++98(22 年前)开始,C++ 中的 inline 关键字与函数是否内联无关。