为什么C语言如此快,其他语言为什么没有或不如它快?

235

在收听Stack Overflow播客时,一直不断地谈到“真正的程序员”使用C编写,并且C之所以更快是因为它“靠近机器”。将前一个论点留给另一个帖子,那么C有什么特别之处使其比其他语言更快呢?

换句话说,有什么可以阻止其他语言编译成二进制代码并像C一样快速运行?


7
你能列举一下具体讲了这个的节目吗?我很想听听它。 - Giovanni Galbo
2
非常惊讶这个问题得到的回答有多么糟糕(大多数回答忽略了编译语言和解释语言之间的根本区别等等,我知道JIT等等),以及有多少人在“为他们喜欢的语言辩护”。(FORTRAN男孩需要冷静一下)。 - Tim Ring
1
不要忘记汇编语言。没有比汇编组装的可执行文件更快或更紧凑的了。汇编语言几乎是纯二进制的,因此它是最快的语言,没有任何偏见。 - KKZiomek
6
C是最快的,因为它代表光的速度,还有相对论? - Meet Taraviya
2
当然,C是最快的编程语言是错误的。任何类型的编程语言都无法与FORTH的速度相比。FORTH用于触发核弹,它是大多数卫星上的编程语言,也是国际空间站以及CERN和ITER中的主要编程语言。我正在比较Microsoft C(不同版本)和FORTH之间的速度。对于C来说,真是太无聊了... - Scoobeedo Cool
显示剩余2条评论
34个回答

222

C语言没有太多特别之处,这也是它快速的原因之一。

新的编程语言支持垃圾回收动态类型和其他便于程序员编写程序的功能。问题在于,这会增加额外的处理开销,降低应用程序的性能。C语言没有这些开销,这意味着没有额外负担,但这也意味着程序员需要能够分配内存并释放它们以防止内存泄漏,并且必须处理变量的静态类型。

虽然如此,许多语言和平台(例如具有Java虚拟机的Java和具有公共语言运行时的.NET)随着即时编译等进展而改善了性能,从字节码生成本地机器代码以实现更高的性能。


5
自动内存管理(即垃圾回收)在短周期程序和/或内存富余的情况下可能比手动内存管理更快。垃圾回收允许简单和快速的分配,程序不需要花费时间去回收内存。 - Kornel
2
C程序通常在需要时分配和释放内存。这很低效。一个好的虚拟机将分配和释放大块内存,从而在许多情况下获得大幅性能提升。 - skaffman
71
除了难以实现之外,一段C程序完全可以执行相同的分块分配和垃圾回收。 - ephemient
5
在正确使用并且给予足够关注时,手动管理和明智分配资源将始终优于任何垃圾回收系统。您对自己的使用模式有绝对的了解,而垃圾回收系统则没有。此外,垃圾回收系统会增加额外负担。 - user90843
"处理静态类型" - 这让人觉得静态类型是一种负担。我认为它是一种特性,因为它允许您在运行程序之前检测到许多错误。 - martinkunev
显示剩余4条评论

97

C语言设计者做出了一个权衡。也就是说,他们决定将速度放在安全性之上。C语言不会:

  • 检查数组索引边界
  • 检查未初始化的变量值
  • 检查内存泄漏
  • 检查空指针解除引用

当你索引到一个数组时,在Java中需要进行一些虚拟机方法调用、边界检查和其他合理性检查。这个过程是有效的且完全正确的,因为它在应该有安全性的地方增加了安全性。但是在C语言中,甚至相当琐碎的事情也没有安全保障。例如,C语言不要求memcpy检查要复制的区域是否重叠。它并不是作为编写大型商业应用程序的语言而设计的。

但是,这些设计决策并不是C语言的缺陷。它们是经过设计的,因为它允许编译器和库编写者从计算机中获得每一位性能。下面是C语言精神的描述,C语言基本原理文件C Rationale中解释了这一点:

C代码可能不可移植。虽然它努力为程序员提供编写真正可移植程序的机会,但委员会并不想强制程序员编写可移植程序,以避免使用C作为“高级汇编语言”:编写特定于机器的代码是C的优点之一。

保持C的精神。委员会将保存传统C语言精神作为主要目标之一。C精神有许多方面,但其本质是C语言基于的基本原则的社区情感。 C精神的某些方面可以用以下短语概括:

  • 相信程序员。
  • 不要阻止程序员做必须做的事情。
  • 保持语言小而简单。
  • 只提供执行操作的一种方式。
  • 即使不能保证可移植性,也要使其快速。
  • 最后这句谚语需要一点解释。高效代码生成的潜力是 C 语言最重要的优势之一。为了确保不会因为看似非常简单的操作而导致代码爆炸,许多操作被定义为按目标机器硬件处理而不是按一般抽象规则处理。例如,在扩展用于表达式的 char 对象的规则中,可以看到愿意接受计算机所做的事情的规则:char 对象的值扩展为有符号或无符号量通常取决于在目标机器上哪个字节操作更有效率。


    65
    C语言通过暴力崩溃的方式检查空指针引用,有时还会通过破坏堆栈帧和数据来检查数组越界和未初始化变量。不幸的是,这些检查是在运行时进行的。 - paxdiablo
    26
    我不会说 C 不安全,这听起来像是你在暗示什么。它假设你不是白痴。如果你把枪对着自己的脚射击,C 会愉快地满足你的要求,因为它认为你比它更聪明。这并不一定是坏事。 - Bob Somers
    26
    @Bob:确切地说,说 C 不安全是因为它让你做危险的事情,就像说汽车不安全是因为它会让你开到悬崖边上一样。C 的安全性取决于驾驶者(但有很多不安全的驾驶者存在)。 - Robert Gamble
    9
    鲍勃,制造缓冲区溢出等 bug 并不意味着你是个白痴,这只是说明你还是人类。我意识到 C 和 C++ 并不是“坏”的(我非常喜欢它们)。 - Johannes Schaub - litb
    7
    C语言非常适合大规模应用程序开发。如果一个人发现自己无法开发比“hello world”更大的项目,那么问题在于程序员本身,而不是语言问题... - user529758
    显示剩余16条评论

    81
    如果你花了一个月的时间用C构建一个0.05秒运行的东西,而我只花了一天用Java写同样的东西,但它需要0.10秒才能运行,那么C真的更快吗?
    但是回答你的问题,良好编写的C代码通常比其他语言中编写良好的代码运行得更快,因为编写C代码“良好”包括在接近机器级别上进行手动优化。
    尽管编译器确实非常聪明,但它们还无法创造性地提出与手动调整算法(假设“双手”属于一个优秀的C程序员)相竞争的代码。
    编辑:
    很多评论都是这样的:“我用C写作,我不考虑优化。”
    但以这个例子从这篇文章来说:
    在Delphi中,我可以这样写:
    function RemoveAllAFromB(a, b: string): string;
    var
      before, after :string;
    begin
      Result := b;
      if 0 < Pos(a,b) then begin
        before := Copy(b,1,Pos(a,b)-Length(a));
        after := Copy(b,Pos(a,b)+Length(a),Length(b));
        Result := before + after;
        Result := RemoveAllAFromB(a,Result);  //recursive
      end;
    end;
    

    在 C 中我写下了这段代码:

    char *s1, *s2, *result; /* original strings and the result string */
    int len1, len2; /* lengths of the strings */
    for (i = 0; i < len1; i++) {
       for (j = 0; j < len2; j++) {
         if (s1[i] == s2[j]) {
           break;
         }
       }
       if (j == len2) {  /* s1[i] is not found in s2 */
         *result = s1[i]; 
         result++; /* assuming your result array is long enough */
       }
    }
    
    但是C语言版本中有多少个优化呢?我们在实现过程中做出了许多关于实现的决策,这些决策在Delphi版本中我并没有考虑。字符串是如何实现的呢?在Delphi中我看不到它。在C语言中,我决定它将是指向ASCII整数数组的指针,我们称之为字符。在C语言中,我们逐个测试字符是否存在。在Delphi中,我使用Pos函数。
    这只是一个小例子。在大型程序中,C语言程序员必须在每几行代码中进行这些低级决策。这累积起来就是一个手工制作、手工优化的可执行文件。

    55
    然而,公平地说,在C语言中需要一个月完成的事情,在Java中也不太可能只需要一天就能完成,即使是一个只需0.05秒执行时间的小程序。 - dreamlax
    21
    我已经用C语言编程多年了,几乎从不需要进行你提到的任何优化。我已经将许多程序从Perl移植到C,通常会看到10倍以上的速度提升和显著的内存使用减少,而无需手动编写任何优化代码。 - Robert Gamble
    4
    我创建了一些C++程序,可以在比Java或.NET程序启动时间更短的时间内处理数千行的数据。这是我对于现代语言的一种沮丧感。C语言非常适合那些需要最小运行时要求的精简程序。PowerBasic也同样适用。 - bruceatk
    48
    你是在说一个用C语言编写需要一个月完成的程序,比用Java语言编写只需一天时间的程序快上两倍,这样做没有意义吗?如果这个程序每天需要运行500,000,000次以上呢?速度提升两倍的意义非常重大。如果它在成千上万甚至百万台CPU上运行,那么为了实现双倍性能所花费的额外一个月的开发成本将是巨大的节省。基本上,在选择开发平台之前,你必须知道/理解你的部署规模。 - nicerobot
    1
    即使考虑到你所说的时间上的真实性,如果你的应用程序极其时间敏感,例如在GPS系统中每纳秒都很重要,我认为你需要花一个月的时间用C语言编写代码。 - ITguy
    显示剩余8条评论

    55

    我没有看到这句话,所以我会说:C语言往往更快,因为几乎所有其他语言都是用C语言编写的。

    Java是基于C语言构建的,Python是基于C(或Java、.NET等)构建的,Perl也是如此。操作系统是用C语言编写的,虚拟机是用C语言编写的,编译器是用C语言编写的,解释器是用C语言编写的。有些东西仍然是用汇编语言编写的,这往往更快。越来越多的东西正在用其他东西编写,这个东西本身就是用C语言编写的。

    你在其他语言(不是汇编语言)中编写的每个语句通常在底层实现时都被实现为几个C语句,并编译成本地机器码。由于这些其他语言往往存在的目的是为了获得比C语言更高级别的抽象,因此在C语言中需要额外的语句集中在添加安全性、添加复杂性和提供错误处理上。这些通常是好事,但它们有一个代价,它的名字是速度大小

    个人而言,我已经用过几十种语言,涵盖了大部分可用的范围,我个人一直在寻找你所提到的魔法:

    我怎么能做到两全其美?我怎么能在我喜欢的语言中玩弄高级别的抽象,然后为了速度降到C语言的细节层次?

    经过几年的研究,我的答案是Python(基于C语言)。你可能想看看。顺便说一句,你也可以从Python降到汇编语言(需要一些特殊库的帮助)。

    另一方面,任何语言都可以编写糟糕的代码。因此,C(或汇编)代码并不自动更快。同样,一些优化技巧可以使高级语言代码的部分接近原始C代码的性能水平。但是对于大多数应用程序来说,程序在等待人或硬件时花费了大部分时间,所以这种差异确实无关紧要。

    享受吧。


    11
    这并不适用于 JIT-编译语言。我的 C# 并不像被编译成 IL 再翻译成 C 再编译成机器码那样。相反,IL 是 JIT 编译的 - 在这一点上,JIT 的实现语言是无关紧要的。它只是产生机器码。 - Jon Skeet
    4
    天佑我不敢质疑传奇人物Jon Skeet,但似乎机器码生成的是C#而不是C,这完全是相关的,因为C#是"高级"语言,具有更多功能和安全检查,所以速度比与之相当的C要慢。 - Rob Williams
    3
    @Jon:我本来也想说类似的话,但实际上这个观点有一定的道理,因为很多.NET库的核心组件实际上是用C编写的,因此具有C语言的速度限制。看看未来这将如何改变将是很有趣的。 - Konrad Rudolph
    1
    这似乎是错误的做法,其他语言的编译器/解释器/虚拟机经常(但并非总是)使用C语言编写(或至少用于最底层),因为C语言非常快(在许多情况下是最快的)。 - Roman A. Taycher
    3
    这个答案是不正确的。正如上面指出的,它不适用于JIT语言,也不适用于具有自己的编译器的语言(如果对它们进行了非凡的努力,它们可以生成比现代C编译器更快的代码)。剩下的唯一另一类语言是解释性语言,它们之所以不比C慢仅仅是因为它们自己是用C编写的,而是因为解释的开销无论如何都很大,即使解释器是用汇编语言编写的。 - Score_Under
    显示剩余4条评论

    39

    这里有很多问题,大部分我都没有资格回答。但对于最后一个问题:

    其他语言为什么不能编译成二进制并且与C一样快?

    简单来说,是抽象层级

    C只有一到两个抽象层级远离机器语言。Java和.NET语言至少有三个抽象层级远离汇编语言。Python和Ruby的情况我不太确定。

    通常,程序员使用的越多的玩具(比如复杂的数据类型等),你距离机器语言就越远,需要进行更多翻译。

    我的回答可能有些偏差,但基本意思就是这样。

    这篇文章有一些很好的评论,提供了更多细节信息。


    3
    从技术上讲,Java和.Net与它们运行的机器语言被无限抽象化。它们在虚拟机中运行。即使使用JIT,原始代码也需要进行大量修改才能得到类似本地机器代码的东西。 - jmucchiello
    1
    .NET代码无法在虚拟机中运行。它会作为本地指令在其所运行的处理器平台上运行(32位x86、64位x86或IA64)。 - Robert C. Barth
    11
    .NET确实使用虚拟机。.NET代码被编译成字节码,由虚拟机执行。虚拟机将字节码转换为本地指令并在运行时执行。 - Robert Gamble
    3
    非常重要的一点是要注意,Java和其他面向对象的语言抽象已经影响了处理器指令集。较新的处理器具有使Java运行更快的指令,如果Java虚拟机知道这些优化并使用它们,那么就可以发挥作用。这不是很大,但是确实有帮助。 - Adam Davis
    1
    也许是ThumbEE http://zh.wikipedia.org/wiki/ARM架构#ThumbEE - Roman A. Taycher
    显示剩余5条评论

    37
    这并不是说C语言速度快,而是因为C的成本模型是透明的。如果一个C程序很慢,那么它就会以明显的方式变慢:通过执行许多语句。与C中操作的成本相比,对象(特别是反射)或字符串的高级操作可能具有不明显的成本。
    两种通常编译成二进制文件与C一样快的语言是标准ML(使用MLton编译器)和Objective Caml。如果你查看benchmarks game,你会发现对于某些基准测试(如二叉树),OCaml版本比C更快。(我没有找到任何MLton条目。)但不要太认真地看待shootout;正如它所说,这只是一个游戏,结果通常反映了人们在调整代码方面付出了多少努力。

    1
    在任何语言中,编写不明显的高昂代码都是可能的。只是在某些语言中,您首先必须编写Lisp或Forth的内部变体... - Donal Fellows
    Rust 在基准测试中与 C 相匹配。 - stark

    20
    C并不总是更快。
    C比现代Fortran慢,比如说。
    C在某些方面通常比Java慢(特别是在JIT编译器对代码进行优化之后)。
    C允许指针别名发生,这意味着一些好的优化是不可能的。特别是当你有多个执行单元时,这会导致数据获取停顿。哎呀。
    假设指针算术运算确实会导致某些CPU系列的性能下降(特别是PIC!在分段x86上曾经非常糟糕)。
    基本上,当你使用矢量单元或并行编译器时,C表现糟糕,而现代Fortran运行得更快。
    C程序员的技巧,比如thunking(即时修改可执行文件),会导致CPU预取停顿。
    你明白我的意思吗?
    我们的好朋友,x86,执行的指令集与实际的CPU架构关系不大。阴影寄存器、加载存储优化器,全部在CPU中。所以C语言接近虚拟金属。真正的金属,英特尔不让你看见。(历史上,VLIW CPU有点失败,所以也许这样也不错。)
    如果你在高性能DSP上用C语言编程(也许是TI DSP?),编译器必须做一些巧妙的处理,将C语言展开到多个并行执行单元上。所以在这种情况下,C语言不接近硬件,但它接近编译器,编译器会进行整个程序优化。很奇怪。
    最后,一些CPU(比如JOP项目7)在硬件上运行Java字节码,嗯,就是在门阵列上。

    1
    Thunking 在 C 语言中的最后编写时间是什么时候?现代 x86 是一个大多数采用 RISC 设计的接口,但这与 VLIW 关系不大... - Calyth
    8
    你的帖子很大程度上忽略了C99的存在。此外,许多C/C++编译器作为扩展提供了C99的restrict关键字(确保没有指针别名)。 - Evan Teran
    我假设每个人都在遵循/过渡到遵循CWE/SANS前25名,并避免在C中进行新设计。因此,没有绿地C,所以几乎没有C99。 - Tim Williscroft
    2
    你能举个例子说明在什么情况下C语言比现代Fortenberry慢吗? - Adam
    Ajile链接已经(有效地)失效:“Ajile.com域名可能正在出售。” - Peter Mortensen
    显示剩余2条评论

    12

    其他语言为什么不能编译成与C一样快的二进制文件呢?

    没有什么可以阻止这种情况的发生。现代语言如Java或.NET语言更注重程序员的生产力而非性能。硬件现在很便宜。同时,编译到中间表示还带来了很多好处,例如安全性、可移植性等。.NET CLR可以利用不同的硬件。例如,您不需要手动优化/重新编译程序以使用SSE指令集。


    我认为这里的可移植性是有争议的。如果你想要真正可移植的代码,你会用C语言编写而不是其他任何语言。我们的代码可以在大约25个操作系统上运行。从DOS和ThreadX开始,到Linux/XP结束,你能找到另一种语言能做到吗 :) - Ilya
    1
    @Ilya 我不同意。在C语言中编写非可移植代码同样容易。看看有些人将代码移植到64位是多么痛苦。 如果你有正确的字节码解释器,字节码语言可以跨平台工作。 - Calyth
    1
    @IIya,可移植的 C 代码是例外而不是规则,我曾在不同的硬件/软件平台之间移植过 C 代码,知道这是一场噩梦。 - aku
    即使对于PC Word也不是这样的。实际上,大多数跨平台应用程序都是用C/C++编写的,Java稍微有一点。对于嵌入式低级开发来说,没有其他选择。C语言是事实上最具可移植性的语言。 - Ilya
    WinForms(Windows,*NIX,MacOS):) 不幸的是无法在某些移动操作系统中使用。 - aku
    显示剩余10条评论

    11

    我猜你忘记了汇编语言也是一种语言 :)

    但说真的,只有当程序员知道自己在做什么时,C程序才会更快。你可以很容易地编写一个C程序,其运行速度比使用其他语言编写的执行相同任务的程序还慢。

    C语言之所以更快,是因为它是设计成这样的。它允许你进行很多“低级别”的操作,从而帮助编译器优化代码。或者说,由你作为程序员来负责优化代码。但这通常非常棘手和容易出错。

    其他语言,如先前提到的其他语言,更注重程序员的生产率。普遍认为程序员的时间比机器时间(即使在旧时代)要昂贵得多。因此,尽量减少程序员花费在编写和调试程序上的时间而不是程序的运行时间,这是非常有意义的。为了实现这个目标,你将牺牲一些可以使程序更快的内容,因为很多事情都被自动化了。


    4
    尽管你曾经用C语言和汇编分别写过程序,但由于编译器比你更聪明,所以C版本很可能会更快。 - mk12

    9
    主要因素是它是一种静态类型语言,并且编译成机器代码。此外,由于它是一种低级语言,通常不会执行你没有命令的任何操作。
    以下是其他一些因素:
    - 变量不会自动初始化 - 数组没有边界检查 - 指针操作不受检查 - 没有整数溢出检查 - 静态类型变量 - 函数调用是静态的(除非使用函数指针) - 编译器编写者有足够的时间来改进优化代码。此外,人们编写C程序是为了获得最佳性能,因此需要对代码进行优化。 - 语言规范的某些部分是实现定义的,因此编译器可以以最优的方式处理事情。
    大多数静态类型语言的编译速度都可以与C一样快或更快,特别是如果它们可以做出C不能做出的指针别名等假设的话。

    C 低级语言?我想现在这是一个相对的意思,与 Java 相比是的,但与汇编相比不是。很好的帖子,让我思考了。 - Mark
    你说得对,它确实是相对的。我的意思是它“接近机器”,不能帮助你做像内存管理或跟踪数组大小这样的事情。 - Matthew Crumley
    2
    C是一种低级语言。C一直都是一种低级语言。你可以很容易地将C代码手动翻译成汇编语言。 - Robert C. Barth
    4
    @Robert:C语言曾经被认为是一种高级语言,因为与汇编语言(非常常见)相比,它确实是。但与今天大多数使用的语言相比,它被认为是一种低级语言。 - Robert Gamble
    说实话,这是一个非常有偏见的答案。几乎所有的C程序员都会进行边界检查等操作。然而,C仍然比C++快得多。 - MarcusJ

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接