最速の ripgrep に勝てる場所を1つだけ選ぶ — 読むバイト数を1/9にして、CLI から GUI へ渡す

技術解説

【追記 2026-09-17】3GBで負けたと思っていたら、勝っていた(ただし冷えていれば)

この記事には「3GB は ripgrep が速い」という趣旨の記述があります。それは ファイルが OS のキャッシュに載っているときの話でした。sudo purge の直後(=しばらくぶりにそのファイルを開くとき)に測り直すと、3GB でも uvp が 2.6倍速いです。

3GB・固定文字列「東京」 rg uvp
コールド(purge 直後) 3.26秒 1.25秒(2.6倍)
ウォーム(続けて2回目) 0.33秒 1.01秒

7種類の検索のうち6つで同じ傾向でした(uvp 1.6.3)。詳しくは 3GBで負けたと思っていたら、勝っていた(ただし冷えていれば) に書いています。計測は Mac(Apple M4 / メモリ32GB / 外付け USB SSD)で、Windows・Linux(非力なノートPC・VMware 上)では傾向が変わる可能性があります。本文の以下の数字は、そのまま(ウォームでの計測)です。

ripgrep には効率で勝てません。

258GB・45億行のファイルを ripgrep が読み終わるのは 278秒。同じファイルを wc -l が読み終わるのは 268秒。差は4%で、その4%のあいだ CPU はほとんど寝ています。この数字を見たとき、アルゴリズムで追いつく道は無いと分かりました。では、どこで戦うのか。

先に結論

258GB・45億行の1ファイルに、ripgrep と自作の索引方式 uvp を同じ作法で当てました(Apple M4 / 32GB / 外付けSSD、毎回 sudo purge/usr/bin/time -p の real、数字はすべて2回目=索引あり)。

  • ripgrep は「ファイルを1回読む時間」の1.04倍で走っています。CPU は1コアも使い切っていない(user/real = 0.04)。効率でこれに勝つ道はありません
  • 勝てる場所は1つだけ、「読むバイト数」です。索引と圧縮で 258.68GB → 28.6GB(1/9.04)。読む量が1/9なら、時間も 278.35秒 → 35.51秒
  • 1問目は必ず負けます(索引を作る 333.67秒)。元を取るのは 1.37問目。3GB は、キャッシュが温まっていれば何問聞いても ripgrep が速い(冷えていれば uvp が 2.6倍。上の追記)
  • 副産物がひとつ。索引を持った瞬間、CLI の結果をそのまま GUI に渡して、そこから何度でも絞り直せるようになりました。絞り込みの段は 0.24秒(50GB・v1.6.2)で、この往復が実用になっています

以前 商用エディタに勝てる場所を1つだけ選ぶ という記事で、機能数では戦わず軸を1つ選ぶ話を書きました。これはその CLI 版です。相手は ripgrep で、先に負けを全部書きます

前提: まともに戦うと必ず負ける

比較の前に、物理下限を出します。

$ /usr/bin/time -p wc -l us-260726.osm
4509830821
real 268.73

268.73秒=963MB/s。258GB を1回読み切るだけの時間です。これに対して ripgrep は、

ヒット rg -c
1 New York 100,492 275.80
2 Brooklyn 30,871 278.35
3 Central Park 14,306 276.75
4 Statue of Liberty 88 293.85
5 coffee_shop 36,506 346.21
中央値 278.35

物理下限の 1.04倍。ヒットが 88件でも 10万件でも同じ。ripgrep は「読み終わったら終わる」ツールで、読む速さはディスクが決めています。

CPU の使われ方を見ると、もっとはっきりします。

rg -c 'New York'   real 275.80   user 11.55   sys 51.60

275秒のうち、CPU を使ったのは 11秒。10コアの M4 で、1コアの4%しか使っていません。使う必要がないからです——ディスクが追いつかない。

ここが出発点です。I/O 律速の世界では、CPU を速くしても、並列にしても、何も起きません。アルゴリズムをどれだけ磨いても、268.73秒の壁は動かない。

(脇道ですが、これは「Rust 製なら速い」が一般則ではない理由でもあります。1本のファイルを並列走査する amber は同じファイルで 357.69秒、ripgrep より遅い。並列化で CPU 時間を 25〜36倍使っても、ディスクが1本なら速くならない。258GB の記事 に実測があります)

残った手は1つ: 読むバイト数を減らす

ディスクの速さが上限なら、読む量を減らすしかありません。

uvp は最初に索引と圧縮を作ります。

$ /usr/bin/time -p uvp convert us-260726.osm
real 333.67

333.67秒。物理下限の1.24倍で、258GB を1回読んで .uwvz を作ります。できたファイルは 28,608,409,953 byte=元の 1/9.04。以後の検索はこの 28.6GB だけを読みます。元の 258GB は消しても構いません。

読むバイト数 963MB/s での下限 実測(中央値) 下限との比
rg -c 258.68GB 268.73秒 278.35秒 1.04倍
uvp 28.6GB 29.72秒 35.51秒 1.20倍

どちらも自分の I/O 下限に張り付いています。違うのは読まねばならないバイト数だけ。そして1バイトあたりの効率では ripgrep のほうが上です(1.04 対 1.20)。勝っているのはアルゴリズムではなく、前払いです。

CPU の使い方も反転します。

rg   real 275.80   user  11.55   → user/real = 0.04(I/O 待ち)
uvp  real  34.70   user 168.63   → user/real = 4.86(約5コアで展開・検索)

読む量が1/9になった瞬間、ディスクが余って CPU が足りなくなる。律速がひっくり返る、というのがこの方式の正体です。

負けの表

軸を1つに絞ると、他は負けます。先に全部書きます。

場面 結果 理由
1問目 ripgrep 278.4秒 対 uvp 369.3秒(1/1.33 索引を作る 333.67秒を払う。全サイズで一定して 1.2〜1.3倍負け(3GB 1/1.18・10GB 1/1.31・50GB 1/1.29)
3GB(ウォーム) 2問目以降も ripgrep が速い(1/3.1 ファイルが OS キャッシュに載り、rg の全走査が 0.33秒。uvp は起動+索引ロードの固定費を必ず払って 1.01秒。絶対差は 0.7秒(冷えていれば 3.26秒 対 1.25秒で uvp)
-head N(先頭だけ) rg \| head 0.05秒 対 uvp 0.32秒 v1.6.2 で全件走査(7.01秒)を止めて先頭 N 件で切るようにしたが、残る 0.25秒は .NET の起動。両者サブ秒・勝てはしない
1バイトあたりの効率 rg 1.04倍 対 uvp 1.20倍 上の表のとおり。展開のぶん uvp が重い
文脈つきの絞り込み(-C N 段ごとに約8.5秒(50GB) 前段のヒット行の外も見るので .uwvz を読み直す。±0 なら 0.24秒

3GB は ripgrep の土俵で、そこは譲ります。差が 0.4秒なので譲っても痛くありません。索引が効き始めるのは、ファイルがメモリに収まらなくなってからです。

何問目で元を取るか

CLI(N) = N × 278.35            (rg・5問の中央値)
uvp(N) = 333.67 + N × 35.58    (索引作成 + .uwvz 検索の平均)

N* = 333.67 ÷ (278.35 − 35.58) = 1.37
質問数 N ripgrep uvp rg ÷ uvp
1 278.4 369.3 1/1.33(ripgrep の勝ち)
2 556.7 404.8 1.38倍
5 1,391.8 511.6 2.72倍
20 5,567.0 1,045.4 5.33倍

2問目で逆転します。式は索引方式全般に当てはまります(T_index ÷ (T_scan − T_search))。10GB・50GB でも同じ形で、2回目以降は 10GB で 5.5〜6.9倍、50GB で 7.3〜10.2倍です。

ここまでが「速さ」の話です。ただ、正直に言うと、これだけなら記事にする価値は薄い。索引を作れば2回目が速いのは、当たり前だからです。

副産物 — CLI と GUI が地続きになった

前払いをした結果、予定していなかったものが手に入りました。

uvp の末尾に -open を付けると、CLI の結果がそのまま GUI に渡ります。

uvp us-260726.osm.uwvz 'New York' -C 0 -grep hospital -v -grep school -v -grep Brooklyn -open

ターミナルで uvp の絞り込みと集計を実行し、末尾に -open を付けたところ。CLI 側の結果が出たあと GUI が起動する

これで起きることを順に書きます。

  1. CLI が 258GB(の索引 28.6GB)を検索し、3段の絞り込みで 35件に落とす — 34.88秒
  2. その 35件を一時ファイルで GUI に渡す。GUI は検索し直さない(v1.6.2。それまでは条件だけ渡して GUI で再検索していたので、巨大ファイルでは同じ検索を2回待っていました)
  3. GUI が開く。索引は既にあるので、開き直しに 258GB を読まない
  4. 一覧の行をクリックすると本文の該当位置へ。±N 行の文脈を出す。別の語で絞り直す——ここが 0.24秒/段(v1.6.2)

GUI 側で CLI の結果を受け取った状態。結果一覧・集計ランキング・本文が並び、ここから別の語で再検索できる

4 が肝です。ここは正直に書いておくと、rg との比較で絞り込みだけ不自然に遅い時期がありました。単発検索は 7〜9倍速いのに、段を重ねた絞り込みだけ 3〜4倍にとどまる。調べたら段ごとに読み直している箇所があり、v1.6.2 で直しました。50GB・3段で 29.46秒 → 8.10秒、1段あたり 0.24秒です。258GB の3段も 34.88秒で、単発(34.85秒)と同じになりました。原因と切り分けの手順は別の記事に書きます。

これで、ようやく次の作業が成立します。

スクリプトで絞って、人が見て、見ながらもう一度絞る。
絞り直すたびに 278秒待つ(CLI だけ)のでも、GUI を開くたびに 258GB を読み直す(GUI だけ)のでもなく、索引を1回作って、CLI と GUI の両方がそれを使う。

grep のパイプは「段を増やしても1パス」で、これは CLI の設計として正しい。でも条件を変えてもう一度となると、また1パス。GUI は条件を変えるのは速いが、開くのが遅い。索引を挟むと、この2つの「遅い」が両方消えます。

CLI と GUI は同じ関数を呼んでいるので(絞り込み・集計・順序)、CLI で出た件数と GUI で出た件数は必ず一致します。検証は 81項目+gz/zip 12項目+258GB 全項目を rg / sed とバイト単位で突き合わせて、製品側の誤りは 0 件でした。

何をしたのか、まとめ

  • ripgrep には効率で勝てません。物理下限の1.04倍、CPU 使用率4%。完成しています
  • 勝てる場所は「読むバイト数」だけでした。1/9 にしたら 1/8 の時間になり、それ以上でもそれ以下でもない
  • 1問目は必ず負けます。分岐点は 1.37問目。3GB は、温まっていれば何問でも負けます。そこは譲ります
  • 前払いの副産物として、CLI と GUI が同じ索引を使い、結果を受け渡せるようになりました
  • 「速い」ではなく「調査1件の中で、CLI と GUI を行き来しても待たない」が、選んだ1つの場所です

使っている道具

uvpUwView Pro v1.6.2 に付属します(Windows・macOS・Linux、1ライセンスで全OS。買い切り/月額。14日間の無料試用あり——試用中も uvp は使えます)。無料版 UwView にも uvf が付いていますが、索引を持たないので検索と -open だけで、速さはありません。

数値はすべて当方環境での自社実測で、独立ベンチマークではありません。ripgrep は多数ファイルの横断検索では最速の部類で、この記事はそこを争っていません。巨大な単一ファイルに、何度も聞く場面だけの話です。設定の問題があればご指摘ください。確認のうえ訂正します。

測定条件と使ったコマンド

  • Apple M4 / メモリ32GB / macOS 26.3.1 / 外付けSSD(963MB/s)
  • ripgrep 15.2.0 / uvp 1.6.1〜1.6.2 / 各計測の前に sudo purge、計測間に150秒(258GB)または60秒(50GB)のクールダウン
  • OpenStreetMap アメリカ全土 XML 258,679,440,228 byte / 4,509,830,821 行、同 日本 51,254,526,392 byte
/usr/bin/time -p wc -l us-260726.osm                       # 物理下限
LC_ALL=C rg -c -F 'New York' us-260726.osm                 # rg
uvp convert us-260726.osm                                  # 索引 .uwvz
uvp us-260726.osm.uwvz 'New York' --no-line-number         # uvp(2回目)
uvp us-260726.osm.uwvz 'New York' -C 0 -grep hospital -v -grep school -v -grep Brooklyn -open

リンク

タイトルとURLをコピーしました