用 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 避免對一樣的字串再跑一次這些運算

整體流程像這樣: