为什么内联函数比函数指针慢?

时间:2013-05-11 02:51:11

标签: c++ performance g++ inline disassembly

请考虑以下代码:

typedef void (*Fn)();

volatile long sum = 0;

inline void accu() {
    sum+=4;
}

static const Fn map[4] = {&accu, &accu, &accu, &accu};

int main(int argc, char** argv) {
    static const long N = 10000000L;
    if (argc == 1)
    {
            for (long i = 0; i < N; i++)
            {
                    accu();
                    accu();
                    accu();
                    accu();
            }
    }
    else
    {
            for (long i = 0; i < N; i++)
            {
                    for (int j = 0; j < 4; j++)
                            (*map[j])();
            }
    }
}

当我编译它时:

g++ -O3 test.cpp

我期望第一个分支运行得更快,因为编译器可以内联对accu的函数调用。并且第二个分支不能内联,因为通过存储在数组中的函数指针来调用accu。

但结果让我感到惊讶:

time ./a.out 

real    0m0.108s
user    0m0.104s
sys 0m0.000s

time ./a.out 1

real    0m0.095s
user    0m0.088s
sys 0m0.004s

我不明白为什么,所以我做了一次objdump:

objdump -DStTrR a.out > a.s

并且反汇编似乎没有解释我得到的性能结果:

8048300 <main>:
8048300:       55                      push   %ebp
8048301:       89 e5                   mov    %esp,%ebp
8048303:       53                      push   %ebx
8048304:       bb 80 96 98 00          mov    $0x989680,%ebx
8048309:       83 e4 f0                and    $0xfffffff0,%esp
804830c:       83 7d 08 01             cmpl   $0x1,0x8(%ebp)
8048310:       74 27                   je     8048339 <main+0x39>
8048312:       8d b6 00 00 00 00       lea    0x0(%esi),%esi
8048318:       e8 23 01 00 00          call   8048440 <_Z4accuv>
804831d:       e8 1e 01 00 00          call   8048440 <_Z4accuv>
8048322:       e8 19 01 00 00          call   8048440 <_Z4accuv>
8048327:       e8 14 01 00 00          call   8048440 <_Z4accuv>
804832c:       83 eb 01                sub    $0x1,%ebx
804832f:       90                      nop
8048330:       75 e6                   jne    8048318 <main+0x18>
8048332:       31 c0                   xor    %eax,%eax
8048334:       8b 5d fc                mov    -0x4(%ebp),%ebx
8048337:       c9                      leave
8048338:       c3                      ret
8048339:       b8 80 96 98 00          mov    $0x989680,%eax
804833e:       66 90                   xchg   %ax,%ax
8048340:       8b 15 18 a0 04 08       mov    0x804a018,%edx
8048346:       83 c2 04                add    $0x4,%edx
8048349:       89 15 18 a0 04 08       mov    %edx,0x804a018
804834f:       8b 15 18 a0 04 08       mov    0x804a018,%edx
8048355:       83 c2 04                add    $0x4,%edx
8048358:       89 15 18 a0 04 08       mov    %edx,0x804a018
804835e:       8b 15 18 a0 04 08       mov    0x804a018,%edx
8048364:       83 c2 04                add    $0x4,%edx
8048367:       89 15 18 a0 04 08       mov    %edx,0x804a018
804836d:       8b 15 18 a0 04 08       mov    0x804a018,%edx
8048373:       83 c2 04                add    $0x4,%edx
8048376:       83 e8 01                sub    $0x1,%eax
8048379:       89 15 18 a0 04 08       mov    %edx,0x804a018
804837f:       75 bf                   jne    8048340 <main+0x40>
8048381:       eb af                   jmp    8048332 <main+0x32>
8048383:       90                      nop
...
8048440 <_Z4accuv>:
8048440:       a1 18 a0 04 08          mov    0x804a018,%eax
8048445:       83 c0 04                add    $0x4,%eax
8048448:       a3 18 a0 04 08          mov    %eax,0x804a018
804844d:       c3                      ret
804844e:       90                      nop
804844f:       90                      nop

看起来直接调用分支肯定比函数指针分支少。 但为什么函数指针分支比直接调用运行得更快?

请注意,我只用“时间”来衡量时间。我已经使用clock_gettime进行测量并获得了类似的结果。

1 个答案:

答案 0 :(得分:6)

第二个分支不能内联是不完全正确的。实际上,存储在数组中的所有函数指针都是在编译时看到的。因此编译器可以通过直接调用替换间接函数调用(并且它会这样做)。从理论上讲,它可以更进一步并内联它们(在这种情况下,我们有两个相同的分支)。但是这个特殊的编译器不够智能。

结果,第一个分支被“优化”优化。但有一个例外。编译器不允许优化volatile变量sum。从反汇编代码中可以看出,这会立即生成存储指令,然后是加载指令(取决于这些存储指令):

mov    %edx,0x804a018
mov    0x804a018,%edx

英特尔软件优化手册(第3.6.5.2节)不建议安排如下说明:

  

...如果负载在它依赖的存储之后过早安排,或者如果要存储的数据的生成被延迟,则可能会有很大的损失。

第二个分支避免了这个问题,因为存储和加载之间有额外的调用/返回指令。所以它表现得更好。

如果我们在中间添加一些(不是非常昂贵的)计算,可以对第一个分支进行类似的改进:

long x1 = 0;
for (long i = 0; i < N; i++)
{
  x1 ^= i<<8;
  accu();
  x1 ^= i<<1;
  accu();
  x1 ^= i<<2;
  accu();
  x1 ^= i<<4;
  accu();
}
sum += x1;