Стоимость вызова функции - PullRequest
       16

Стоимость вызова функции

0 голосов
/ 05 августа 2011

Приложение, с которым я сейчас имею дело, использует некоторый численный алгоритм грубой силы, который вызывает множество крошечных функций миллиарды раз. Я бродил, насколько производительность может быть улучшена за счет исключения вызовов функций с использованием наклонного и статического полиморфизма.

Какова стоимость вызова функции относительно вызова не встроенной и не встроенной функции в следующих ситуациях:

1) вызов функции через указатель функции

2) вызов виртуальной функции

Я знаю, что это трудно измерить, но очень приблизительная оценка сделала бы это.

Спасибо!

Ответы [ 4 ]

3 голосов
/ 05 августа 2011

Чтобы компилятор вызова функции-члена должен:

Fetch address of function -> Call function

Для вызова виртуальной функции компилятору необходимо:

Fetch address of vptr -> Fetch address of the function -> Call function

Примечание. Этот виртуальный механизм является деталью реализации компилятора, поэтому реализация может отличаться для разных компиляторов, может даже не быть vptr или vtable в этом отношении. Сказав так, обычно компиляторы реализуют его с vptr и vtable, а затем выше справедливо.

Так что есть некоторые накладные расходы наверняка (Еще один Fetch). Чтобы точно знать, как сильно это влияет, вам придется профилировать свой исходный код, так как нет более простого способа.

2 голосов
/ 05 августа 2011

Это зависит от вашей целевой архитектуры и вашего компилятора, но вы можете написать небольшой тест и проверить сгенерированную сборку.

Я сделал один, чтобы сделать тест:

// test.h
#ifndef FOO_H
#define FOO_H

void bar();

class A {
public:
    virtual ~A();
    virtual void foo();
};

#endif

// main.cpp
#include "test.h"

void doFunctionPointerCall(void (*func)()) {
    func();
}

void doVirtualCall(A *a) {
    a->foo();
}

int main() {
    doFunctionPointerCall(bar);

    A a;
    doVirtualCall(&a);

    return 0;
}

Обратите внимание, что вам даже не нужно писать test.cpp, поскольку вам просто нужно проверить сборку для main.cpp.

Чтобы увидеть выходные данные сборки компилятора, с gcc используйте флаг -S:

gcc main.cpp -S -O3

Это создаст файл main.s, с выводом сборки. Теперь мы можем видеть, что сгенерировал gcc для вызовов.

doFunctionPointerCall:

.globl _Z21doFunctionPointerCallPFvvE
    .type   _Z21doFunctionPointerCallPFvvE, @function
_Z21doFunctionPointerCallPFvvE:
.LFB0:
    .cfi_startproc
    jmp *%rdi
    .cfi_endproc
.LFE0:
    .size   _Z21doFunctionPointerCallPFvvE, .-_Z21doFunctionPointerCallPFvvE

doVirtualCall:

.globl _Z13doVirtualCallP1A
    .type   _Z13doVirtualCallP1A, @function
_Z13doVirtualCallP1A:
.LFB1:
    .cfi_startproc
    movq    (%rdi), %rax
    movq    16(%rax), %rax
    jmp *%rax
    .cfi_endproc
.LFE1:
    .size   _Z13doVirtualCallP1A, .-_Z13doVirtualCallP1A

Обратите внимание, здесь я использую x86_64, сборка изменится для других архитектур.

Глядя на сборку, похоже, что она использует два дополнительных movq для виртуального вызова, возможно, это некоторое смещение в vtable. Обратите внимание, что в реальном коде необходимо сохранить некоторые регистры (будь то указатель на функцию или виртуальный вызов), но для виртуального вызова все равно потребуется два дополнительных указателя movq поверх функции.

0 голосов
/ 05 августа 2011

Вызовы функций являются значительными издержками, если функции невелики.CALL и RETURN при оптимизации на современных процессорах все равно будут заметны, когда будет сделано много вызовов.Также небольшие функции могут быть распределены по памяти, так что CALL / RETURN также может вызывать пропадание кеша и чрезмерное разбиение на страницы.

//code
int Add(int a, int b) { return a + b; }
int main() {
    Add(1, Add(2, 3));
...
}

// NON-inline x86 ASM
Add:
    MOV eax, [esp+4] // 1st argument a
    ADD eax, [esp+8] // 2nd argument b
    RET 8 // return and fix stack 2 args * 4 bytes each
    // eax is the returned value

Main:
    PUSH 3
    PUSH 2
    CALL [Add]
    PUSH eax 
    PUSH 1
    CALL [Add]
...

// INLINE x86 ASM
Main:
    MOV eax, 3
    ADD eax, 2
    ADD eax, 1
...

Если ваша цель - оптимизация, и вы вызываете много небольших функций, всегда лучше встроить их.Извините, мне все равно, какой уродливый синтаксис ASM используется компиляторами c / c ++.

0 голосов
/ 05 августа 2011

Просто используйте профилировщик, такой как кодовый анализатор AMD (с использованием IBS и TBS), в противном случае вы можете пойти по более «хардкорному» маршруту и ​​прочитать руководства по оптимизации Agner Fog (они помогут как для точной синхронизации команд, так и для оптимизации вашего кода): http://www.agner.org/optimize/

...