ByteDance/Sonic:適用於Go的閃電般快速的JSON庫

導讀:“在微服務的世界中,每一毫秒都至關重要。看看 TikTok 的工程師團隊如何徹底改變 Go 中的 JSON 處理的
你是否曾部署過 Go 服務,並在處理數千個 JSON 請求時看到 CPU 激增?嗯,你並不孤單。
TikTok 的工程師面臨著大規模的這一挑戰——每天數十億個請求。他們的解決方案是什麼?他們建立了 Sonic,這是一個改變 Go 開發人員遊戲規則的 JSON 庫。

每個 Go 開發人員都知道的 JSON 問題

如果你是一個 Go 開發人員,您可能使用過標準encoding/json軟體包。它確實有效,但讓我們面對現實吧 – 它並不是最快的。如果您正在構建現代 Web 服務或 API,那麼您每天都會使用 JSON – 從 REST API 到配置檔案。

當 TikTok 的工程師發現他們的服務每秒處理數百萬個 JSON 請求時,即使 JSON 處理效能有微小的改進也會對伺服器成本和使用者體驗產生巨大影響。

讓我們看一個常見的場景:

// Your typical JSON processing with standard libraryimport "encoding/json"type User struct { Name string `json:"name"` Posts []Post `json:"posts"`}// Processing this for millions of requests...json.Unmarshal(data, &user)
在小規模上,這種方法還不錯。但當你處理 TikTok 規模的流量時,這些毫秒的延遲會造成巨大的伺服器成本和延遲。這正是字節跳動團隊決定迎難而上的原因。

速度冠軍:遇見索尼克

首先我們來看一些實數。處理中等大小的 JSON 檔案(約 13KB)時:

// Processing user profile with posts and metadataStandard JSON: 106,322 ns/op (nanoseconds per operation)Sonic: 32,393 ns/opMemory Usage:- Standard: 49,136 bytes with 789 allocations- Sonic: 11,965 bytes with just 4 allocations
  • 小號(400B,11鍵,3層)

  • 大型(635KB、10000+ 鍵、6 層)

請各位參閱bench.sh瞭解基準程式碼。
https://github.com/bytedance/sonic/blob/main/scripts/bench.sh
秘訣:四個簡單技巧

1.即時編譯(JIT)

想象一下你是一名廚師。你不必每次都遵循通用食譜,而是為你經常做的每道菜建立一個特殊的、最佳化的食譜。這就是 JIT 的作用!

  • 常規 Go JSON 庫:對所有 JSON 使用相同的通用程式碼
  • Sonic:為您的特定 JSON 結構建立專門的程式碼路徑

2. SIMD:同時完成更多工作

將 SIMD 想象為有多隻手來完成一項任務:

  • 常規方式:逐張對卡片進行排序
  • SIMD 方式:一次對多張卡進行排序
Sonic 使用這些“多手”(SIMD 指令)來並行處理 JSON 資料,從而使一切變得更快。

3. 智慧記憶體使用

Sonic 使用了一個巧妙的技巧:當它在 JSON 中找到一個沒有任何特殊字元的字串時,它不會複製。相反,它只是指向原始字串。

這就像給出方向而不是繪製新地圖!

// Example JSON{"name": "John", "city": "New York"}// Standard library: Makes copies of "John" and "New York"// Sonic: Just references these strings if they're simple

4. 可選功能 = 更快的速度

Sonic 對於哪些功能可選做出了一些明智的選擇:

  • 預設不對地圖鍵進行排序(節省 10% 的處理時間)
  • 預設情況下不轉義 HTML(節省 15% 的處理時間)
如果需要,你仍然可以啟用這些功能,但透過使它們成為可選功能,Sonic 可以在大多數常見用途中保持快速執行。

設計

該設計很容易實現:

  1. 針對編解碼器動態組裝帶來的函式呼叫開銷,JIT採用技術在執行時組裝與schema對應的操作碼(asm),最終以Golang函式的形式快取到堆外記憶體中。
  2. 針對大資料與小資料並存的實際場景,我們透過前置條件判斷(字串大小、浮動精度等)與標量指令相結合SIMD,達到最佳適配。
  3. 針對go語言在編譯最佳化方面的不足,我們決定使用C/Clang自己編寫並編譯核心計算函式,並開發了一套asm2asm工具,將經過全面最佳化的x86彙編翻譯成plan9,最終載入到Golang執行時。
  4. 考慮到解析和跳過之間的巨大速度差距,我們的 AST 解析器當然會使用該lazy-load機制,但是會以更具適應性和效率的方式減少多鍵查詢的開銷。

具體來說,我們進行了一些進一步的最佳化:
  1. 由於 Golang 無法內聯原生的 asm 函式,我們發現其代價甚至超過了 C 編譯器最佳化帶來的提升。所以我們在 JIT 中重新實現了一組輕量級的函式呼叫:
    • Global-function-table + static offset用於呼叫指令
    • 使用暫存器傳遞引數
  2. Sync.Map最初用於快取編解碼器,但是對於我們準靜態(讀遠多於寫)、元素較少(通常不超過幾十個)的場景,它的效能並不是最優的,所以我們用 tech.重新實現了一個高效能且併發安全的快取open-addressing-hash + RCU
https://github.com/bytedance/sonic/blob/main/docs/INTRODUCTION.md#design
現實世界的益處
讓我們討論一下現實世界中重要的數字:
  1. 記憶體使用情況(用於中等 JSON):
    • 標準 Go:使用約 49KB 記憶體,有 789 個分配
    • Sonic:僅使用 4 次分配,僅使用約 12KB
    • 當您處理數百萬個請求時,這個數字是巨大的!
  2. 實際影響:
    • 更快的 API 響應
    • 降低伺服器成本
    • 更好的使用者體驗
    • 每個伺服器處理更多請求

您應該轉換至 Sonic 嗎?

如果您符合以下情況,Sonic 可能非常適合您:
  • 你處理大量的 JSON 資料
  • 效能對於您的應用程式很重要
  • 您正在使用 AMD64 或 ARM64 處理器
但請記住:
  • 需要 Go 版本 1.17 或更高版本
  • 它適用於 Linux、MacOS 和 Windows
  • 如果需要某些功能(如 HTML 轉義),則需要明確啟用它們

快速入門示例

使用 Sonic 非常簡單:

import "github.com/bytedance/sonic"// Encodingdata := map[string]string{"hello": "world"}bytes, err := sonic.Marshal(data)// Decodingvar result map[string]stringerr = sonic.Unmarshal(bytes, &result)

總結

Sonic 向我們開發者展示了,即使是像 JSON 處理這樣常見的功能,仍有顯著的改進空間。透過使用現代 CPU 功能 (SIMD)、智慧編譯 (JIT) 和周到的設計選擇,它比標準庫實現了顯著的效能提升。

請記住:在軟體開發中,不僅僅是讓事情正常運轉——有時,讓它們更快地運轉可以為你的應用程式開闢新的可能性!~

編輯:聆聽世界的羊

參考:
https://dev.to/trungdlp/bytedancesonic-a-lightning-fast-json-library-for-go-1931
相關閱讀:

相關文章