【问题标题】:C-like callback handling: which algorithm preforms faster?类 C 回调处理:哪种算法执行得更快?
【发布时间】:2011-08-20 04:41:10
【问题描述】:

我有一系列像 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)是否会改变结果中的任何内容? (这个回调数组的全部原因是为了管理游戏中的回调)

【问题讨论】:

  • 哇,微小的微优化。这就是分析的用途。尽管null pointers,有一些关于 LKML 的有趣讨论,主题类似。
  • 方法 B 更快。因为调用函数会比 NULL 比较消耗更多时间。
  • 您的编译器、您的架构和您的应用程序。对这两种方法计时,并记住结果可能不适用于不同的编译器、不同的架构或不同的应用程序。
  • 基准,基准,基准。
  • 除非你有分析器的输出来确认它的重要性,否则请不要进入这个。与此同时,做任何能产生更干净代码的事情。

标签: c++ c callback


【解决方案1】:

您必须为此查看汇编代码。在我的平台(gcc,32 位)上,我发现编译器无法优化对 nullcallback 的调用。但是,如果我将您的方法 A 改进为以下

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

【讨论】:

    【解决方案2】:

    这完全取决于您的实际情况。如果可能的话,我更喜欢方法 A,因为它更容易阅读和生成更清晰的代码,特别是如果你的函数有返回值:

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

    当然,当您不仅有一个函数签名而且假设有 100 个不同的签名时,方法 A 会变成 tedouis。并且对于每个你必须编写一个空函数。

    出于性能考虑,这取决于 nullcallback() 是否为罕见情况。如果很少见,方法 A 显然更快。如果不是方法 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]);
    } 
    

    这里没有有效回调时会跳过整个循环。如果 nullcallback() 地址已知,也可以使用方法 A 进行此调整,即。仅在某些模块中未定义。

    【讨论】:

      【解决方案3】:

      您可以通过简单地将数组初始化为零来进一步优化您的代码:

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

      那么您就完全不需要 for 循环将每个指针设置为 NULL。您现在只需为callbackacallbackb 进行分配。

      【讨论】:

      • 或者,如果您使用向量和默认构造函数,您可以轻松地将所有元素初始化为 nullcallback()
      • 使用 C99,您甚至可以使用 { [2] = callbacka, [4] = callbackb } 进行初始化。
      【解决方案4】:

      对于一般情况,方法 B 是首选,但对于函数指针 LUT,当 NULL 是例外时,方法 A 在微观上更快。

      主要示例是 Linux 系统调用表,NULL 调用只应在运行构建在较新系统上的二进制文件或程序员错误的极少数情况下发生。系统调用经常发生,以至于纳秒甚至皮秒的改进都可以提供帮助。

      其他可能值得证明的实例是 MAME 等模拟器中的操作码 LUT。

      【讨论】:

      • 我完全不同意。对于一般情况,我更喜欢方法 A,因为它提高了可读性。
      • 所以不是微观上更快,而是皮下级更快?
      • @Martin 我找到了一篇观点相似的论文,但与 Jens 相似,编译器比 A 有更多机会优化 method B。跨度>
      • @Steve-o:那是因为您使用的是类 C 函数指针。将其转换为使用 C++ 仿函数,编译器应该能够轻松优化它。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-27
      • 1970-01-01
      相关资源
      最近更新 更多