Подтвердить что ты не робот

Почему функция inlined медленнее, чем указатель на функцию?

Рассмотрим следующий код:

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 для измерения и получил аналогичные результаты.

4b9b3361

Ответ 1

Не совсем верно, что вторая ветвь не может быть встроена. Фактически, все указатели функций, хранящиеся в массиве, отображаются во время компиляции. Таким образом, компилятор может заменить вызовы косвенных функций прямыми вызовами (и он делает это). Теоретически он может идти дальше и встраивать их (и в этом случае мы имеем две идентичные ветки). Но этот конкретный компилятор недостаточно умен, чтобы сделать это.

В результате первая ветвь оптимизирована "лучше". Но за одним исключением. Компилятор не может оптимизировать изменчивую переменную 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;