代码之家  ›  专栏  ›  技术社区  ›  drigoSkalWalker

函数指针会使程序变慢吗?

  •  50
  • drigoSkalWalker  · 技术社区  · 15 年前

    每个人都说这会让我的程序运行缓慢。

    我做了个程序来检查。 我对这两个案子的结果都一样(测量时间。)

    提前谢谢。

    为了回应一些人。 这样地:

    int end = 1000;
    int i = 0;
    
    while (i < end) {
     fp = func;
     fp ();
    }
    

    当你执行这个的时候,我得到的时间和我执行这个的时间是一样的。

    while (i < end) {
     func ();
    }
    

    而且它不会像很多人说的那样让程序运行缓慢。

    8 回复  |  直到 15 年前
        1
  •  92
  •   AnT stands with Russia    15 年前

    你看,从性能的角度来看,在一些实际情况下,比如在一个循环中多次重复调用函数,性能可能一点也不不同。

    对于习惯于将C代码看作是由抽象C机器执行的东西的人来说,这听起来可能很奇怪,因为抽象C机器的“机器语言”与C语言本身非常相似。在这种情况下,“默认情况下”对函数的间接调用确实比直接调用慢,因为它正式涉及到额外的内存访问以确定调用的目标。

    然而,在现实生活中,代码是由一台真正的机器执行的,并由一个优化编译器编译,该编译器对底层机器体系结构有很好的了解,这有助于它为特定的机器生成最优化的代码。在许多平台上,从一个循环执行函数调用的最有效方法实际上会导致 直接调用和间接调用的代码,导致两者的性能相同。

    例如,考虑x86平台。如果我们“字面上”将直接和间接调用转换成机器代码,我们可能会得到这样的结果

    // Direct call
    do-it-many-times
      call 0x12345678
    
    // Indirect call
    do-it-many-times
      call dword ptr [0x67890ABC]
    

    前者在机器指令中使用一个立即数操作数,而且通常比后者快,后者必须从某个独立的内存位置读取数据。

    call 指示它提供了一个 登记 . 这种格式非常重要的一点是 . 这对我们意味着什么?这意味着一个好的优化编译器必须并且将利用这一事实。为了实现上述循环,编译器将尝试通过中的寄存器使用调用 二者都

    // Direct call
    
    mov eax, 0x12345678
    
    do-it-many-times
      call eax
    
    // Indirect call
    
    mov eax, dword ptr [0x67890ABC]
    
    do-it-many-times
      call eax
    

    请注意,现在重要的部分—循环体中的实际调用—在这两种情况下完全相同。不用说,演出会很精彩 .

    甚至有人可能会说,不管听起来有多么奇怪,在这个平台上,直接调用(在 呼叫 更慢的 登记 (与存储在内存中相反)。

    当然,整个事情并不像一般情况下那么容易。编译器必须处理寄存器的有限可用性,别名问题等等,但是像你的例子中那样简单的情况(甚至在更复杂的情况下),上面的优化将由一个好的编译器执行,并且将完全消除循环直接调用和循环间接调用之间的任何性能差异。这种优化在调用C++函数时特别适用,因为在一个典型的实现中,所涉及的指针完全由编译器控制,使其完全了解混叠图像和其他相关的东西。

    当然,总是有一个问题,你的编译器是否足够聪明,以优化这样的事情。。。

        2
  •  26
  •   Tyler McHenry    15 年前

    我认为当人们这么说的时候,他们指的是这样一个事实:使用函数指针可能会阻止编译器优化(内联)和处理器优化(分支预测)。然而,如果函数指针是一种有效的方法来完成您正试图做的事情,那么任何其他方法都可能有同样的缺点。

    而且,除非函数指针在性能关键型应用程序或速度非常慢的嵌入式系统的紧密循环中使用,否则这种差异很可能是可以忽略不计的。

        3
  •  10
  •   Beanz    15 年前

    在c/c++api中,最常见的函数指针是回调函数。如此多的api这样做的原因是,编写一个在事件发生时调用函数指针的系统要比其他方法(如消息传递)有效得多。我个人也使用函数指针作为更复杂的输入处理系统的一部分,其中键盘上的每个键都有一个通过跳转表映射到它的函数指针。这允许我从输入系统中删除任何分支或逻辑,只需处理输入的按键。

        4
  •  10
  •   hlovdal    15 年前

    每个人都说这会让我

    这种说法很可能是错误的。首先,如果使用函数指针的替代方法是

    if (condition1) {
            func1();
    } else if (condition2)
            func2();
    } else if (condition3)
            func3();
    } else {
            func4();
    }
    

    相当地 比只使用一个函数指针慢得多。虽然通过指针调用函数确实有一些(通常可以忽略的)开销,但通常情况下,比较直接函数调用和通过指针调用的区别是不相关的。

    不可能的 )要知道,有时这可能是非常非直观的(例如,linux内核开发人员已经开始删除 inline

        5
  •  8
  •   Péter Török    15 年前

    通过函数指针调用函数是

    使用函数指针是因为它们可以使程序更简单、更干净、更易于维护(当然,如果使用得当)。这不仅弥补了可能非常小的速度差异。

        6
  •  7
  •   Yacoby    15 年前

    如果需要使用函数指针,请使用它们,因为任何尝试执行相同操作但避免使用它们的操作都会比使用函数指针更慢,更不易维护。

        7
  •  7
  •   user267027    15 年前

    不过,看看cqsort比较函数。因为比较函数不能内联并且需要遵循基于堆栈的标准调用约定,所以排序的总运行时间可以是 (更确切地说是3-10倍)对于整数键来说,比直接的内联调用的相同代码慢。

        8
  •  1
  •   Praxeolitic    7 年前

    答案取决于函数指针的用途,因此也取决于替代方法。如果函数指针被用来实现一个选项,而这个选项是我们的程序逻辑的一部分,并且不能简单地删除,那么将函数指针调用与直接函数调用进行比较是一种误导。尽管如此,我还是会继续进行比较,然后再回到这个想法。

    与直接函数调用相比,函数指针调用在禁止内联时最有可能降低性能。因为内联是一种网关优化,我们可以在函数指针比等效的直接函数调用任意慢的情况下:

    void foo(int* x) {
        *x = 0;
    }
    
    void (*foo_ptr)(int*) = foo;
    
    int call_foo(int *p, int size) {
        int r = 0;
        for (int i = 0; i != size; ++i)
            r += p[i];
        foo(&r);
        return r;
    }
    
    int call_foo_ptr(int *p, int size) {
        int r = 0;
        for (int i = 0; i != size; ++i)
            r += p[i];
        foo_ptr(&r);
        return r;
    }
    

    Code generated 对于 call_foo() :

    call_foo(int*, int):
      xor eax, eax
      ret
    

    美好的 foo() 不仅是内联的,而且这样做允许编译器消除前面的整个循环!生成的代码只是通过将寄存器与自身异或,然后返回,从而将返回寄存器归零。另一方面,编译器必须为中的循环生成代码 call_foo_ptr() (GCC7.3中有100多行代码),而大多数代码实际上什么都不做(只要 foo_ptr 仍然指向 ). (在更典型的场景中,您可以预期将一个小函数内联到一个热的内部循环中可能会将执行时间减少大约一个数量级。)

    食物ptr const ,那么 会产生相同的代码。然而,这就要求我们放弃法律所提供的间接的机会 . 对你来说“公平”吗 食物ptr 成为 ? 如果我们对 食物ptr ,则为否,但如果是这种情况,则直接函数调用也不是有效的选项。