世界を動かす技術を、日本語で。

lsr: io_uringを用いたls

概要

lsr は、独自のIOライブラリ ourioio_uring を活用した超高速lsコマンド実装。 従来のlsや他の代替ツールと比較して システムコール数が桁違いに少ないZig言語StackFallbackAllocator を用いて更なる最適化を実現。 ベンチマーク で示される圧倒的な性能とコンパクトなバイナリサイズ。 tangled.sh で開発・公開、フィードバックやアイコンリクエスト歓迎。

lsr: 超高速lsコマンドの誕生

  • lsr は、ls(1)コマンドの機能を ourio ライブラリで再実装した高速版ツール。
  • io_uring を徹底活用し、I/O処理のほとんどを カーネル空間で非同期化
  • 既存のlsやeza、lsd、uutils lsより 圧倒的に高速・低オーバーヘッド を実現。

ベンチマーク結果

  • hyperfine を用いたディレクトリ内ファイル数nごとの測定。

    • n=10, n=100, n=1,000, n=10,000で比較。
  • lsr の実行時間は他ツールの 1/2〜1/10 程度。

  • strace -c によるシステムコール数も 桁違いに少ない

    • 例: n=10,000でlsrは 848回、lsは 30,396回

lsrの内部構造

  • プログラムは 3段階構成
    • 引数解析
    • データ収集( I/Oのほぼ全て
    • データ表示
  • ディレクトリオープン、stat、lstat、各種ファイル読み出し を全て io_uring経由 で実施。
  • stat呼び出しをバッチ化 し、システムコール回数を大幅削減。

メモリアロケーションと最適化

  • Zig stdlib StackFallbackAllocator を利用。
    • 1MBの固定メモリ を先行確保、不足時のみ他アロケータへフォールバック。
    • mmap等のシステムコールも削減
  • libc非依存、動的リンク不要 の静的バイナリ。
    • GNU lsよりも小さい: ReleaseSmallビルドで 138.7KB (lsは 79.3KB)。

他ツールとの比較・考察

  • lsd は各ファイルごとに clock_gettime を5回程度呼び出しており、詳細は不明。
  • uutils ls はsyscall数は少ないが、 ソート処理 がボトルネック。
    • lsrも 30%程度の時間をソート に費やすが、それでも高速。
  • io_uringの威力 を実感できる好例。
    • サーバー用途など、他分野でも 大幅な効率化の可能性

開発・コントリビュート情報

  • tangled.sh を利用して開発・公開。
    • バグ報告やアイコンリクエストは atprotoアカウント+appパスワード で誰でも可能。
  • リポジトリ や詳細は下記リンク参照。
    • https://tangled.sh/@rockorager.dev/lsr

まとめ

  • lsr はls互換の超高速ツールで、 io_uringourio による徹底的な最適化が特徴。
  • システムコール削減・高速処理・小型バイナリ の三拍子。
  • 今後のサーバーや高性能I/Oツール開発への示唆 を与えるプロジェクト。

Hackerたちの意見

いいね!なんで全てのコマンドラインツールがio_uringを使わないのか理解したいな。例えば、USB 3.2 Gen 2のNVMeはピーク740MB/sしか出ないけど、aioやio_uringを使うと1005MB/s出るんだよね。毎回たくさんのファイルを同時にコピーしてるわけじゃないけど、キューの長さの戦略やロックが少ないのも助けになってると思う。

一つの理由は、最近の最先端のインストールだけじゃなくて、全てのLinux環境で動くようにするためだね。

おそらく、ポータビリティを重視する歴史的な好みがあって、#ifdefがたくさんあるとプラットフォームやバージョン特有のものが採用されるのが遅くなるんだろうね。でも、今の時点では、さまざまなPOSIX系プラットフォーム間のポータビリティの利点はかなり低くなってる。

ポーの法則がまたやってきた。

確か、io_uringは初期にかなり重大なセキュリティ問題があったんだよね(数年前)。今は修正されてるはずだけど、それが普及を妨げたかもしれない。

それは素晴らしいスピードアップだね。これってどのツールなの?

io_uringはセキュリティの悪夢だよ。

io_uringは非同期インターフェースで、効果的に使うにはイベントベースのアーキテクチャが必要なんだ。でも、多くのコマンドラインツールはまだシンプルな直列スタイルで書かれてる。もしCに非同期やそれに似たメカニズムがあったら、非同期プログラミングを直列でやってるふりができて、移植が楽になるんだけどね。そうじゃないと、かなりのリファクタリングが必要になるよ。それに、io_uringはまだ安定してないし、10年後にはもっと新しいハードウェアを活かすための別のメカニズムに取って代わられるかもしれない。だから、io_uringが定着するのを待つのはかなり現実的な戦略だと思う。10年後には、自動で書き換えをしてくれるツールやAIが出てるかもしれないしね。

なんで全てのコマンドラインツールがio_uringを使わないのか理解しようとしてるんだけど。まだ新しいからね。lsコマンドを含むcoreutilsパッケージ(それを作るために統合された3つのパッケージも含む)は数十年前のものだし、io_uringはその後に登場した。共有リングバッファスタイルのシステムコールが、従来の同期システムコールに取って代わるには時間がかかるだろうね。

io_uringは最近のものだね。

たくさんのファイルがあるNFSサーバーに対してどうパフォーマンスが出るのか気になるな、特にちょっと微妙な接続の上で。信頼性のないネットワークサービスをブロッキングのPOSIXシステムコールの後ろに置くのは、NFSがひどい設計選択である主な理由の一つだよね(壊れたNFSフォルダから読み込んでるアプリをctrl+cしようとしたことがある人なら分かる)。でも、io_uringがその悪い部分を少しでも軽減してくれるのか気になる。

壊れたNFSフォルダから読み込んでるアプリでctrl+cを試したことがある人なら誰でも分かると思うけど、理論的には「intr」マウントがハングしたリモートサーバーを待ってる操作を信号で中断できるようにしてたんだ。でも、Linuxはずいぶん前にそのオプションを削除しちゃったんだよね[1](FreeBSDはまだサポートしてるけど)[2]。「soft」がLinuxでの唯一の回避策かもね。 [1]: https://man7.org/linux/man-pages/man5/nfs.5.html [2]: https://man.freebsd.org/cgi/man.cgi?query=mount_nfs&sektion=...

Sambaもね。

NFSのデザイナーたちは、分散システムを高い一貫性と可用性を持つシステム(ハードドライブ)にエミュレートすることにしたんだよね。これは合理的なトレードオフだったと思う。lsみたいな既存のツールが、ディレクトリをリストしている最中にサーバーが再起動することに対応する必要がなかったからね。(元々のNFSプロトコルはステートレスだから、クライアントはサーバーの再起動に耐えられる。)じゃあ、編集中のファイルをホストしているサーバーが応答しなくなったらviはどうするの?こういうエラーハンドリングを持ってるツールはないよね。io_uringがこれをどう解決するのか分からないけど、基盤のNFSコールがタイムアウトしたらエラーを返すのかな?諦めてエラーを返すまでどれくらい待つの?

本当に面白いね、違いは実際にある。ただ、もう少し良いカラーサポートが追加されるといいな。僕は「eza --icons=always -1」ってコマンドをlsとして設定してるんだけど、すごく見栄えがいいんだ。一方でlsr -1を使うと、基本的なことは同じだけど、色の違いがある。lsrも出力に色を付けるけど、ezaほど多くのことを知らないんだよね。例えば、.opusはezaでは音楽アイコンとして正しい色(僕の場合は緑っぽい?)で表示されるけど、lsrでは普通のファイルとして表示される。後悔は全くないけど、パッチを当てるのは結構簡単だと思うし、これが本当に堅牢で速いのは認めざるを得ない。もっとこういうのをcatや他のシステムユーティリティ用にも作ってくれないかな?それと、tangled.shを使ってるのも好きで、atprotoを使ってるのも面白いね。zigで書かれてるのも、個人的にはrustよりも初心者には触りやすい気がする(ごめんねrustaceans)。

カラーリングサポートについては、LS_COLORS / dircolorsを実装するのが一番いいと思う。俺のGNU lsは見た目がいい感じだよ。

“bat”はかなり良い現代版の“cat”だよ。 https://github.com/sharkdp/bat

でも、io_uringはgetdentsをサポートしてないんだよね。だから、主な利点は一括でのstat(ls -l)ってことになる。前の結果を処理してる間にgetdentsが進行中だといいんだけど。

POSIXがNFSの「readdirplus」操作(getdents + stat)を採用すれば、io_uringの利点がいくつか無くなるかもしれないね。

これ、io_uringを使った時の期待される amortized performance increase のデモとしては面白いね。使い方のチュートリアルとしても。なんでezaみたいなものから乗り換える必要があるのか理解できない。1万ファイルをリストする時、違いは40msと20msの間なんだ。コマンドを一回実行しただけじゃ全然気づかないよ。

俺は数百万のJSONファイルが入ったディレクトリを持ってるんだけど、lsやduは数分かかるんだ。coreutilsのほとんどは、実際に現代のSSDを活用するには速すぎないんだよね。

そうそう、これはio_uringの使い方をもっと学ぶためのちょっとした楽しい実験として書いたんだ。実際の時間の節約は微々たるもので、人生全体で5秒くらいかな。それが目的じゃなかったけどね、ハハ。

プロジェクトの作者だよ!これについてちょっと書いたのがここにあるよ: https://rockorager.dev/log/lsr-ls-but-with-io-uring

GNU lsに対するスピードアップのどれくらいがローカライズ機能の欠如によるものなの?君の結果表は、私のローカルでの観察とほぼ一致してるよ:13,000ファイルのディレクトリでは、ls -alが33msかかる。でも、その時間の25%はlibcのstrcollに使われてる。LC_ALL=Cだと27msだけど、これが君のプログラムの時間に近づいてるね。

私のbfsプロジェクトもio_uringを使ってるよ: https://github.com/tavianator/bfs/blob/main/src/ioq.c lsrとbfs -lsを比べるのが気になるな。bfsは複数スレッドが有効なときだけio_uringを使うけど、bfs -j1でも使う価値があるかもしれないね。

(ありがとう!それをメインリンクにするよ(背景情報が多いから)で、リポスレッドも上に含めるね。)

これは素晴らしいね。今、C++プロジェクトを進めてるんだけど、最終的には全部か一部をZigに移行することを考えてる。僕の小さなlibevringはまだ若いから、ourioに置き換えることにはとてもオープンだよ。こういうプロジェクトにC/C++バインディングを持つことについてどう思う?

いい文章だね。おそらく、抽象化のコストを測ってるんじゃないかな。特に、ロケールに基づく文字列やUTF-8文字を扱えるルーチンは、結果を出すまでにやることが多いからね。これは、SunでI18Nプロジェクトをやったときに直面したことだよ。私の経験では、プログラムが「そのまま動く」環境の数と速度には直接の相関関係があった。元々のUNIXのls(1)は、最大サイズのファイル名で、面倒な文字は許可されず、すべて7ビットASCII文字で表現できて、神が意図した12ビットのメタデータだけだったから、かなり速かったんだ。VFSみたいに、ソースファイルシステムを「期待される」ファイルシステムのパラメータにマッピングするものを追加すると、遅延が生じる。異なる文字セットをマッピングする?遅延が増える。ディスプレイ用の色?遅延が増える。小さなコストが積み重なっていくんだ。1: 「internationalization」みたいな長い単語を最初と最後の文字、そして間の文字数に縮めたのを初めて見たときは笑ったよ。2: それはユーザー、グループ、その他のための読み取り、書き込み、実行、setuid、setgid、そして「sticky」だね。 :-)

その時間スケールでは、ハイパーファインよりもtim(https://github.com/c-blake/bu/blob/main/doc/tim.md)を使った方がいいよ。名前が同じだからってだけじゃなくてね!それは「time」の文字を一つ削った幸運な偶然だよ。:-) Nimにいると、ちょっと挑戦的になるかもしれないけど。

.mjsや.cjsのファイル拡張子にはアイコンがあるのに、.c、.h、.shにはないのが面白いね。

syscallsを約35倍削減するのが「たった」2倍のスピードアップになるって、なんか不思議だね(ls -laのベンチマークと比べて)。

以前に読んだ他のio_uringプロジェクトのベンチマークをぼんやり覚えてるんだけど、io_uringのシステムコールは置き換えようとしていた他のシステムコールよりも高コストだって言ってた気がする。期待ほどではないにしても、まだ大きな改善だよね。その投稿を思い出せたらいいんだけど、その印象はずっと頭の中に残ってるんだ。

これらのシステムコールはほとんどVDSOを通ってるから、あんまりコストはかからないよ。

lsdが何をしているのか全然わからない。ソースコードは読んでないけど、straceを見た感じだと、ファイルごとに約5回clock_gettimeを呼んでる。なんでだろう?わからない。内部でステップのタイミングを計ってるのかな?それとも各タイムスタンプの「X分/時間/日/週前」みたいなことを計算してるのかな?(アクセス、作成、変更、...)。別のライブラリ関数の古い遺物かもしれないね...

これ、今の時代は実際のシステムコールじゃないはずだよ;vDSOで処理されるべき(man 7 vDSO)。でも、zigはそれを使ってないのかもね。

ちょっと脱線するけど、ハードウェアがサポートしている場合、企業向けサーバーで10G NICを使ったときに、io_uringがLD_PRELOADに対してどれくらいのマイクロ秒を節約できるか知ってる?Mellanox 4か5だと仮定しよう。私の理解では、各々が約10usの節約をもたらすらしいけど、もっと少ないかも。いくつかのベンチマークに基づいているけど、明確にそれに焦点を当てたものではなくて、あまり正確ではない実験があった。どうやら、節約は積み重ならないみたいだね。実際の経験に基づいた数字はある?