各種程式語言的速度測量與比較應該怎麼做 - C# / C++ / Java / Go 以相同條件比較的實踐指南

· · Benchmark, Performance, C#, C++, Java, Go

「C++ 聽說很快」 「Go 在實運營中很輕」 「Java 長時間轉的話相當快」 「C# 也因為有 .NET 的 JIT 意外地強」

這類話題經常出現。 但這裡最不該做的是 把不同人在不同環境測的數字排起來直接當成語言的優劣

C# 和 Java 容易受 JIT 或 warm-up 的影響,C++ 和 Go 通常是事前編譯完成的。 GC 的有無或特性也不同。標準函式庫或周邊函式庫的實作差異也相當有影響。此外同一台機器也會因電源設定、熱、背景處理、輸入資料的偏差讓結果輕易震盪。相當泥濘的世界。

本文整理 儘可能公平地比較 C# / C++ / Java / Go 的測量方法。 先說結論,不要試圖用 1 個數字決定「哪個語言最快」 是最重要的。

本文始終以 比較方法的整理 為主題。 排出環境依賴的數字看起來像占卜,所以這裡不寫實測排名。取而代之,僅整理 如何設計能讓比較有價值

先講結論

先講結論,C# / C++ / Java / Go 的速度比較中真正有效的是以下 7 個。

  1. 先決定要比什麼的速度 是啟動時間、穩態的 throughput、p95 延遲還是記憶體效率,測量方式會改變。

  2. 不以 1 個 bench 下結論 CPU 計算、記憶體分配、並行處理、啟動時間,強的語言或執行環境的表現會改變。

  3. C# 和 Java 分開 cold 和 warm 混合包含初次執行的比較和 warm-up 後的穩態比較,話題會扭曲。

  4. 以同樣的演算法、同樣的輸入、同樣的正確性確認測量 不是實作快,而是在解不同的問題,是 bench 常見的事。

  5. 區分語言內的 microbenchmark 和跨語言的 end-to-end bench 各語言專用的 harness 方便,但跨語言比較用外部的共通 runner 跑比較有道理。

  6. 不只看平均也看中位數和分布 只要 1 次 GC 或背景處理插入,平均就會壞。

  7. 不只留數字也留條件 bench 結果同時是速度的記錄和實驗條件的記錄。沒寫條件的結果之後會相當痛苦。

最初該決定的

用 1 個詞解決「快」大概會出事。 先決定 要稱什麼為快

例如同一個程式,想看的東西相當不同。

1. 想看啟動時間嗎

CLI 工具、短命批次、啟動 1 次就結束的輔助工具,cold startprocess startup 有效。 此軸中,是否包含 JIT 或類別載入的初始化成本,結果會大幅改變。

2. 想看長時間運轉的 throughput 嗎

伺服器、常駐程序、worker、長時間運作的轉換處理,steady-state 的 throughput 重要。 此情況初次只是慢本身不是本質,warm-up 後能穩定延伸到哪是主題。

3. 想看 tail latency 嗎

API、UI、即時系處理中,比起平均,p95 / p99 更重要的情況有。 平均快但偶爾大停,使用者體驗或 SLA 上痛苦。

4. 想包含記憶體效率看嗎

不只 CPU 時間,最大 RSS、分配量、GC 次數、GC pause 不看的話會誤讀實運營的沉重度。 「快但吃不少記憶體」和「稍慢但穩定輕量」根據用途評價會逆轉。

簡言之最初該決定的問題是

這個比較想知道的不是哪個語言快,而是 哪個 workload 在哪個條件下哪個指標能快速處理

這裡曖昧收集數字最後會收不攏。

為何語言比較困難

混合 JIT 和 AOT 變成不同實驗

C# 和 Java 通常受 JIT 影響。 另一方面 C++ 和 Go 通常是事前編譯完成。

也就是說測初次執行,不只是 程式本體的速度執行環境的啟動・類別載入・JIT 準備 也一起測。 反之,只看充分 warm-up 後,就成為 穩態的最佳化能效多少 的比較。

兩者都有意義。 但 意義不同

比起語言差異,實作差異更大是常見的

同樣的「排序」也,

  • 一邊用標準函式庫
  • 一邊自行實作
  • 一邊做了多餘的複製
  • 一邊每次重新產生輸入

光這些結果就相當變。

此外 JSON、壓縮、加密、正規表達式這類處理,比起 語言本身函式庫實作 的差相當有效。 所以不明示測什麼,「語言比較」本意變成「函式庫比較」。

C++ 有因最佳化使處理消失的陷阱

特別 microbenchmark 中,編譯器判斷「這個計算結果沒人用」,有時會把處理消掉。 這樣不是快,而是 根本沒做事 的小怪談。

C++ 中這問題特別明顯,所以使用結果、輸出 checksum 或 benchmark 框架的最佳化抑制功能相當重要。

GC 的存在不是「不利」也不是「有利」,是特性

C#、Java、Go 有 GC。 把它單純說「有 GC 所以慢」太粗略。

實際上,

  • 如何處理大量短命物件
  • 堆大小設定
  • GC 的頻率和 pause
  • 物件佈局
  • 函式庫的分配習性

更有效。

反之 C++ 可以手動管理或用 RAII 細緻控制,但相應地設計或實作的差易出現。 也就是說 管理方式的差異不直接是善惡或優劣

比較中不該做的

1. 混合 Debug 和 Release

這是論外。 比較對象一定對齊 相當於正式的最佳化建置

2. 沒解同樣的問題

輸入格式不同、輸出不同、只有一邊沒錯誤處理、記憶體再利用方針不同。 放任這些,會變成 測量不是速度而是需求差

3. 只執行 1 次下結論

只 1 次的執行大概是雜訊。

  • JIT
  • 頁快取
  • CPU 的加速
  • 背景任務
  • GC
  • 初次的檔案讀取

這些 1 次全部混在一起。

4. 混合 warm-up

測 C# 和 Java 時,要包含初次還是只看 warm-up 後,曖昧的話議論會崩壞。 cold 和 warm 當成不同 來處理。

5. 不做正確性確認

bench 先於「快」的是「回傳同樣的結果」。 對比較對象的全部實作,一定確認 從同樣輸入得到同樣 checksum 或同樣輸出

6. 只以 1 個 microbenchmark 決定世界觀

tight loop 贏了未必實服務整體贏。 反之啟動時間輸了,長時間運轉也可能夠強。

比較 C# / C++ / Java / Go 時的基本方針

這裡相當重要。 推薦 2 層構成

1. 語言內的測量用符合該語言的 harness

各語言有吸收該語言事情的 benchmark 工具。

  • C#: BenchmarkDotNet
  • Java: JMH
  • Go: go test -benchbenchstat
  • C++: Google Benchmark

這些會某程度照料各自的執行環境事情、統計處理、測量的陷阱。 對 語言內的比較實作的深挖 相當有效。

2. 跨語言的比較在外側放共通 runner

另一方面直接把 C# 的 BenchmarkDotNet 的結果Java 的 JMH 的結果 並列有點危險。 harness 本身的作法不同。

所以跨語言時,把各實作做成 能以同樣 CLI 合約呼叫的可執行檔,從外側用同樣條件跑是推薦的。

例如各語言準備以下形式的可執行檔。

bench --scenario sort_int32 --dataset data/sort_10m.bin --mode warm
bench --scenario group_words --dataset data/words_100mb.txt --mode cold
bench --scenario parallel_hash --dataset data/blob_1gb.bin --threads 8

然後共通 runner 側,

  • 執行順序隨機化
  • 分 cold / warm
  • 傳同樣的資料集
  • 驗證 checksum
  • 採集 wall-clock 和記憶體
  • 留 CSV / JSON 的 raw data

這樣的流程。

這樣做的話,各語言中的最佳實踐跨語言的公平性 會易分開處理。

具體例: 該準備什麼 bench 項目

被說「想比 C# / C++ / Java / Go」時,若只 1 個就 不易誤解的單純 CPU 系,若多個就準備 性格不同的 workload 3~4 個 是推薦的。

推薦構成

1. sort_int32_10m

目的: 看 CPU + 記憶體頻寬 + 暫存區域的使用方式

  • 輸入: 固定 seed 產生的 1,000 萬件 int32
  • 處理: 把陣列 sort 回傳 checksum
  • 注意點: 每次回到同樣的未排序輸入

這個相對好懂。 但因包含標準排序實作的差,比起 語言本身,成為 含標準函式庫的比較

2. hash_group_count

目的: 看雜湊表、字串處理、分配、GC 的傾向

  • 輸入: 固定文字資料
  • 處理: 數每個單字的出現次數
  • 輸出: 前 N 件和 checksum

這個接近實務的反面,字串函式庫或 map 實作的差也相當有效。 相應地比較接近現實。

3. parallel_sha256

目的: 看並行處理、排程器、worker pool、同步的習性

  • 輸入: 固定大小的二進位 chunk 列
  • 處理: 以 N 執行緒依序雜湊,回傳最終 checksum
  • 條件: 執行緒數 1 / 2 / 4 / 8 階段化

比起單純 tight loop,並行執行時的延伸方式 容易看到。

4. startup_noopstartup_parse_small

目的: 看啟動時間

  • noop: 啟動立即結束
  • parse_small: 把小輸入只處理 1 次結束

這裡 C# / Java 的 JIT 或初始化成本容易看到,和 C++ / Go 的表現相當不同。 反之這裡有差,也跟長時間處理的勝敗是分開的。

JSON 或 HTTP bench 怎麼辦

JSON 或 HTTP 接近實務,當然有意義。 但此情況是 不是語言比較,而是含函式庫・框架・生態系的比較

這本身不壞。 在實務上反而這邊更重要的情況多。 但是在文章或報告中,

這不是語言的比較,是含標準實作和主要函式庫的比較

明記比較不易誤解。

各語言該對齊的條件

C++

  • 對齊最佳化建置
  • 固定編譯器
  • 固定標準函式庫實作
  • 明記 -O3 / /O2、LTO、PGO 等條件
  • 注意結果不要因最佳化消失
  • 懷疑是否因未定義行為看起來快

C++ 自由度高的相應地條件差會直接大幅顯現。 所以 用哪個編譯器、用哪個旗標、用哪個 STL 測 相當重要。

C#

  • 對齊 Release 建置
  • 固定 .NET 的版本
  • 記錄 Server GC / Workstation GC 等條件
  • 明記 Tiered Compilation、ReadyToRun、Native AOT 的有無
  • 分 cold 和 warm

C# 的 .NET 設定差會改變表現。 特別 JIT 的 C#Native AOT 的 C# 同樣是「C#」但是不同軸。 這裡混了比較對象就不是語言而是 發佈形態

Java

  • 固定 JDK 的供應商和版本
  • 明記 GC
  • 固定 warm-up / measurement / fork
  • 記錄堆大小或 JVM 選項
  • 分 cold start 和 steady-state

Java 易受 JIT 恩惠的反面,初次的表現相當變。 所以 短命程序的比較長時間運轉的比較 分開是必須的。

Go

  • 固定 Go 的版本
  • 固定 GOMAXPROCS
  • 明記 CGO_ENABLED
  • 若改 GOGC 一定記錄
  • 可能的話留 benchmark 格式的輸出

Go 相對好處理,但並行 bench 中 GOMAXPROCS 的影響大。 另外是否用 cgo 世界會變,那裡一定留條件。

執行環境的對齊方法

任何語言,不對齊環境的比較大概是比環境。

該對齊的

  • 同 CPU / 記憶體 / 儲存
  • 同 OS 版本
  • 同電源條件
  • 接近同室溫的條件
  • 同輸入資料
  • 同程序優先度
  • 同核心數條件
  • 同容器或裸機條件

特別有效的

電源設定和 CPU 頻率

筆電的話光 AC 連線或電池就變另一個世界。 CPU governor 或 power mode 沒對齊,比較結果會相當震盪。

關於 Windows 的電源條件、通知、背景雜訊、熱、執行順序的對齊方法,別篇 Windows 中如何比較不同版本程式的執行速度 詳細整理。Windows 上測的話這裡相當有效。

最初幾次快,後半降的話懷疑熱或節流。 把 A 全跑完後把 B 全跑不如 A / B / A / B 交替跑減少偏差。

背景處理

更新、索引、同步、防毒掃描、瀏覽器、聊天工具。 這些樸素但普通插入。

該測什麼

語言比較中至少分以下 4 個看是推薦的。

1. wall-clock time

使用者等的實時間。 最先該看的指標是這個。

2. CPU time

「實際用 CPU 多少」。 只 wall-clock 快 CPU time 沒變的話可能是等待時間或 I/O 的影響。

3. memory / allocations

  • 最大 RSS
  • 總分配量
  • alloc 次數
  • GC 次數
  • GC pause

看這些會看到速度背後的成本。

4. 分布

  • 中位數
  • p95 / p99
  • min / max
  • 標準差或震盪

只用平均說,偶爾飛的處理的真相看不到。

執行步驟的推薦

實運營易做的流程整理,大致以下順序。

1. 決定 workload

先明確要比什麼。

  • 啟動時間
  • 穩態 throughput
  • tail latency
  • 記憶體效率
  • 並行擴展

2. 固定共通資料集

輸入資料用固定 seed 或固定檔案對齊。 若連資料產生也包含,那也需要各語言同條件。

3. 先通過正確性確認

小資料和大資料中確認全部實作回傳同樣結果。 讓它輸出 checksum 或雜湊易處理。

4. 固定 build 條件

各語言做 Release / 最佳化完成的執行形式,記錄版本和旗標。

5. 分 cold 和 warm

特別 C# 和 Java 這裡重要。

  • cold: 含程序啟動直後
  • warm: 幾次執行後的穩定狀態

這 2 個不要混同一個表比較乾淨。

6. 執行順序交替或隨機化

例:

cpp -> csharp -> java -> go
go -> java -> cpp -> csharp
csharp -> go -> java -> cpp
...

這樣熱或雜訊的偏差減少。

7. 確保次數

輕的 microbenchmark 要相當多,end-to-end 至少想要 10 次以上。 差小但次數少,解釋相當危險。

8. 保存 raw data

不只聚合結果,留 各 run 的生資料。 之後看能讀外部值或 warm-up 的習性。

9. 有差就取 profile

有差時在那才挖原因。

  • CPU profile
  • allocation profile
  • GC log
  • flame graph
  • OS 側的追蹤

到這裡,不是「快 / 慢」,而是能說 為什麼那樣

結果的讀法

數字出來後,讀法錯了還是危險。

只初次 C# / Java 慢

懷疑 JIT、類別載入、初始化的影響。 此情況,

  • 啟動時間重要就是有意義的差
  • 長時間運轉為主題就是該分表的差

C++ 在 tight loop 強

低階最佳化、物件配置、最小的執行環境額外開銷有效的可能性。 但只看那裡說「所以實服務也最快」是跳躍。

Go 在啟動時間或易發佈上看起來有利

單一二進位、較輕的啟動、好用的並行模型有效的情況。 但不是所有 CPU 系 workload 都有利。

C# / Java 在 steady-state 相當追上,或逆轉

JIT 的最佳化有效的可能性。 這也不是罕見的話。 所以 含啟動的比較穩態的比較 不混很重要。

allocation-heavy 的處理差大

此情況比起語言名,

  • 記憶體佈局
  • 字串或 map 的處理
  • GC 的行為
  • 多餘的複製

更有效的情況多。

記錄模板

bench 結果至少留以下項目,之後有幫助。

timestamp,language,scenario,run_kind,cold_or_warm,elapsed_ms,cpu_ms,max_rss_mb,alloc_bytes,gc_count,checksum
compiler_or_runtime,compiler_version,flags,os,cpu,threads,input_id,notes

例如 run_kind 能這樣分。

  • micro
  • macro
  • startup
  • parallel

cold_or_warm 至少想明示以下之一。

  • cold
  • warm

bench 比起 測量之後能解釋 更重要。

總結

C# / C++ / Java / Go 的速度比較真正重要的是, 把 哪個語言最快 這粗略的問題, 落到 哪個 workload 在哪個條件下哪個指標比較 這個實驗的形式。

特別不易偏離的重點再整理如下。

  • 分啟動時間和穩態
  • 以同演算法、同輸入、同正確性確認測
  • 不以 1 個 bench 下結論
  • 分語言內的 benchmark 和跨語言的 benchmark
  • 比起平均看中位數和分布
  • 留條件和 raw data

然後最後最重要的是 不要過度用語言名決定勝敗。 現實的性能由語言、執行環境、函式庫、build 條件、資料、OS、硬體的合作決定。

「C++ 快」「Java 強」「Go 輕」「C# 也夠快」的話全部在某種意義上是對的。 但 在哪條件下這樣說 缺了,大概成為霧中對打。

對齊條件,多個 workload,分 cold / warm,看到分布。 樸素但結局這是最強的。

參考資料

  • BenchmarkDotNet Getting Started https://benchmarkdotnet.org/articles/guides/getting-started.html

  • OpenJDK JMH Project https://openjdk.org/projects/code-tools/jmh/

  • JMH GitHub Repository / README https://github.com/openjdk/jmh

  • Go testing package https://pkg.go.dev/testing

  • Go benchstat https://pkg.go.dev/golang.org/x/perf/cmd/benchstat

  • Google Benchmark User Guide https://google.github.io/benchmark/user_guide.html

  • Windows 中如何比較不同版本程式的執行速度 https://comcomponent.com/blog/2026/03/16/002-windows-benchmark-comparing-program-versions/

相關主題

與本文一起看易理解的頁面。

此主題的諮詢處

性能比較的設計、計測條件的對齊方法、結果的解釋、原因的深挖,是與下列服務相性好的主題。

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽