为什么C在函数调用方面很慢?

为什么C在函数调用方面很慢?,c,function,C,Function,我是新来的,所以如果我以错误的方式发了这篇文章,我深表歉意 我想知道是否有人能解释为什么C在函数调用方面如此缓慢 对于递归斐波那契的标准问题,很容易给出一个浅显的答案,但如果我能尽可能深入地了解更深层次的原因,我将不胜感激 谢谢 伊迪丝1:对不起,弄错了。我误解了Wiki上的一篇文章。C在函数调用方面并不慢。 调用C函数的开销非常低 我要求你提供证据来支持你的主张。我不知道你的意思。C基本上是CPU汇编指令之上的一个抽象层,速度非常快。 你真的应该澄清你的问题。我不知道你对递归斐波那契标准问题的

我是新来的,所以如果我以错误的方式发了这篇文章,我深表歉意

我想知道是否有人能解释为什么C在函数调用方面如此缓慢

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

谢谢


伊迪丝1:对不起,弄错了。我误解了Wiki上的一篇文章。

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


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

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

我不知道你对递归斐波那契标准问题的肤浅回答是什么意思


朴素递归实现的问题不是函数调用太慢,而是调用的次数成倍增加。通过缓存结果,您可以减少调用次数,从而允许算法在线性时间内运行。

当您进行函数调用时,您的程序必须在堆栈上放置多个寄存器,可能会推送更多内容,并弄乱堆栈指针。这就是所有可以慢下来的东西。这实际上相当快。x86_64平台上大约有10条机器指令

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

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

因此,不,C调用并不慢。

原因是递归斐波那契,而不是C语言。递归斐波那契有点像

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

这是计算斐波那契数最慢的算法,通过使用名为functions list->的堆栈存储使其变慢。

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


如果您使用的是探查器,或者说您正在进行的函数调用很慢,那么它必须是函数中的代码。尝试在此处发布您的代码,我们将看到发生了什么。

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

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

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

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

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

斐波那契数生成算法有一个特殊的特性,称为。尾部递归函数可以轻松地自动转换为迭代,即使它被表示为递归函数。一些语言,特别是递归非常常见而迭代很少的函数式语言,保证它们能够识别这种模式,并自动将这种递归转换为隐藏的迭代。一些优化C编译器也会做到这一点,但不能保证。在C语言中,由于迭代既常见又惯用,而且编译器不一定会为您进行尾部递归优化,所以它是 一个更好的方法是将其显式地编写为一个迭代,以实现最佳性能


因此,将这句话解释为对C函数调用速度的评论,相对于其他语言而言,是在拿苹果和桔子做比较。其他有问题的语言是那些能够接受Fibbonaci数字生成过程中发生的某些函数调用模式的语言,并自动将它们转换为更快的语言,但速度更快,因为它实际上根本不是函数调用。

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

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

但是,如前所述,计算斐波那契数的朴素算法一开始效率极低。一个更高效的算法将运行得更快,无论是用C语言还是其他语言。其他斐波那契算法可能不会从所讨论的优化中看到几乎相同的好处


简而言之,C通常不支持某些优化,这些优化在某些情况下可能会带来显著的性能提升,但在大多数情况下,您可以通过使用稍微不同的算法实现同等或更大的性能提升。

我同意Mark Byers的观点,因为你提到了递归斐波那契。尝试添加printf,以便每次添加时都打印一条消息。您将看到递归斐波那契所做的加法比乍一看要多得多。

本文讨论的是递归和迭代之间的区别

这是题为计算机科学中的算法分析

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

//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的大小精确确定增长速度

此外,一些递归算法是快速的,或者可能被欺骗而变得更快。这取决于算法-这就是为什么算法分析是重要和有用的


这有意义吗?

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

在相当多的函数式语言和或多或少具有函数式风格的语言中,递归通常非常常见。为了保持合理的速度,这些语言的许多实现都做了大量的工作来优化递归调用,以便在可能的情况下将它们转换为迭代

相当多的函数也会记录以前调用的结果,例如,它们会跟踪函数最近传递的许多值的结果。当/如果再次传递相同的值,它们可以简单地返回适当的值,而无需重新计算


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

你从哪里知道C在函数调用方面很慢?如果C在函数调用方面很慢,哪些语言在函数调用方面很快?很抱歉,我似乎误解了一些东西。我只是想用谎言来尽可能深入地解释整个故事。我在wikipedia的递归文章中看到,在支持迭代循环构造的C和Java等语言中,由于管理堆栈所需的开销和函数调用的相对缓慢,递归程序通常会产生大量的时间和空间开销;。这不是《盗梦空间》,但我们必须更深入。尽可能深入…我认为问题在于函数式语言编译器比C语言更擅长优化递归函数调用。C语言和任何语言一样,与特定编译器生成的程序集一样慢或快。由于函数调用非常特定于平台,因此不能将函数调用的成本与语言挂钩。但是,函数调用的优化可以与
特定编译器。汇编不一定比C代码快。此外,调用约定不止一个,有些使用寄存器传递参数。这正好支持你所说的:调用函数并不慢,特别是我遇到的许多汇编程序员。你会感到压力重重,尽管现在要找到一个比现代编译器优化器做得更好的asm人绝非不可能。关于C,即使是世界上最好的C编译器也只能优化这么好的代码。我相信,一个好的汇编语言程序员可以用C语言复制一个函数,使它不仅更小,而且更快。对于今天的处理器来说,速度可能不会有太大的差异,但我认为汇编每次都会领先一步,即使只是快了一小部分。@Steven Sudit:IA64 AMD64调用约定使用8个64位寄存器来传递整数和小结构,一些不记得有多少SSE寄存器来传递实际参数。只有在这还不够的情况下,或者通过大于64位的复制结构传递时,堆栈才会用于推送和弹出参数。不用说,这不是很典型。@Steven Sudit对不起,是6个寄存器而不是8个寄存器。除了你提到的那些,他们还使用RDI和RSI。此外,它们使用xmm0到xmm7传递实数参数。好吧,我肯定会有速度较慢的,但关键是递归Fib计算器对大数的运算速度永远都不会很快,除非它使用了作弊的记忆。@Steven Sudit:定义函数以返回包含两个连续斐波那契数的结构。这并不是真正的记忆化,但会让事情变得更快。@supercat:很可爱,但作弊更厉害,因为这相当于一个特别小的缓存的记忆化。@Steven Sudit:我不这么认为。对递归返回两个数字FIBINACI例程的任何调用都不会使用以前任何调用所做的任何计算,也不需要“n”以外的任何参数。顺便说一句,我最喜欢的“愚蠢的”斐波那契例程的变体使用了一个子例程,其中包含一个按引用点作为结果。每次调用的结果都会简单地递增;因为计算Fn的函数调用总数是Fn,所以增量就足以计算出答案。@supercat:我知道我们确实处于愚蠢的境地,但是。。。我把双返回fib称为记忆化的一个例子,原因很简单,对于生成的一半数字,它不必从头开始,而是使用记录的值。因为它只是部分记忆,我希望它会运行得更快,但算法复杂度相同,因此它不能满足大数字的速度要求。相比之下,有一个答案是logn!是的。谢谢你的回答。事实上,我已经用递归、迭代和记忆方法解决了Fib问题,但我只想找出更深层的东西。当他们问我的时候,仅仅说它填满了书堆是不好的。会吗@Muggen:挖掘一份报告,指导您完成幼稚递归fib实现的算法分析;对迭代实现做一个分析——这是我能想到的最深入的部分。与c编译器相比,将函数式语言所做的优化与递归调用进行比较。是的,函数式语言编译器(如ML)更有可能提供更好的优化。因此,ML比C的递归调用快。然而,在C中,您很可能可以用不同的方式实现递归算法,这样您就不需要进行递归函数调用。虽然我的书呆子想补充一点,把东西放在堆栈上实际上是使函数调用相对缓慢的原因,因为这些都是额外的内存写入。而且内存写入速度很慢。但是如果有人遇到了问题,那么是的,正如你正确指出的那样,这个程序的结构很差。天哪,我真希望C编译器能做更多更好的代码重构优化。好的代码生成器如果能够避免,就不会把东西推到堆栈中;相反,它们将参数值放在寄存器中,只使用一对调用/返回指令。打电话比打电话快很多。