2023年9月15日金曜日

Deno で readLinesSync() を作った

Deno / Node.js にはファイルから非同期処理で 1行ずつ読み込む readLines() があります。 昔よりだいぶ楽になったなあと思うのですが、実行速度がすこし遅いので、同期処理で 1行ずつ読み込む readLinesSync() を作りました。

readlines-sync

Deno ならこんな感じで使えます。 Node.js / Bun ではバイナリを扱ったことがなかったので気付かなかったのですが、 dnt 経由で使うか、Uint8Array でブロックごとに読み込むコードを書く必要がありそうです。
import { readLinesSync } from "https://raw.githubusercontent.com/marmooo/readlines-sync/main/mod.js";

const file = await Deno.open("FILE");
for (const line of readLinesSync(file)) {
  console.log(line);
}
file.close();

ベンチマーク結果は以下です。最後の 3つが自作したもので、sync1 が一番最初に実装したもの、 sync2 はキャッシュのヒット率を考慮したもの、readLinesSync は sync2 を Deno / Node の API に近づけた完成物です。 Node.js の readLines() もベンチマークに入れたかったですが、まだ Deno からは使えないのでパス。 ただ node:readline と同じ速度と予想はできます。
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
runtime: deno 1.36.4 (x86_64-unknown-linux-gnu)

bench.js
benchmark            time (avg)        iter/s             (min … max)       p75       p99      p995
--------------------------------------------------------------------- -----------------------------
node:readline          1.34 s/iter           0.7       (1.29 s … 1.44 s)    1.36 s    1.44 s    1.44 s
npm:n-readlines        3.46 s/iter           0.3       (3.42 s … 3.56 s)    3.48 s    3.56 s    3.56 s
readLines              3.45 s/iter           0.3       (3.37 s … 3.51 s)    3.48 s    3.51 s    3.51 s
TextLineStream         2.16 s/iter           0.5       (2.14 s … 2.17 s)    2.17 s    2.17 s    2.17 s
split                  1.11 s/iter           0.9        (1.1 s … 1.12 s)    1.11 s    1.12 s    1.12 s
sync1               713.78 ms/iter           1.4 (711.41 ms … 717.82 ms) 715.81 ms 717.82 ms 717.82 ms
sync2               710.79 ms/iter           1.4 (708.41 ms … 719.14 ms) 711.09 ms 719.14 ms 719.14 ms
readLinesSync          750 ms/iter           1.3 (748.78 ms … 751.63 ms) 750.44 ms 751.63 ms 751.63 ms
少なくとも Deno でよく使われる readLines() の 4倍くらい早いです。 というか readLines() が遅すぎるんだよなあ。 同期処理のほうが早いので Standard Library でも今回作ったものはぜひサポートして欲しい機能です。 そのため Third party modules に上げるべきかは微妙で、今のところ登録していません。 (とはいえ作られる気配がないので諦めるべきだろうか…?)

sync メソッドが用意されていないのは昔から疑問ですが、理由はわかっています。 readLinesSync() を真面目に作ろうとするなら普通は BufReader を改造したくなりますが、 BufReader.readline() が async、BufReader.readSlice() が async、Reader.fill() が async、Reader.read() が async になっていて、 それらすべての基底クラスを同期処理に変換しないと同期処理にできないためです。 このへんは設計的にどうなのかと感じなくもないのですが、JavaScript の async 感染的なところもあり複雑です。

BufReader 経由の実装は公式からも否定されているので、Deno/Node に依存しない Uint8Array から作ったのが、今回の代物です。 JavaScript 世界の sync/async の最適化のなさは不思議でしかないのですが、解決されることはあるのかなあ。 2023-09-22 追記: Deno 1.37.0 になって TextLineStream が高速になりました。 2023-11-24 追記: readLines が deprecated になりました。TextLineStream が今後の主流になりそうです。 それは良いのですが、現状だと TextLineStream が dnt で使えないのが難点です。 Custom Shims を書くのもちょっとなあ…。ま、そのうちなんとかなるでしょう。

0 件のコメント: