我以前覺得使用 goroutine 和 channel 的性能開銷是基本忽略不計的--尤其是和 IO 的性能開銷相比--但是最近我做了一個實驗,實際驗證了下。
我在給我的課程項目做一個玩具相關的資料庫。一開始,我從 CSV 文件里加載數據表,後來我需要添加一個二進位的表格結構。不幸的是,第一次嘗試(加載二進位表格)的效果比加載 CSV 文件差遠了。
掃描二進位表格的時間慢了兩倍!這也太反常了,因為二進位表格結構更簡單,不需要字符串轉換。 幸好,我用了幾個不錯的 go 的性能測試工具研究了一下這個問題。
掃描 CSV 表格的 CPU profile 看起來比較合理,大部分時間浪費在 IO 相關的系統調用上;
掃描 CSV 表格的 CPU profile
但是掃描二進位表格的 CPU profile 看起來一點都不合理;只有很少一部分時間花在 IO 上。
掃描二進位表格的 CPU profile(第一次實驗結果)
原來,這個不合理的 CPU profile ,是由於我使用了 go 的並發原型。 當時我想用 goroutine 和 channel 把生產者和消費者解耦,簡化 scanner 的代碼結構。
創建 scanner 啟動生產者 goroutine,用來實現 IO 操作/解碼,給 channel 返回結果:
消費者 goroutine,通過重複調用 NextRecord 獲取結果,只需從 result channel 中讀數據:
可是,從 CPU profile 看出,在這一步,goroutine 花了大量時間阻塞在 channel 操作上,go 在運行時浪費了大量資源在調度/並發原型上。
我重寫了二進位表格掃描代碼片段,直接在消費者 goroutine 上做了所有操作:
不過,這個小改動在性能上卻有非常大的影響。下面是更新後的 CPU profile,看起來比之前更合理:
掃描二進位表格的 CPU profile (改進後)
下面是更新後的 benchmark 結果:
比讀取 CSV 文件快 2 倍多,比第一次實驗快 3 倍多,這還差不多!
我一直以為合理的使用 goroutine 和 channel 可以使代碼簡潔,在大部分情況下,不可能是性能問題的根本原因。但是這次實驗給我提了個醒,就是 go 的超級並發模型不能隨便濫用。
[校訂]
感謝 Raghav 的建議,他提供了更多 benchmark 的測試實例!
- 在 select 添加 context.Context ,減少 producer 超時時間: 59.4s
- 在 select 添加 context.Context ,減少 consumer 超時時間: 58.0s
- 使用緩衝為 1 的 channel :23.5s
- 使用緩衝為 1000 的 channel :17.4s
- 在 done channel 中去掉 select (done channel 不在上述代碼片段里):19.9s
- 在 done channel 去掉 select,同時使用緩衝為 1000 的 channel:14.3s
看來我們又學到一點:channel 緩衝大小和 select 語句的複雜度都對性能都有很大影響。
[校對2]
感謝 Stuart Carnie 的建議,他建議通過 channel 同時發送批次記錄,而不是一次只發送一條!下面是我使用不同的批次大小得到的 benchmark 結果:
- 1: 28.83s
- 10: 12.36s
- 100: 8.92s
- 1,000: 8.68s
- 10,000: 8.74s
- 100,000: 9.32s
看來,正如 Stuart 所說,將 channel 寫操作的個數減少 3 個數量級,在此處同樣產生很大影響。
via: https://medium.com/@robot_dreams/goroutines-and-channels-arent-free-a8684f3b6560
作者:Elliott Jin 譯者:ArisAries 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出