各種程式語言的速度測量與比較應該怎麼做 - 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 個。
-
先決定要比什麼的速度 是啟動時間、穩態的 throughput、p95 延遲還是記憶體效率,測量方式會改變。
-
不以 1 個 bench 下結論 CPU 計算、記憶體分配、並行處理、啟動時間,強的語言或執行環境的表現會改變。
-
C# 和 Java 分開 cold 和 warm 混合包含初次執行的比較和 warm-up 後的穩態比較,話題會扭曲。
-
以同樣的演算法、同樣的輸入、同樣的正確性確認測量 不是實作快,而是在解不同的問題,是 bench 常見的事。
-
區分語言內的 microbenchmark 和跨語言的 end-to-end bench 各語言專用的 harness 方便,但跨語言比較用外部的共通 runner 跑比較有道理。
-
不只看平均也看中位數和分布 只要 1 次 GC 或背景處理插入,平均就會壞。
-
不只留數字也留條件 bench 結果同時是速度的記錄和實驗條件的記錄。沒寫條件的結果之後會相當痛苦。
最初該決定的
用 1 個詞解決「快」大概會出事。 先決定 要稱什麼為快。
例如同一個程式,想看的東西相當不同。
1. 想看啟動時間嗎
CLI 工具、短命批次、啟動 1 次就結束的輔助工具,cold start 或 process 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 -bench和benchstat - 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_noop 或 startup_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 能這樣分。
micromacrostartupparallel
cold_or_warm 至少想明示以下之一。
coldwarm
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
testingpackage https://pkg.go.dev/testing -
Go
benchstathttps://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/
相關主題
與本文一起看易理解的頁面。
此主題的諮詢處
性能比較的設計、計測條件的對齊方法、結果的解釋、原因的深挖,是與下列服務相性好的主題。
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
Windows 應用安全處理子行程的 checklist - Job Object、結束傳播、標準輸入輸出、watchdog 的最佳實務
在 Windows 應用上安全處理子行程,關鍵不在挑啟動 API,而是設計行程樹的擁有者與結束流程。本文整理 Job Object 的 KILL_ON_JOB_CLOSE、GUI 與 console 的 graceful shutdown、stdio 平行抽乾與 EOF、w...
使用共享記憶體時的陷阱與最佳實踐 - 先整理同步、可見性、壽命、ABI、安全性
整理在同一機器內以共享記憶體交換大型資料時的陷阱與設計要點。把 control plane 和 data plane 分離、縮小並行模型、用固定寬度整數和標頭設計 ABI、以 offset 取代指標、明示 commit protocol、為當機復原放入 generation...
在 Windows 上如何比較不同版本程式的執行速度 - 從電源模式等環境的對齊到可到的極限
本文整理在 Windows 上比較不同版本程式速度時,如何把 power mode、power plan、熱、背景雜訊與快取狀態這些雜訊削到最低,並依 wall-clock、CPU time、cycle 三種指標把差異拆開讀,必要時再用 ETW / WPR 挖出真正的理由,...
用 Native AOT 把 C# 做成原生 DLL 的方法 - 用 UnmanagedCallersOnly 從 C/C++ 呼叫
從現有 C/C++ 應用程式以 in-process 方式呼叫 C# 邏輯時,本文示範以 Native AOT 將類別庫發佈為原生 DLL,並用 UnmanagedCallersOnly 公開 cdecl 進入點。透過 handle、錯誤碼與扁平 C ABI 設計交界面,整...
在 Windows 上,單一執行檔(single binary)能做到什麼程度 - 能收進 1 個 EXE 的範圍、無法消除的 Windows 依賴、以及發布前的判斷表
整理在 Windows 上把應用做成單一 EXE 時的真正界線:發布物收成 1 個、把 runtime 同捆、免 installer、降低 OS 依賴是 4 個層次。搭配 .NET、C++、WebView2、WinUI、服務驅動的判斷表,幫你在發布設計階段做對選擇。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。