请考虑以下代码:
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进行测量并获得了类似的结果。
答案 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;