TypeScriptで MessagePack encoder/decoder を実装した

npm install @msgpack/msgpack でインスコできます。NodeJS v12 でベンチマークしたかぎり、JSONと同程度の速度で、これまで最速といわれてきた msgpack-lite よりもさらに少しだけ高速です。

github.com

もともとこのリポジトリには uupaaさんによる実装(tagged as classic) があったんですが、メンテされなくなって久しく npmjs.com にもリリースされていないという状況でした。

https://github.com/msgpack/msgpack-javascript/tree/classic

その後kawanetさんが msgpack-lite を実装したのが2015年。これが2019年現在、もっとも週間ダウンロード数の多いMessagePack for JSの実装です。

msgpack-lite ピュアJavaScript実装の速いMessagePackライブラリ - Qiita

msgpack-liteはちゃんと動いてメンテされていて stream encoder / decoder もある優秀な実装なんですが、NodeJS向けに最適化されていて、実際くだんのmsgpack-liteの紹介エントリでもBufferに大きく依存しているとあり、ブラウザサポートは「Bufferのpolyfillがあればブラウザでも動く」という水準です。用途を察するに fluentd client for NodeJS で使うことを想定しての実装であり、これはこれでベストな実装です。

ただ今回はブラウザ向けに最適化された実装がほしかったのと、ES2018 (async iteration) とTypeScriptという条件で設計したらいろいろ変わるはずだなあという思いがありました。

要件

  • ES2018+ な設計と実装
    • TypedArraysに依存した設計やESクラスの構文などを活用したい
    • AsyncIterable<Uint8Array> をうけとるストリーミングデコーダがほしい
  • ES5 (IE11) でも動く
    • そうはいってもまだIE11サポートを完全には切れないので…
    • TypeScriptのdownlevel compile (target: "es5"downlevelIteration: true を利用) と polyfill (core-js) を想定する
  • streaming decoder
    • Fetch APIの streaming download と繋いでstreaming decodeすると、特に回線の遅い環境で効果的だと思われる

実装

ブラウザ向けに最適化といいつつ、いろいろがんばったら NodeJS v12 ではmsgpack-liteより速くなりました。@msgpack/msgpack はDataViewに大きく依存していて、 Improving DataView performance in V8 · V8 が実装されたV8が入ったのがNodeJS v12 だからこそですね。

なお↓のベンチマークは mspgack-lite に同梱されているものを流用しています。 Buffer.from(JSON.stringify(...)) はI/Oを想定しているからで、JSON.stringifyの戻り値であるJavaScriptのstringはUTF8 encodeしてバイト列にしないとファイルに保存したりネットワークで転送したりできないため、ベンチマークの公平性という観点から入れたと思われます。MessagePackのエンコードはバイト列を生成するのでそのままI/Oに載せられるため、加工はしていません。

なおベンチマークはいまのところNodeJSでしか行ってませんが、@msgpack/msgpack はECMA-262の機能しか使ってないのでChromeでも同じ傾向になるはずです。またMessagePack実装はmsgpack-liteとしか行ってませんが、(すくなくとも2015年時点では)msgpack-liteが最速ということだったので他の実装との比較はここでは省略しています。

Benchmark on NodeJS v12

NodeJS v12 だと encodeは Buffer.from(JSON.stringify(obj)) よりも少し速く、decodeはJSONほぼ同水準という結果になりました。

Benchmark on NodeJS/v12.1.0

operation op ms op/s
buf = Buffer.from(JSON.stringify(obj)); 493600 5000 98720
buf = JSON.stringify(obj); 959600 5000 191920
obj = JSON.parse(buf); 346100 5000 69220
buf = require("msgpack-lite").encode(obj); 358300 5000 71660
obj = require("msgpack-lite").decode(buf); 270400 5000 54080
buf = require("@msgpack/msgpack").encode(obj); 594300 5000 118860
obj = require("@msgpack/msgpack").decode(buf); 343100 5000 68620

Benchmark on NodeJS v10

参考までに NodejS v10 の結果も載せておきますが、見ての通りかなり遅いです。

Benchmark on NodeJS/v10.15.3

operation op ms op/s
buf = Buffer.from(JSON.stringify(obj)); 369500 5000 73900
buf = JSON.stringify(obj); 942100 5000 188420
obj = JSON.parse(buf); 356800 5000 71360
buf = require("msgpack-lite").encode(obj); 283300 5000 56660
obj = require("msgpack-lite").decode(buf); 211200 5000 42240
buf = require("@msgpack/msgpack").encode(obj); 147000 5000 29400
obj = require("@msgpack/msgpack").decode(buf); 132400 5000 26480

ユースケース

Kibela Web API (beta) がMessagePack (application/x-msgpack) を扱えるので、こんな感じで decodeAsync を使えます。

github.com

まだブラウザでのテスト(特にIE11)が十分でないので @msgpack/msgpack のバージョンは v1.0.0 にしてませんが、ブラウザでの挙動を十分に確認したら v1.0.0 にしようと思っています。