更新紀錄(僅初版,2026年03月17日 發布)
- 初次發布
引用本文(DOI: 10.5281/zenodo.21616314)
本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。
小村 豪(2026)。〈各種程式語言的速度測量與比較應該怎麼做 - C# / C++ / Java / Go 以相同條件比較的實踐指南〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616314 https://staging.comcomponent.com/zh-TW/blog/2026/03/17/000-language-benchmark-csharp-cpp-java-go/
- DOI(最新版本)
- 10.5281/zenodo.21616314
- DOI(此版本)
- 10.5281/zenodo.21616315
「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 應用程式的工作列通知區常駐與 Toast 通知 —— NotifyIcon 的陷阱與 AppNotification 的選型
本文整理將業務用 Windows 應用程式常駐於工作列通知區,並透過 Toast 通知告知使用者的實作要點。內容涵蓋 NotifyIcon 的正確用法與「關閉後仍留在通知區」的設計、檔案總管重新啟動時的重新註冊、三種 Toast API(Windows App SDK Ap...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
常見問題
整理諮詢這個主題時常見的問題。
- C#、C++、Java、Go 哪個語言最快?
- 這個問題不該用一個數字回答。正確做法是把「哪個語言最快」這個粗略的問題,落到「哪個 workload 在哪個條件下哪個指標比較」的實驗形式。C# 和 Java 容易受 JIT 或 warm-up 影響,C++ 和 Go 通常是事前編譯完成,GC 特性與函式庫實作的差異也相當有影響,所以不同 workload 下強的語言會改變,不要以一個 benchmark 下結論。
- 測量時為什麼要把 cold 和 warm 分開?
- 因為 C# 和 Java 通常受 JIT 影響,測初次執行時不只測程式本體的速度,執行環境的啟動、類別載入、JIT 準備也一起被測進去。混合包含初次執行的比較和 warm-up 後的穩態比較,話題會扭曲。cold 適合看 CLI 工具或短命批次的啟動時間,warm 適合看伺服器等長時間運轉的穩態 throughput,兩者要當成不同的實驗處理,不要放進同一個表。
- 跨語言的速度比較該怎麼設計?
- 推薦兩層構成。語言內的測量用符合該語言的 harness:C# 用 BenchmarkDotNet、Java 用 JMH、Go 用 go test -bench 和 benchstat、C++ 用 Google Benchmark。跨語言比較則把各實作做成能以同樣 CLI 合約呼叫的可執行檔,由外側的共通 runner 以同樣條件執行,並隨機化執行順序、分 cold/warm、傳同樣的資料集、驗證 checksum、保留原始資料。
- benchmark 的結果只看平均值可以嗎?
- 不行。只要一次 GC 或背景處理插入,平均就會壞掉,所以要同時看中位數、p95/p99、min/max 與分布。除了 wall-clock time,也建議看 CPU time、最大 RSS、分配量、GC 次數等記憶體面的成本。另外不只留數字也要留條件與 raw data,bench 結果同時是速度的記錄和實驗條件的記錄,沒寫條件的結果之後會相當痛苦。