我从事编程的那段时间,听说C和C是速度标准。最快中的最快,直接编译为汇编代码,速度上没有什么可以与 C 或 C 竞争。而且,似乎没有人挑战这个共同信念。
显然,数字算术运算在 C 中的运行速度必须比在任何其他语言中快得多。但他们有吗?
前段时间,我决定为许多不同的语言编写一组简单的基准测试,看看速度到底有多大差异。
想法很简单:从零开始,使用直接计算来找到十亿个整数的总和。一些编译器(例如rustc)用公式表达式替换这种简单的循环,当然,公式表达式将在恒定的时间内进行计算。用这样的编译器来避免这种情况。我在数字成本运算中使用了类似的方法,例如按位或。
得到结果后,我非常惊讶。我的世界观发生了翻天覆地的变化,我不得不重新考虑我所知道的关于编程语言速度的一切。
您可以在下表中看到我的结果:
Linux 64 位,1.1 GHz CPU,4GB RAM
Language | compiler/version/args | time |
---|---|---|
Rust (bitwise or instead of ) | rustc 1.75.0 with -O3 | 167 ms |
C | gcc 11.4.0 with -O3 | 335 ms |
NASM | 2.15.05 | 339 ms |
Go | 1.18.1 | 340 ms |
Java | 17.0.13 | 345 ms |
Common Lisp | SBCL 2.1.11 | 1 sec |
Python 3 | pypy 3.8.13 | 1.6 sec |
Clojure | 1.10.2 | 9 sec |
Python 3 | cpython 3.10.12 | 26 sec |
Ruby | 3.0.2p107 | 38 sec |
您可以在这里找到所有测试来源:
https://github.com/Taqmuraz/speed-table
因此,正如我们所看到的,C 并不比 Java 快很多,差异约为 3%。此外,我们还发现其他编译语言的算术运算性能与 C 非常接近(Rust 甚至更快)。使用 JIT 编译器 编译的动态语言显示出更糟糕的结果 - 主要是因为算术运算被包装到动态分派的函数中。
解释型动态语言没有 JIT 编译器表现出最差的性能,这并不奇怪。
在那次惨败之后,C 粉丝会说 C 中的内存分配要快得多,因为你直接从系统分配它,而不是要求 GC。
现在和之后我将使用 GC 术语作为 垃圾收集器 和 托管堆,具体取决于上下文。
那么,为什么人们认为 GC 这么慢?事实上,GC已经预先分配了内存,分配就是简单地将指针向右移动。大多数情况下 GC 使用系统调用将分配的内存填充为零,类似于 C 中的 memset,因此需要 恒定时间。而 C 中的内存分配需要不确定的时间,因为它取决于系统和已经分配的内存。
但是,即使考虑到这些知识,我也无法期望 Java 能取得如此好的结果,您可以在下表中看到:
1.1 GHz 2 cores, 4 GB RAM |
Running tests on single thread. |
Result format : "Xms-Yms ~Z ms" means tests took from X to Y milliseconds, and Z milliseconds in average |
integers array size | times | Java 17.0.13 new[] | C gcc 11.4.0 malloc | Common Lisp SBCL 2.1.11 make-array |
---|---|---|---|---|
16 | 10000 | 0-1ms, ~0.9ms | 1-2ms, ~1.2ms | 0-4ms, ~0.73ms |
32 | 10000 | 1-3ms, ~1.7ms | 1-3ms, ~1.7ms | 0-8ms, ~2.ms |
1024 | 10000 | 6-26ms, ~12ms | 21-46ms, ~26ms | 12-40ms, ~7ms |
2048 | 10000 | 9-53ms, ~22ms | 24-52ms, ~28ms | 12-40ms, ~19ms |
16 | 100000 | 0-9ms, ~2ms | 6-23ms, ~9ms | 4-24ms, ~7ms |
32 | 100000 | 0-14ms, ~3ms | 10-15ms, ~11ms | 3-8ms, ~7ms |
1024 | 100000 | 0-113ms, ~16ms | 234-1156ms, ~654ms | 147-183ms, ~155ms |
2048 | 100000 | 0-223ms, ~26ms | 216-1376ms, ~568ms | 299-339ms, ~307ms |
how many instances | Java 17.0.3 new Person(n) | C g 11.4.0 new Person(n) |
---|---|---|
100000 | 0-6ms, ~1.3ms | 4-8ms, ~5ms |
1 million | 0-11ms, ~2ms | 43-69ms, ~47ms |
1 billion | 22-50ms, ~28ms | process terminated |
您可以在这里找到所有测试来源:
https://github.com/Taqmuraz/alloc-table
在那里我总共测试了四种语言:C、C、Java 和 Lisp。而且,带有 GC 的语言总是显示出更好的结果,尽管我对它们的测试比 C 和 C 严格得多。例如,在 Java 中,我通过虚拟函数调用分配内存,因此它可能不会被静态优化,而在 Lisp 中,我正在检查分配数组的第一个元素,因此编译器不会跳过分配调用。
C 粉丝仍然有动力去保护他们的信仰,所以,他们说“是的,你分配内存确实更快,但你必须在之后释放它!”。
真的。而且,突然之间,GC 释放内存的速度比 C 还要快。但是如何呢?想象一下,我们从 GC 进行了 100 万次分配,但后来我们的程序中只引用了 1000 个对象。而且,比方说,这些对象分布在所有这么长的内存范围内。 GC 进行堆栈跟踪,找到那 1000 个“活动”对象,将它们移动到上一代堆峰值,并将堆峰值指针放在最后一个对象之后。就这些了。
所以,
无论你分配了多少对象,GC的工作时间取决于之后你保留了多少对象。
而且,与此相反,在 C 中,您必须手动释放所有分配的内存,因此,如果您分配内存 100 万次,您也必须进行 100 万次释放调用(否则将会出现内存泄漏)。这意味着
GC 的 O(1)-O(n) 与 C 的 O(n) 或更差,其中 n是之前发生的分配次数。
demands | languages with GC | C/C |
---|---|---|
arithmetic | fast with JIT | fast |
allocating memory | fast O(1) | slow |
releasing memory | fast O(1) best case, O(n) worst case | O(n) or slower |
memory safe | yes | no |
现在我们可能会看到——垃圾收集并不是一种必要的罪恶,而是我们唯一希望拥有的最好的东西。它为我们提供了安全和性能两者。
虽然 C 在我的测试中确实显示出较差的结果,但它仍然是一门重要的语言,并且有自己的应用领域。我的文章并不是为了拒绝或删除 C。 C也不错,只是没有人们想象的那么优越。许多好的项目失败只是因为有些人决定使用 C 而不是 Java,例如,因为他们被告知 C 更快,而 Java 由于垃圾收集而慢得令人难以置信。当我们编写非常小且简单的程序时,C 很好。但是,我绝不会建议用 C 编写复杂的程序或游戏。
C 不简单,不灵活,语法超载,规范太多复杂。使用 C 编程,你不会实现自己的想法,但 90% 的时间都会与编译器和内存错误作斗争。
这篇文章的目的是拒绝C语言,因为速度和性能只是人们在软件开发中使用这种语言的借口。使用 C ,您要付出时间、程序性能和心理健康的代价。所以,当你在 C 和任何其他语言之间做出选择时,我希望你选择最后一种。
以上是C和C真的那么快吗?的详细内容。更多信息请关注PHP中文网其他相关文章!