【问题标题】:Why is C slow with function calls?为什么 C 函数调用很慢?
【发布时间】:2010-08-13 21:43:14
【问题描述】:

我是新来的,如果我以错误的方式发帖道歉。

我想知道是否有人可以解释为什么 C 函数调用这么慢?

对关于递归斐波那契的标准问题给出一个肤浅的答案很容易,但如果我尽可能深入地了解“更深层”的原因,我将不胜感激。

谢谢。

Edit1:很抱歉这个错误。我误解了 Wiki 中的一篇文章。

【问题讨论】:

  • 您从哪里得知 C 执行函数调用很慢?
  • 如果 C 语言的函数调用速度很慢,那么哪些语言的函数调用速度很快?
  • 抱歉,我好像误会了什么。我只是想用 Fib 数字尽可能深入地解释整个故事。我在维基百科的递归文章中看到“在支持迭代循环结构的语言(例如 C 和 Java)中,由于管理堆栈所需的开销和相对缓慢,递归程序通常会花费大量的时间和空间成本函数调用;"。
  • 这不是开始,但我们必须更深入。尽可能深...
  • 我认为问题在于,与 C 相比,函数式语言编译器更擅长优化递归函数调用。C 就像任何语言一样,与特定编译器的程序集一样慢(或快)生成。由于函数调用非常特定于平台,因此您不能将函数调用的成本与语言联系起来。然而,函数调用的优化可以绑定到特定的编译器。

标签: c function


【解决方案1】:

当您进行函数调用时,您的程序必须将几个寄存器放在堆栈上,可能会压入更多的东西,并弄乱堆栈指针。这就是“慢”的全部内容。实际上,这相当快。 x86_64 平台上大约 10 条机器指令。

如果你的代码很稀疏并且你的函数很小,那么它会很慢。这就是斐波那契函数的情况。但是,您必须区分“慢速调用”和“慢速算法”:使用递归实现计算斐波那契套件几乎是最慢的直接方法。函数体中涉及的代码几乎与函数序言和尾声(发生推送和弹出的地方)中的代码一样多。

在某些情况下,调用函数实际上会使您的代码总体上更快。当您处理大型函数并且您的寄存器很拥挤时,编译器可能很难决定在哪个寄存器中存储数据。但是,在函数调用中隔离代码将简化编译器决定使用哪个寄存器的任务。

所以,不,C 调用并不慢。

【讨论】:

  • +1。尽管我的书呆子想补充一点,“将东西放在堆栈上”实际上是使函数调用相对变慢的原因,因为这些是额外的内存写入。并且内存写入很慢。但是,如果遇到问题,那么是的,正如您正确指出的那样,该程序的结构很差。天哪,我希望 C 编译器可以做更多更好的代码重构优化。
  • 好的代码生成器不会将东西压入堆栈,如果他们可以避免的话;相反,他们将参数值放在寄存器中,并且只使用一对调用/返回指令。比这更快地“调用”更难。
【解决方案2】:

根据您在评论中发布的附加信息,您似乎感到困惑的是这句话:

"在语言中(例如 C 和 Java) 有利于迭代循环 构造,通常有 巨大的时间和空间成本 与递归程序相关联, 由于管理所需的开销 堆栈和相对缓慢 函数调用;"

在递归实现斐波那契计算的上下文中。

这是说进行递归函数调用比循环慢,但这并不意味着函数调用一般来说很慢或 C 中的函数调用是比其他语言的函数调用慢

斐波那契生成自然是一种递归算法,因此最明显和最自然的实现涉及许多函数调用,但也可以表示为迭代(循环)。

斐波那契数生成算法特别具有称为tail recursion 的特殊属性。尾递归递归函数可以很容易地自动转换为迭代,即使它表示为递归函数。一些语言,特别是递归非常普遍且迭代很少的函数式语言,保证它们会识别这种模式并自动将这种递归转换为“幕后”的迭代。一些优化的 C 编译器也会这样做,但不能保证。在 C 中,由于迭代既常见又惯用,并且由于编译器不一定会为您进行尾递归优化,因此最好将其显式编写为迭代以实现最佳性能。

因此,将这句话解释为对 C 函数调用速度的评论,相对于其他语言,是比较苹果和橘子。其他有问题的语言是那些可以采用某些函数调用模式(恰好发生在斐波那契数生成中)并自动将它们转换为更快的语言,但更快,因为它实际上不是函数调用全部。

【讨论】:

    【解决方案3】:

    C 在函数调用方面并不慢。 调用 C 函数的开销极低。

    我要求你提供证据来支持你的主张。

    【讨论】:

    • 比较函数式语言对递归调用的优化与 c 编译器相比,是的,函数式语言编译器(例如 ML)更有可能提供更好的优化。因此,ML 比递归调用比 C“更快”。但是,在 C 中,您很可能以不同的方式实现递归算法,这样您就不需要进行递归函数调用。
    【解决方案4】:

    对于递归计算斐波那契数等工作,C 可能比其他一些语言慢有几个原因。不过,两者都与慢速函数调用无关。

    在相当多的函数式语言(以及或多或少的函数式风格很常见的语言)中,递归(通常非常深度递归)非常普遍。为了保持合理的速度,这些语言的许多实现都做了大量的工作来优化递归调用(除其他外),尽可能将它们转化为迭代。

    相当多的也“记忆”以前调用的结果——即,它们跟踪最近传递的一些值的函数结果。当/如果再次传递相同的值时,它们可以简单地返回适当的值而无需重新计算。

    然而,应该注意的是,这里的优化并不是真正更快的函数调用——它是避免(通常是很多)函数调用。

    【讨论】:

      【解决方案5】:

      Recursive Fibonacci 是原因,而不是 C 语言。递归斐波那契类似于

      int f(int i)
      {
          return i < 2 ? 1 : f(i-1) + f(i-2);
      }
      

      这是计算斐波那契数最慢的算法,并且通过使用称为函数列表的堆栈存储 -> 使其更慢。

      【讨论】:

      • 好吧,我敢肯定有更慢的,但关键是没有递归 Fib 计算器可以快速处理大量数字,除非它使用记忆(这是作弊)。
      • @Steven Sudit:定义函数以返回一个包含两个连续斐波那契数的结构。这并不是真正的记忆,但会让事情变得更快。
      • @supercat:可爱,但作弊更多,因为这相当于使用特别小的缓存进行记忆。
      • @Steven Sudit:我不这么认为。对递归(返回两个数字)的调用不使用任何先前调用所做的任何计算,也不需要除“n”之外的任何参数。顺便说一句,我最喜欢的“愚蠢”斐波那契例程变体使用了一个子例程,该子例程带有一个参考点作为结果。结果只是随着每次调用而递增;由于计算 F(n) 的函数调用总数为 F(n),增量足以计算答案。
      • @supercat:我意识到我们已经牢牢地处于愚蠢的境地,但是......我将两次返回 fib 称为记忆化示例的原因很简单,对于生成的数字的一半,它不必从头开始,而是可以使用记录的值。因为它只是分数记忆,我希望它会运行得更快但具有相同的算法复杂性,因此它不能被称为“大数快速”。相比之下,有一个答案是 logn!
      【解决方案6】:

      我不确定您所说的“对递归斐波那契标准问题的肤浅回答”是什么意思。

      朴素的递归实现的问题不在于函数调用很慢,而在于您进行了指数级的大量调用。通过缓存结果 (memoization),您可以减少调用次数,让算法在线性时间内运行。

      【讨论】:

        【解决方案7】:

        在所有语言中,C 可能是最快的(除非您是汇编语言程序员)。大多数 C 函数调用是 100% 纯堆栈操作。这意味着当你调用一个函数时,这在你的二进制代码中也是如此,CPU 将你传递给函数的任何参数推送到堆栈上。之后,它调用该函数。然后该函数会弹出您的参数。之后,它会执行构成您的函数的任何代码。最后,任何返回参数都被压入堆栈,然后函数结束并弹出参数。任何 CPU 上的堆栈操作通常都比其他任何东西都快。

        如果您正在使用分析器或某项说明您正在执行的函数调用很慢,那么它必须是您的函数内部的代码。尝试在此处发布您的代码,我们将看看发生了什么。

        【讨论】:

        • Assembly 不一定比 C 代码快。此外,调用约定不止一种,有些使用寄存器来传递参数。这只是支持你所说的:调用函数并不慢。
        • 尤其是我遇到的很多汇编程序员。如今,您将很难(尽管远非不可能)找到一个能比现代编译器优化器做得更好的 asm 专家。
        • 关于 C 的事情是,即使是世界上最好的 C 编译器也只能将代码优化得那么好。我相信,一个优秀的汇编语言程序员可以在 C 中复制一个函数,并使其不仅更小,而且更快。使用今天的处理器,速度上的差异可能不会很大,但每次我认为组装都会提前完成,即使它只是快一点。
        • @Steven Sudit:IA64 AMD64 调用约定使用 8 个 64 位寄存器来传递整数和小型结构以及一些(不记得有多少)SSE 寄存器到传递真实的论点。只有当这还不够或者你传递大于 64 位的复制结构时,堆栈才会用于推送和弹出参数。不用说,这不是很典型。
        • @Steven Sudit 抱歉,它是 6 个寄存器而不是 8 个。除了您提到的那些之外,它们还使用 RDI 和 RSI。此外,他们使用 xmm0 到 xmm7 来传递实际参数。见x86-64.org/documentation/abi-0.99.pdf
        【解决方案8】:

        我不确定你的意思。 C 基本上是 CPU 汇编指令之上的一个抽象层,速度非常快。 你真的应该澄清你的问题。

        【讨论】:

          【解决方案9】:

          在某些语言中,主要是函数式范式,在函数体末尾进行的函数调用可以进行优化,以便重复使用相同的堆栈帧。这可以潜在地节省时间和空间。当函数既短又递归时,好处尤其显着,因此堆栈开销可能会使实际完成的工作相形见绌。

          因此,在这种优化可用的情况下,朴素的斐波那契算法将运行得更快。 C 通常不执行此优化,因此其性能可能会受到影响。

          但是,正如已经说过的,斐波那契数的朴素算法首先是非常低效的。更有效的算法将运行得更快,无论是用 C 还是其他语言。其他斐波那契算法可能不会从所讨论的优化中看到几乎相同的好处。

          因此,简而言之,C 通常不支持某些优化,这些优化可能会在某些情况下导致显着的性能提升,但在大多数情况下,在这些情况下,您可以通过使用算法略有不同。

          【讨论】:

            【解决方案10】:

            我同意 Mark Byers 的观​​点,因为您提到了递归斐波那契。尝试添加一个 printf,这样每次添加时都会打印一条消息。您会看到递归斐波那契正在做更多乍一看可能会出现的添加。

            【讨论】:

              【解决方案11】:

              文章讲的是递归迭代的区别。

              这属于计算机科学中称为算法分析的主题。

              假设我编写了斐波那契函数,它看起来像这样:

              //finds the nth fibonacci
              int rec_fib(n) {
               if(n == 1)
                  return 1;
               else if (n == 2)
                  return 1;
               else 
                 return fib(n-1) + fib(n - 2)
              }
              

              如果你把它写在纸上(我推荐这个),你会看到这个金字塔状的形状出现。

              完成这项工作需要花费大量时间。

              然而,还有另一种写斐波那契的方法(还有其他几种)

              int fib(int n) //this one taken from scriptol.com, since it takes more thought to write it out.
              {
                int first = 0, second = 1;
              
                int tmp;
                while (n--)
                  {
                    tmp = first+second;
                    first = second;
                    second = tmp;
                  }
                return first;
              }
              

              这个只需要与 n 成正比的时间长度,而不是你之前看到的二维生长的大金字塔形状。

              通过算法分析,您可以准确确定这两个函数的运行时间与 n 大小的增长速度。

              另外,一些递归算法很快(或者可以被欺骗变得更快)。这取决于算法——这就是算法分析如此重要和有用的原因。

              这有意义吗?

              【讨论】:

              • 是的。感谢你的回答。事实是我已经解决了递归、迭代和内存记忆的 Fib 问题,但我只是想找出更深层次的东西。当他们问我时,只说“它填满了堆栈”是不好的。会吗? :)
              • @Muggen:挖掘一份报告,引导您完成对朴素递归 fib 实现的算法分析;对迭代实现进行分析——这与 能想到的一样深。
              猜你喜欢
              • 1970-01-01
              • 2013-10-10
              • 1970-01-01
              • 1970-01-01
              • 2014-01-02
              • 2017-05-26
              • 1970-01-01
              • 2022-08-04
              • 2017-06-07
              相关资源
              最近更新 更多