内联函数是否作为参数传递,真的在 C/C++ 中内联执行?

Are inline functions passed as argument, really executed inline in C/C++?

我有一个很长的(迭代次数)for 循环,我希望可以对其某些部分进行个性化设置。代码如下所示:

function expensive_loop( void (*do_true)(int),  void (*do_false)(int)){
    for(i=0; i<VeryLargeN; i++){
       element=elements[i]
       // long computation that produce a boolean condition
       if (condition){ 
         do_true(element); 
       }else{
         do_false(element);
       }
    }
}

现在,问题是每次调用 do_truedo_false 时,由于堆栈的 push/pop 会产生开销,这会破坏代码的高性能.

为了解决这个问题,我可以简单地创建多个 expensive_loop 函数的副本,每个副本都有自己的 do_true 和 do_false 实现。这将使代码无法维护。

那么,人们如何制作迭代的内部部分,使其可以个性化,同时仍然保持高性能?

请注意,该函数接受 指向函数的指针,因此通过指针调用这些函数。如果 expensive_loop 的定义和那些函数可用并且没有违反编译器内联限制,优化器可能会通过函数指针内联这些调用。

另一种选择是使该算法成为接受可调用对象(函数指针、带有调用运算符的对象、lambda)的函数模板,就像标准算法一样。这样编译器可能会有更多的优化机会。例如:

template<class DoTrue, class DoFalse>
void expensive_loop(DoTrue do_true, DoFalse do_false) { 
    // Original function body here.
}

-Winline g++ 的编译器开关:

-Winline

Warn if a function can not be inlined and it was declared as inline. Even with this option, the compiler will not warn about failures to inline functions declared in system headers.

The compiler uses a variety of heuristics to determine whether or not to inline a function. For example, the compiler takes into account the size of the function being inlined and the the amount of inlining that has already been done in the current function. Therefore, seemingly insignificant changes in the source program can cause the warnings produced by -Winline to appear or disappear.

当通过指针调用函数时,它可能不会警告函数未被内联。

问题是do_truedo_false中实际设置的函数地址,直到link时才解决,优化的机会不多。

如果您在代码中显式设置这两个函数(即函数本身不是来自外部库等),您可以使用 C++ 模板声明您的函数,以便编译器准确知道哪个你当时想调用的函数。

struct function_one {
  void operator()( int element ) {
  }
};

extern int elements[];
extern bool condition();

template < typename DoTrue, typename DoFalse >
void expensive_loop(){
  DoTrue do_true;
  DoFalse do_false;

  for(int i=0; i<50; i++){
    int element=elements[i];
    // long computation that produce a boolean condition
    if (condition()){ 
      do_true(element); // call DoTrue's operator()
    }else{
      do_false(element); // call DoFalse's operator()
    }
  }
}

int main( int argc, char* argv[] ) {
    expensive_loop<function_one,function_one>();

return 0;
}

编译器将为您指定的每个 DoTrue 和 DoFalse 类型组合实例化一个 expensive_loop 函数。如果您使用多个组合,它会增加可执行文件的大小,但它们中的每一个都应该达到您的预期。

对于我显示的示例,请注意函数是空的。 编译器只是剥离函数调用并离开循环:

main:
    push    rbx
    mov     ebx, 50
.L2:
    call    condition()
    sub     ebx, 1
    jne     .L2
    xor     eax, eax
    pop     rbx
    ret

参见 https://godbolt.org/g/hV52Nn

中的示例

在您的示例中使用函数指针可能不会内联函数调用。这是在 expensive_loop

的程序中为 mainexpensive_loop 生成的汇编器
// File A.cpp
void foo( int arg );
void bar( int arg );

extern bool condition();
extern int elements[];

void expensive_loop( void (*do_true)(int),  void (*do_false)(int)){
    for(int i=0; i<50; i++){
       int element=elements[i];
       // long computation that produce a boolean condition
       if (condition()){
         do_true(element);
       }else{
         do_false(element);
       }
    }
}

int main( int argc, char* argv[] ) {
    expensive_loop( foo, bar );

    return 0;
}

和参数传递的函数

// File B.cpp
#include <math.h>

int elements[50];

bool condition() {
    return elements[0] == 1;
}

inline int foo( int arg ) {
    return arg%3;
}

inline int bar( int arg ) {
    return 1234%arg;
}

在不同的翻译单元中定义。

0000000000400620 <expensive_loop(void (*)(int), void (*)(int))>:
  400620:       41 55                   push   %r13
  400622:       49 89 fd                mov    %rdi,%r13
  400625:       41 54                   push   %r12
  400627:       49 89 f4                mov    %rsi,%r12
  40062a:       55                      push   %rbp
  40062b:       53                      push   %rbx
  40062c:       bb 60 10 60 00          mov    [=14=]x601060,%ebx
  400631:       48 83 ec 08             sub    [=14=]x8,%rsp
  400635:       eb 19                   jmp    400650 <expensive_loop(void (*)(int), void (*)(int))+0x30>
  400637:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  40063e:       00 00
  400640:       48 83 c3 04             add    [=14=]x4,%rbx
  400644:       41 ff d5                callq  *%r13
  400647:       48 81 fb 28 11 60 00    cmp    [=14=]x601128,%rbx
  40064e:       74 1d                   je     40066d <expensive_loop(void (*)(int), void (*)(int))+0x4d>
  400650:       8b 2b                   mov    (%rbx),%ebp
  400652:       e8 79 ff ff ff          callq  4005d0 <condition()>
  400657:       84 c0                   test   %al,%al
  400659:       89 ef                   mov    %ebp,%edi
  40065b:       75 e3                   jne    400640 <expensive_loop(void (*)(int), void (*)(int))+0x20>
  40065d:       48 83 c3 04             add    [=14=]x4,%rbx
  400661:       41 ff d4                callq  *%r12
  400664:       48 81 fb 28 11 60 00    cmp    [=14=]x601128,%rbx
  40066b:       75 e3                   jne    400650 <expensive_loop(void (*)(int), void (*)(int))+0x30>
  40066d:       48 83 c4 08             add    [=14=]x8,%rsp
  400671:       5b                      pop    %rbx
  400672:       5d                      pop    %rbp
  400673:       41 5c                   pop    %r12
  400675:       41 5d                   pop    %r13
  400677:       c3                      retq
  400678:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40067f:       00

即使使用 -O3 优化级别,您也可以看到如何仍然执行调用:

400644:       41 ff d5                callq  *%r13