TextEncoder / TextDecoder の速度比較

IE を除く現代の多くのブラウザでは [TextEncoder](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/TextEncoder) と [TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/TextDecoder) の API が使えて、UTF-8 や Shift_JIS の格納された Uint8Array と、string の相互変換が可能です。

（エンコード）文字列を Uint8Array の任意オフセット位置に UTF-8 で書き込む

（デコード）Uint8Array の任意オフセット位置に書かれた UTF-8 文字列を読み込む（バイト数は既知）

という条件で文字列の入出力処理について、下記の３実装の速度を比較してみます。

（１）Node.js の Buffer の当該機能を使った場合
（２）Pure JS で適宜実装した場合
（３）TextEncoder/TextDecoder API を利用した場合

今回は「Uint8Array の入出力かつ任意オフセット」という条件がキモです。
この条件だけ検証している理由は、なぜなら私がそういう実装が欲しいからです。
なお２の Pure JS 実装は [fastestsmallesttextencoderdecoder](https://www.npmjs.com/package/fastestsmallesttextencoderdecoder) モジュールとか、
たくさん種類があるんだけど、手元のコードなので、最速ではないかも。

検証環境は Node v14.17.0 arm64 M1 Macbook Pro です。

［グラフ補足］

- SB は半角 `A` が続く、シングルバイト文字の文字列です。
- MB は全角 `Ａ` が続く、マルチバイト文字の文字絵です。
- グラフの横軸は文字数。縦軸は ops/sec です。上ほど速い。

## エンコード（UTF-8書き込み）

書き込み速度の結果はシンプルです。

- 100文字くらいまでの短い文字列なら、オブジェクト生成が少ない Pure JS 実装が圧倒的に速い。
- それを超えるような長い文字列なら、ネイティブな TextEncoder・Buffer が速い。
- TextEncoder#encode と Buffer#write に優位な差がない。
- シングルバイト文字・マルチバイト文字でも、傾向は大差ない。

![ENCODE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12419/3f6c2bae-3b8b-c1cd-d7a6-171a2c528502.png)

ベンチマークのソースコードは [text-encoder-bench-js](https://gist.github.com/kawanet/a66a0e2657464c57bcff2249286d3a24#file-text-encoder-bench-js) に置きました。

Buffer 版はこんな実装。入力が Uint8Array なので、いったん ArrayBuffer 経由で Buffer オブジェクトを作り直してから write で書き込む手順。

```js
  const encode = (data, offset, string) => {
      const {buffer, byteOffset, byteLength} = data;
      return Buffer.from(buffer, byteOffset, byteLength).write(string, offset);
  };
```

TextEncoder 版はこんな実装。encodeInto を使うために subarray でオフセット位置の Uint8Array オブジェクトを切り出すオーバーヘッドがあるもの。

```js
  const encoder = new TextEncoder();
  const encode = (data, offset, string) => encoder.encodeInto(string, data.subarray(offset));
```

Buffer はバージョンによって中の実装が変わるから、内部は一緒だったりするのかも。

## デコード（UTF-8読み込み）

読み込み速度も似たような話だが、なぜか Buffer が速い。

- 32文字くらいまでの短いシングルバイト文字の文字列なら、Pure JS が圧倒的に速い。
- それを超える長いシングルバイト文字の文字列なら、Buffer が数倍速い。
- 256文字くらいまでの短いマルチバイト文字の文字列なら、Pure JS が速い。
- それを超える長いマルチバイト文字の文字列なら、そこまで大きな差がない。

![DECODE.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12419/7f509ab8-bf32-74c0-2e6b-aec5d25d6b80.png)

ベンチマークのソースコードは [text-decoder-bench-js](https://gist.github.com/kawanet/a66a0e2657464c57bcff2249286d3a24#file-text-decoder-bench-js) に置きました。

Buffer 版はこんな実装。入力が Uint8Array なので、いったん ArrayBuffer 経由で Buffer オブジェクトを作り直してから toString で取り出す手順。

```js
  const decode = (data, offset) => {
      const {buffer, byteOffset, byteLength} = data;
      return Buffer.from(buffer, byteOffset + offset, byteLength - offset).toString();
  };
```

TextEncoder 版はこんな実装。subarray でオフセット位置の Uint8Array オブジェクトを切り出すオーバーヘッドがあるもの。

```js
  const decoder = new TextDecoder();
  const decode = (data, offset) => decoder.decode(data.subarray(offset));
```

Pure JS が配列を作って ​String.fromCharCode.apply を使う実装なので、
単純な文字列結合を使った場合は、結果が違うかもしれない。

TextDecoder よりも Buffer が速い理由を予想してみると、
Buffer は TextDecoder とは別の独自実装を持っていて、
内部文字列表現が OneByteString だけで済むときは特別に高速になってるのかな。
ただし、マルチバイト文字が入り TwoByteString に切り替わるとバイト数のぶん遅くなる。
（グラフの右軸は文字数なので、全角 `Ａ` は UTF-8 で3倍のバイト数になる）

シングルバイト文字100文字くらいまで Buffer の速度がサチってるので、
たぶんオブジェクト生成のオーバーヘッドで頭打ちになるラインがこの辺なのか。
デコード処理の実装時の戦略としては、
エンコード時と同じように100バイトくらいを閾値にして、
Pure JS 実装 or Buffer/TextEncoder 実装を切り替えてもいいかも。

より細かく分類すると、Buffer が使える Node.js 環境では、
シングルバイト文字前提なら入力長 32 バイトくらい、
マルチバイト文字前提なら入力長 256 バイトくらいが、
Pure JS 実装 or Buffer 実装を切り替える閾値になるか。
Buffer が使えないブラウザ環境なら、入力長 256 バイトくらいが
Pure JS 実装 or TextDecode 実装を切り替える閾値になるか。

----

## 検証結果まとめ

- 短い文字列なら、オブジェクト生成オーバーヘッドの少ない Pure JS 実装が最速。
- 長い文字列なら、ネイティブの Buffer / TextEncoder / TextDecoder を使うべし。
- どうせバージョンによって挙動が変わるから、一概には言えないが、シンプルに考えると、100文字や100バイトくらいを切り替え閾値にしても良いかも。