從比並 JSON 庫談開源意義
在開發 RapidJSON 期間,我建立了另一開源項目 nativejson-benchmark,至今已整合 41 個 C/C++ JSON 開源庫,評測它們對標準符合程度以及性能。本文先談一下評測結果,再談這個項目的意義。
(題圖 Photo by Fervent Jan)
評測方法
標準符合程度(conformance):
1. 使用 JSON_checker 判斷是否能分辨正確及錯誤的 JSON(parse validation)。
2. 解析字元串類型 JSON,如正確結果比對(parse string)。
3. 解析數字類型 JSON,如正確結果比對(parse double)。
4. 生成 DOM,再用 DOM 生成 JSON。判斷來回往返(roundtrip)的字元串是否相同。
有些庫可能沒有生成 JSON 的功能,那麼就不能完成來回往返檢測。
性能檢測:
1. 解析 JSON 至 DOM(parse)。
2. 把 DOM 轉換至 JSON(stringify)。
3. 美化轉換(prettify)。
4. 遍歷 DOM 統計各種 JSON 類型的數量及字元串長度(statistics)。
5. 使用 SAX 做來回往返(sax roundtrip)。
6. 使用 SAX 做統計 (sax statistics)。
7. 生成一個程序,檢測可執行檔的大小。該程序能解析 JSON 至 DOM 並把統計結果列印出來。
現時有 3 個性能測試用的 JSON,合共大小是 4.6MB。
加入此評測的庫,最低限度要能實現 parse 和 statistics。其他檢測是可選的。
結果
以下是 2016 年 9 月 9 日,在 MacBook Pro (Retina, 15-inch, Mid 2015, Corei7-4980HQ 2.80GHz)、clang 7.0 64-bit 的執行結果。也可用 互動版本觀看。
綜合標準符合程度(越高越好):
目前只有 RapidJSON 的全精度版本及 taocpp/json 能獲得 100% 完美得分。
其實這個檢測中,部分數字解析及相關的來回往返測試是過於嚴格的,超越了標準所需的程度。有大約 80%-90% 已經可以說完全符合標準。之後會調整檢測,把一些不屬於標準必須的部分僅作參考,不算進整體分數。
有些庫未能處理 "u0000" 和代理對(surrogate pair),尚可接受。有些庫連 "
" 這類轉義符都不處理,就有點太過分了。
另一個常見問題是,一個 JSON 值結束後只能含有空白字元,例如
[1, 2, 3] excited
就是不合法的。有些庫沒做這個檢測。
解析時間(越低越好):
前排是 RapidJSON、gason、ujson4c、sajson。然而,後三者的標準符合程度都較低。現時最快的 RapidJSON 以 7.9ms 解析 4.6MB 的 JSON 文本,比最後一名快 140 倍以上。
解析後的內存分配大小(越低越好):
Qt 未能成功檢測其內存消耗,請忽略。RapidJSON 暫無對手,它在 x64 架構下的內存總消耗是 (JSON 節點數量 ? 16 位元組 + 長字元串位元組大小)。長字元串是指超過 13 個位元組的字元串。之前發現了一個比 RapidJSON 更省的 jbson,但因為在 travis 上崩潰暫未加進評測。
Stringify 和 Prettify 時間(越低越好):
這兩項都是 RapidJSON 完勝。這裡有一個問題是測試的 JSON 數據中,最大的一個主要內容全為浮點數,而這類型的生成也是各類型中最耗時的,也許將來要再調整測試數據。
代碼大小(越低越好):
由於要使用 V8 的 VM 來使用其 JSON 功能,所以執行文件特別大(靜態鏈接)。較前排的 Folly、Qt、C++ REST SDK 都是使用動態鏈接的,所以特別小,其他的都是靜態鏈接。而在靜態鏈接庫中,通常以 C 編寫的較小,C++ 的較大。
起源
最初開發 RapidJSON 時,在項目內部寫了一個性能測試,與 YAJL 和 JsonCpp 比較性能,僅量度耗時。然而,耗時並不是唯一需要量度的數值,也許一些庫很快,卻沒有做各種處理,或是數字的轉換方式並不精確。另外,要把各個其他 JSON 庫加入 RapidJSON 的代碼庫中,也對使用者造成不便。
因此,後來就把那個性能測試獨立成項目,制定了一個通用介面,每個庫都只需實現該介面,便能作出各項檢測,最後生成圖表。
為了簡化編譯過程,整合大部分較簡單的 JSON 庫的時候,都是在測試 .cpp 中直接 #include 該庫的 .cpp 實現。這樣也確保編譯參數都完全相同。只有像 Qt、V8、C++ Rest SDK 等需要自行安裝。
學習
在整合這幾十個庫的同時,我也會參考他們的功能、API 設計、實現技術等等。遇到性能優良的庫,也會仔細看看它有沒有用到什麼特別的技術。同時也會在想,有哪些功能都是缺乏的,了解更廣,方能創新。
另一方面,我相信其他開發者也會通過這個平台,查看自己的庫的優缺點,學習其他庫並作出改進。當然,除了這種暗地裡互相學習的,也有討論交流的。
貢獻
在建立這個項目以來,通常是被動地從 JSON 庫的作者里接收 Pull Request,也許一些 JSON 庫的作者也不知道有這個項目。
這個項目的目的,除了給予使用者一個參考,也希望能提升各個庫的品質。沒有一個庫是完美的,需求各自不同。如果能提升品質,對它的使用者也有幫助。
因此,我最近為每個庫的標準符合程度檢測生成 markdown 格式的報告,並主動地發送成各個項目的 issue ,供他們參考。這個舉動已獲得不少回應,有些作者已積極修改一些問題。
另外,我之前整合遇到一些問題時,也會發 issue 查詢討論。例如,最近一輪的排查中,發現 facebook/folly 的 toJson() 異常地慢:
經性能分析後,發現問題僅在於一行代碼:
// json.cppvoid escapeString( StringPiece input, std::string& out, const serialization_opts& opts) { auto hexDigit = [] (int c) -> char { return c < 10 ? c + 0 : c - 10 + a; }; out.reserve(out.size() + input.size() + 2); // <-- 這一行 out.push_back("); // ...
此行原來的意義,應該是希望預分配字元串的輸出緩衝,避免過程中需要重新分配。但在一般的實現中,std::string::reserve() 在空間不足時,會把緩衝設置為指明的大小。由於每次的分配都是剛好夠用而已,下次再輸出字元串就必會再重新分配,造成 的性能瓶頸。而如果只是用 std::string::push_back(),它分配新空間會為現有大小的兩倍(或其他倍數),達至分攤 的時間複雜度。所以,經過實驗,只要刪去該行,就能達到正常的性能:
這個測試顯示消耗時 1928ms ? 29ms,我已提供 Issue 及 Pull Request。
後語
JSON 的處理工作對於一些後台服務、嵌入式系統來說,可能是一個很重要的部分,影響整體性能。雖然現時我的工作其實並沒怎麼能用到這些功能,我仍然覺得,開發 RapidJSON 及這個評測項目對於我個人、公司及開源社區也是有幫助的。
我會持續更進這些項目。之前還希望加入其他原生庫,例如是 Objective C、Go、Rust 寫成的庫,如果有熟悉這方面的同學,歡迎與我聯繫。
有鑒於看過這麼多 JSON 庫,最近也寫了一個極簡的 C JSON 庫,希望用它來寫幾篇入門級的《從零開始的 JSON 庫教程》,供剛學過編程的大學同學閱讀,請各位支持。
推薦閱讀:
※JSON日誌文件
※豆瓣閱讀器通過 JSON 獲取文章內容,看到的數據格式是被混淆/加密的,這是通過什麼原理實現的?如何將其解碼?
※零拷貝讀取文件成go對象
※json是什麼?
※JSON适合大数据传输吗?跨语言JSON数据传输需要注意什么?