C-подобная обработка обратных вызовов: какой алгоритм преформируется быстрее? - PullRequest
0 голосов
/ 20 августа 2011

У меня есть массив обратных вызовов вроде этого void (*callbacks[n])(void* sender), и мне интересно, какой из этих кодов преформируется быстрее:

//Method A
void nullcallback(void* sender){};

void callbacka(void* sender) 
{
    printf("Hello ");
}

void callbackb(void* sender)
{
    printf("world\n");
}

int main()
{
    void (*callbacks[5])(void* sender);
    unsigned i;
    for (i=0;i<5;++i)
        callbacks[i] = nullcallback;
    callbacks[2] = callbacka;
    callbacks[4] = callbackb;
    for (i=0;i<5;++i)
        callbacks[i](NULL);
};

или

//Method B
void callbacka(void* sender) 
{
    printf("Hello ");
}

void callbackb(void* sender)
{
    printf("world\n");
}

int main()
{
    void (*callbacks[5])(void* sender);
    unsigned i;
    for (i=0;i<5;++i)
        callbacks[i] = NULL;
    callbacks[2] = callbacka;
    callbacks[4] = callbackb;
    for (i=0;i<5;++i)
        if (callbacks[i])
            callbacks[i](NULL);
};

некоторые условия:

  • Имеет ли значение, если я знаю, что большинство моих обратных вызовов действительны или нет?
  • Имеет ли значение, если я компилирую свой код с использованием компилятора C или C ++?
  • Изменяет ли целевая платформа (Windows, Linux, Mac, iOS, Android) какие-либо результаты в результатах?(вся причина этого массива обратного вызова в управлении обратными вызовами в игре)

Ответы [ 4 ]

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

Для этого вам нужно посмотреть код ассемблера. На моей платформе (gcc, 32bit) я обнаружил, что компилятор не может оптимизировать вызов nullcallback out. Но если я улучшу ваш метод А до следующего

int main(void) {
  static void (*const callbacks[5])(void* sender) = {
    [0] = nullcallback,
    [1] = nullcallback,
    [2] = callbacka,
    [3] = nullcallback,
    [4] = callbackb,
  };
  for (unsigned i=0;i<5;++i)
        callbacks[i](0);
};

компилятор может развернуть цикл и оптимизировать вызовы, в результате просто

    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $16, %esp
    movl    $0, (%esp)
    call    callbacka
    movl    $0, (%esp)
    call    callbackb
    xorl    %eax, %eax
    leave
    ret
    .size   main, .-main
1 голос
/ 20 августа 2011

Это полностью зависит от вашей реальной ситуации.Если возможно, я бы предпочел метод A, потому что он проще читать и создавать более чистый код, в частности, если ваша функция имеет возвращаемое значение:

ret = callbacks[UPDATE_EVENT](sender);
// is nicer then
if (callbacks[UPDATE_EVENT])
    ret = callbacks[UPDATE_EVENT](sender);
else
    ret = 0;

Конечно, метод A становится tedouis, когда вы не толькоподпись одной функции, но скажем, 100 разных подписей.И для каждого вы должны написать нулевую функцию.

Для оценки производительности зависит, является ли nullcallback () редким случаем или нет.Если это редко, метод А, очевидно, быстрее.Если нет, метод B мог бы быть немного быстрее, но это зависит от многих факторов: какую платформу вы используете, сколько аргументов имеют ваши функции и т. Д. Но в любом случае, если ваши обратные вызовы выполняют «реальную работу», т.е.не только некоторые простые вычисления, это не должно иметь никакого значения.

Когда ваш метод B действительно может быть быстрее, это когда вы не только вызываете обратный вызов для одного отправителя, но и для очень многих:

extern void *senders[SENDERS_COUNT]; // SENDERS_COUNT is a large number

if (callbacks[UPDATE_EVENT])
{
    for (int i = 0; i < SENDERS_COUNT; i++)
        callbacks[UPDATE_EVENT](senders[i]);
} 

Здесь весь цикл пропускается, когда нет действительного обратного вызова.Эту настройку также можно выполнить с помощью метода A, если известен адрес nullcallback (), т.е.не определено только в каком-либо модуле.

1 голос
/ 20 августа 2011

Для общего случая метод B предпочтителен, но для LUT указателя функции, когда NULL является исключением, чем метод A , микроскопически быстрее.

Основной примерВ таблице системных вызовов Linux вызовы NULL должны происходить только в редких случаях при запуске двоичных файлов, созданных на более новых системах, или из-за ошибки программиста.Системные вызовы происходят достаточно часто, так что могут помочь улучшения наносекунды или даже пикосекунды.

Другие примеры, которые могут оказаться достойными, - это использование LUT-кода операции внутри эмуляторов, таких как MAME.

1 голос
/ 20 августа 2011

Вы можете оптимизировать свой код, просто инициализируя массив нулем, чтобы начать с:

void (*callbacks[5])(void* sender) = { 0 };

Тогда вы полностью устранили необходимость для цикла for устанавливать каждый указатель на * 1004.*.Теперь вам просто нужно выполнить задания для callbacka и callbackb.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...