我從事程式設計的那段時間,聽說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是之前發生的分配次數。
所以,我想鞏固垃圾收集語言對 C 和 C 的勝利。這是總結表:
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中文網其他相關文章!