用 encodeInto() 提高 MessagePack 中的字串編碼速度

#encoding #messagepack #nodejs #utf8

最近花很多力氣優化 msgpack-nodejs 這個專案
參考了其他先進的優化策略、把效能拉到跟當時 nodejs 生態圈最快的專案同等級
但不夠多自己原創的東西,也還是覺得有優化空間,所以還是努力一點一滴的累積優化
終於在今天,撇除跑分太鬼的 msgpackr,編碼速度贏了 22%、解碼速度贏 19%,覺得終於可以休息了

這邊文章是想在把這些細節遺忘前做點紀錄



msgpack-javascript 的 src/Encoder.ts 給了我相當大的啟發,他做的事情大概是:

  1. utf8Count() 計算字串的UTF8編碼總共要幾 Bytes

  2. ensureBufferSizeToWrite() 確定目前 Buffer 有足夠的寫入空間

  3. writeStringHeader() 寫入 Spec 規定的 String header、字串長度

  4. utf8EncodeTE() 或 utf8EncodeJs() 對字串編碼,根據 nodejs 版本,可能使用到 encodeInto()

  5. this.pos += byteLength; 移動指針

上述動作中,2、3、5 都是各個相關專案會出現的操作,但 1、4 很特別



首先講 4 ,自己刻UTF8編碼的原因應該是:
TextDecoder 在解碼小於 200 Bytes 的 UTF8 資料時特別慢
但我自己也這樣做過,因為當時有寫 benchmark 且發現沒效益才放棄

但是其中動用到 encodeInto() 的部分會比較特別,因為這個 API 能把編碼結果寫到指定的 Uint8Array 裡面,不會回傳編碼後的資料,也許是因為少了編碼過程中的 buffer resize 所以效能可能好一點。但它有個問題:無法在編碼前得知編碼後的 byteLength。所以該專案才會有下面要講的第 1 步驟。



再來講 1,他這麼做的主因應該是:
想要得到 encodeInto() 的效能優勢,但 Spec 要求先寫字串長度,才能寫 UTF8 bytes
所以乾脆自己先把長度算出來

個人認為這樣不值得
因為 utf8Count 的實作與 utf8EncodeJs 雷同,都是一個一個 bytes 讀過去
但一個是把 char code 轉換成 UTF8 編碼;一個是把所需 bytes 數量加總後回傳
跑這兩個 function 幾乎等於做兩次編碼,所以我覺得找其他方式會比較好



我也想得到encodeInto() 的效能優勢,但不想要自己計算長度

我用encodeInto() 先把結果寫進 StringBuffer,接著利用 API 回傳的 written bytes,對編碼結果用 subarray() 重新建立 View(這麼做不會複製任何 data bytes),而這個 View 就能用來取得編碼後的長度以及資料。

長度用 writeStringHeader() 寫入、資料用 ByteArray append() 寫入
而 append() 裡面用到的 TypedArray set(),MDN 說會用很智慧的方式複製資料(?)

整個流程如這樣 src/encoder/encoder.ts#L203-L225
然後再加上 LruCache 避免對一樣的字串再跑一次這些運算

整體流程像這樣: