Рассмотрим следующий код:
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
Я ожидаю, что первая ветвь будет работать быстрее, потому что компилятор может встроить вызов функции в acc. И вторая ветвь не может быть встроена, потому что acc вызывается через указатель функции, хранящийся в массиве.
Но результаты меня удивили:
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 для измерения и получил похожие результаты.
Не совсем верно, что вторая ветвь не может быть встроенной. Фактически, все указатели функций, хранящиеся в массиве, видны во время компиляции. Таким образом, компилятор может заменить косвенные вызовы функций прямыми вызовами (и это так). Теоретически он может пойти дальше и встроить их (и в этом случае у нас есть две одинаковые ветви). Но этот конкретный компилятор не достаточно умен, чтобы сделать это.
В результате первая ветка оптимизируется «лучше». Но с одним исключением. Компилятору не разрешено оптимизировать переменные sum
, Как вы можете видеть из дизассемблированного кода, при этом сразу создаются инструкции хранилища, а затем инструкции загрузки (в зависимости от этих инструкций хранилища):
mov %edx,0x804a018
mov 0x804a018,%edx
В Руководстве по оптимизации программного обеспечения Intel (раздел 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;
Других решений пока нет …