Rust の ripgrep に C# で追いつくまで — 巨大ファイルは205秒から同等へ、8.6万本は19倍の負けから同等へ、複雑な検索は uvp が ripgrep の1.07〜3.5倍

技術解説

【追記 2026-10-16】いまの速さ — 複雑な検索は 1.07〜3.53倍から 1.22〜4.56倍に

本文の表は v1.7.3.7(2026年10月4〜5日)と試験版 uvp 1.8.1.3(10月6日)の値です。同じ Mac で、いまの配布版 v1.9.0(2026年10月14日)を測りました。

ripgrep ÷ uvf・uvp(1回目+2回目) 本文(当時) いま(v1.9.0)
複雑な検索3例・カーネル約6万本(uvp) 1.87〜3.53倍 2.35〜4.56倍
複雑な検索3例・1GB の1本(uvp) 1.07〜1.92倍 1.22〜2.13倍
検索7種・3GB/10GB(無料版 uvf) 項目ごとに 1.05〜1.24倍/1/1.01〜1.15倍 合計で 1.08/1.08倍(同等)
uvp の1回目(索引作り込み・3GB 文字列) 1/1.05 約 1/1.3(同等の範囲)

uvp の検索7種は、索引作りを含めて 3.44倍(3GB)・3.55倍(10GB)です。カーネル 8.6万本と Rust 版の表は、今回の測り直しには入っていません。詳しくはベンチマークとv1.9.0「Full Spectrum」の記事。

言語のせいではありませんでした。

ripgrep は Rust で、私の uvf・uvp は C#(.NET)で書いています。3週間前、50GB の検索で 205秒 対 56秒と、4倍近く負けていました。「Rust だから速いのか」と、何度も考えました。

3つの土俵で追いかけた記録と、同じ作りを Rust で書き直して確かめた結果を並べます。差がどこでついていたのかは、2章の途中で分かります。

先に結論: 差は言語ではなく「読む回数・開く回数・絞り方」。直したら無料版 uvf は ripgrep と同等に、索引を持つ uvp は ripgrep より速くなった

巨大ファイル(OpenStreetMap の XML 50GB)は、uvf が 205秒から 50.82秒になり、ripgrep(54.94〜55.81秒)と同等になりました。多数の小さなファイル(Linux カーネルのソース 8.6万本)は、2回目に ripgrep の19.3倍かかっていたのが、8問の2回計で 1/1.08(同等)に。有料版 uvp は 4.0倍です。複雑な検索(複数行のブロック・近い行の組み合わせ・CSV への収集)では、uvp が ripgrep(+perl・awk)の 1.07〜3.53倍。Windows 用の高機能 grep アプリと比べると 8.6〜43倍で、そのアプリには ripgrep も 7.9〜17.9倍の差をつけていました。そして、同じ作りを Rust で書き直した版は C# 版と同じ速さ(36.85秒 対 37.55秒)で、ripgrep との差は縮まりませんでした。Mac(Apple M4・10コア)・メモリ32GB・外付け USB SSD。詳細は記事末尾。

uvf・uvp は私が作っている UwView の検索コマンドです(uvf は無料版、uvp は有料版 UwView Pro)。いま配布している安定版は v1.7.3.6.9、v1.7.3.7 は Mac だけの先行版です。3章の複雑な検索は、次の版 v1.8 で入る機能(範囲 -range・伏せ字 -mask・整形 -format)を試験版(uvp 1.8.1.3)で測りました。数字ごとの版と日付は記事末尾にまとめています。


1. 巨大ファイル: 205秒から同等へ。効いたのは「読む回数」と「読み方」

50GB の XML(8.9億行)から「東京」(94,979件)を探す時間です。どれもキャッシュを捨てた直後(cold)。

時期・版 uvf ripgrep 比
2026-09-15・v1.6.0 205秒 56秒 ripgrep が 3.7倍速い
2日後 50.82秒 54.94〜55.81秒 同等

直したのは、Rust と C# の違いとは関係のないところでした。

  • ファイルを2回半読んでいた。 行の先頭位置を集める(約100秒)、探す(約82秒)、当たった行を読み直す(約20秒)。これを1回の読みにまとめた
  • mmap の順次読みは、ディスクの帯域の57%しか出ていなかった。 mmap で 551MB/s、pread を 4MB ずつにすると 966MB/s。素読み(dd 927MB/s)に並んだ
  • 改行の数え方を SIMD にしても、ディスクから読むときは1秒も縮まなかった。 律速はディスクだった(ただし後で、キャッシュから読む2回目では効いた。下の 1.7.3.6.9)

読み方で速さが変わるのは、Rust の ripgrep も同じです。ripgrep は大きいファイルを既定で mmap で読みますが、Windows 11 の 50GB・正規表現では --no-mmap にすると 282.84秒 → 97.83秒(2.89倍) になりました(macOS は変わらず)。言語より、どう読むかが先に効きます。

その後の主な変化です。

  • v1.7.3.6.7:正規表現と大小無視の併用(-E -i)が ripgrep の 1/1.68 → 1/1.07。目印に = と " を選んでいて、XML のほぼすべての属性に当たっていた。英字から選ぶようにした
  • v1.7.3.6.9:キャッシュに載った2回目が ripgrep を少し追い越した(3GB 0.29秒 対 0.31秒、10GB 0.88秒 対 0.97秒)。改行を16バイトずつまとめて数えるようにして、3GB で 0.15秒 → 0.028秒
  • C# に固有の事情が1つだけあった。 配布している uvf は NativeAOT(前もって機械語にした形)で、.NET の正規表現が解釈実行になる。全部の行に正規表現を当てる重い式だけ、同じフォルダーにある画面の本体(JIT が効く)に任せている

いまの位置(v1.7.3.7・検索7種・cold+hot の2回計):

大きい1本 uvf の比 uvp の比
3GB 1.05〜1.24倍(-E -v だけ 1/1.10) 1/1.05〜4.86倍
10GB 1/1.01〜1.15倍 1/1.10〜5.29倍
50GB 1.06〜1.23倍 1.68〜9.61倍

uvp の 1/1.05・1/1.10 は、索引(.uwvz)を作る1問目です。2問目からは索引を読みます。

2. 多数の小さなファイル: 19倍の負けから同等へ。効いたのは「開く回数」

巨大ファイルで並んだので安心していたら、Linux カーネルのソース(8.6万本・1.9GB)でまったく別の顔が出ました。

版 uvf の2回目(PageTransHuge) ripgrep 比
1.7.3.6.5 26.58秒 1.38秒 ripgrep が 19.3倍速い
1.7.3.6.7 3.40秒 1.44秒 ripgrep が 2.4倍速い

原因は、検索ではなくファイルを開く回数でした。8.6万本を開いて閉じるだけで、1本ずつだと 7.7秒かかります。前の版は、検索の前に「pbf ではないか」「圧縮ではないか」を確かめるため、全部を1本ずつ順に3回開いていました。名前で形式が決まるものだけ先に確かめ、それ以外は検索で開いたときに先頭を見て決めるようにしました。

続けて、5種類の検索を cold+hot で合計した値です。

版 uvf ripgrep 比 直したこと
1.7.3.6.7 52.18秒 31.50秒 1/1.66 開く回数を減らした(上)
1.7.3.6.9 36.21秒 30.22秒 1/1.20(同等) 空いた作業役が次のファイルを取りに行く形に。開くときは絶対パスを渡す(8並列で開いて読むだけで 2.33秒 → 1.59秒)。256KB 以下は1回で読み切る
1.7.3.7(8問) 60.85秒 56.25秒 1/1.08(同等) —

uvp はもっと遠回りしました。最初の版(1.7.3.6)は、8.6万本を1本の索引に束ねようとして落ちました(「どのファイルから来たか」の一覧が 20.9MB になり、上限の 1MB を超えた)。上限を直すと束ねられましたが、索引作りに 56.31秒。いまは 8.4秒(1問目の検索込み)で、8問の2回計は 14.02秒=ripgrep の 4.0倍です。

Rust で書き直してみた

ここで、いちばん気になっていたことを確かめました。同じ作りを Rust で書いたら、ripgrep に並ぶのか。

uvf の検索部分を Rust に移した ruvf を2つ作りました。1対1で移した port と、探すところを Rust の regex crate に任せた idiom です。出力は uvf と1バイトも違わないことを確かめてから、カーネル 8.6万本・5種類の検索で測りました(cold+hot・3回の中央値)。

ripgrep uvf(C#) ruvf-idiom(Rust) ruvf-port(Rust)
30.62秒 37.55秒 36.85秒 42.89秒

Rust 版は C# 版と同じ速さでした(idiom 対 uvf で 1.02倍)。ripgrep との差(uvf の 1.23倍)は、Rust にしても縮まりません。port が遅いのは、uvf の「ファイルごとに正規表現を組み立て直す」作りをそのまま移したからで(Rust の regex は組み立てが .NET より重い)、1回だけ組み立てれば idiom と同じになりました。起動の時間も、ruvf 1.7ms・ripgrep 2.7ms・uvf 4.7ms で、差は約3ms です。

差は言語ではなく、作りの側にありました。 見立てでは、ripgrep はファイルを広げながら探し始め、uvf は名前の順に出すために広げ終わってから探し始めます。これは C# のままで直せる話です。

1つだけ、Rust 版がはっきり速いところがありました。必須の文字列を取り出せない正規表現です(200MB で 2〜5倍)。理由は、NativeAOT の .NET の正規表現が解釈実行になることと、Rust の regex が1文字やバイトの並びでも先に行を絞ること。後者を C# の uvf に入れると、その式が 2.3〜6.1倍速くなり、[0-9]{4}-[0-9]{2} は ripgrep と同じ(1.34秒 対 1.33秒)、[ぁ-ん]{3,} は Rust 版より速く(0.45秒 対 1.17秒)なりました。Rust から学んだのは、言語ではなく絞り方です。

起動の時間・正規表現10種類の表・「Rust らしく書いたら遅かった」例などの全部は、Qiita に書きました → Rust で書き直せば ripgrep に並ぶのか

3. 複雑な検索: ripgrep は1行ずつ、uvp は段で組む

最後の土俵は、Windows 用の高機能 grep アプリの使い方として紹介されていた3つの検索です。どれも1行の照合だけでは書けません。

  • 例1:[ERROR] SessionStart から [INFO] SessionEnd までの複数行のブロックを取り、中に Error_Code: 503 を含まないブロックだけ残し、中の IP アドレスを x.x.x.x に伏せる
  • 例2:DatabaseTimeout と User_ID: 9999 が 10行以内にあり、IgnoredWarning が無いところ
  • 例3:順番がばらばらの行から User・Action・Time を取り出し、CSV に整えて集める

ripgrep で書くと、例1は rg -U -P の複数行の正規表現に perl をつなぎ、例2は rg で行を集めて awk で近さを判定、例3は rg -P -o -r '$1,$2,$3' です。uvp は v1.8 で、1つのコマンドで書けるようになります。

# 例1:503 を含まないセッションのブロックを、IP を伏せて書き出す
uvp app.log -seq '[ERROR] SessionStart,[INFO] SessionEnd' -range \
            -grep 'Error_Code: 503' -v \
            -mask '\b(\d{1,3}\.){3}\d{1,3}\b' 'x.x.x.x'

# 例3:User・Action・Time を取り出して CSV に(順番がばらばらでもよい)
uvp '*.log' '^(?=.*User: (\S+))(?=.*Action: (\S+))(?=.*Time: (.+?)\s*$)' -E \
            -format '$1,$2,$3' --csv --csv-header '名前,操作,日時' -out result.csv

測ったのは2つの土俵です。kernel はカーネルの *.c・*.h(約6万本・1.8GB)の約6%に例の断片を差し込んだもの、big は 1GB のログ1本。cold+hot の2回計で、uvp と ripgrep の結果はすべて一致しました。

土俵 例 Mac:ripgrep ÷ uvp Windows:ripgrep ÷ uvp Windows:高機能 grep アプリ ÷ uvp
kernel 例1 範囲・除外・伏せ字 1.87 1.85 20.26
kernel 例2 近い行・AND・NOT 2.14 1.65 22.92
kernel 例3 CSV に収集 3.53 2.39 42.94
big 例1 1.07 1/1.03 9.07
big 例2 1.46 1.09 8.62
big 例3 1.92 2.27 38.50

(1 より大きいほど uvp が速い。1/1.03 は uvp が 1.03倍遅い=同等)

高機能 grep アプリは、速さでは使えませんでした。 Windows だけで動き、試用版は結果を書き出せないので時間だけを測っています。しかも測ったのは同じ式で探すところだけで、収集・伏せ字・除外はさせていません。それでも uvp の 8.6〜43倍、ripgrep と比べても 7.9〜17.9倍かかり、kernel の cold は1例に2〜3分でした。画面で置換を確かめながら進めるような、GUI ならではの便利さはこの比較では測っていません。

ripgrep はやはり速いです。perl や awk をつないでも、uvp との差は 1.1〜3.5倍に収まります。uvp が上回るのは、索引で2回目の読みを短くし、段(範囲・除外・伏せ字・整形)を1回の流れで処理するからです。

正直に書くと、2回目(hot)だけで見れば、Mac の kernel の例1・例2 は uvp が 1/1.2・1/1.1(同等の範囲)で、索引を開く固定費が乗ります。Windows の kernel では、1回目(索引作り込み)に 65〜74秒かかっています(Mac は 6〜9秒)。原因はまだ調べていません。

学んだこと: Rust と C# の差は、ほとんど作りの差だった

  1. 速さを決めたのは、読む回数・開く回数・絞り方。 巨大ファイルでは読む回数、小さなファイルでは開く回数、正規表現では先に行を絞る手がかり。どれも言語を変えずに直せた
  2. C# に固有の事情は、NativeAOT の正規表現が解釈実行になることだけ。 重い式は JIT の本体に任せ、手がかりで先に絞れば、差は残らなかった
  3. 相手を「Rust だから速い」と決めつけない。 Rust の ripgrep も、読み方(mmap か pread か)で 2.89倍変わる
  4. 1回だけ探すなら、ripgrep が今も最高の道具。 索引を持つ uvp が上回るのは、同じ場所に何度もたずねるときと、段を組む複雑な検索のとき

正直に書いておくこと

  • 小さなファイルの2回目は、uvf がまだ遅い(カーネルで ripgrep の 1/1.4。1本ずつ開く固定費 約2.1秒)。2回計では同等(1/1.08)
  • uvp が複数ファイルを初めて束ねる1回目は遅い(平文と gz の混在12本で 24.95秒 対 39.78秒=ripgrep が 1.59倍速い)
  • uvp の2回目には約 0.12秒の固定費が乗る(1,000本に分けた XML で 0.043秒 対 0.126秒=ripgrep が 2.93倍速い)
  • 3章の uvp は試験版(1.8.1.3)で、範囲・伏せ字・整形は次の版 v1.8 で入る予定です
  • ruvf は比べるための実験で、配布する予定はありません
  • 秒数は環境によって変わります。OS をまたいで比べないでください

数字の条件

  • Mac(Apple M4・10コア・メモリ32GB)・外付け USB SSD。Windows の列は別の機械(Windows 11・x64・論理8コア・メモリ16GB)
  • cold は sudo purge のあと10秒待ってからの1回目、hot は続けて2回目。比は ripgrep の時間 ÷ こちらの時間(2回計は cold+hot の合計で比べる)
  • 1章:v1.6.0 と2日後は 2026年9月15〜17日(cold・uvf)。ripgrep の mmap は 2026年9月(ripgrep 15.2.0)。表は v1.7.3.7(2026年10月4〜5日)
  • 2章:1.7.3.6.5・1.7.3.6.7 は 2026年10月2〜3日、1.7.3.6.9 は 10月4日、8問は 1.7.3.7(10月5日)。ruvf は 10月4日(rustc 1.99.0・regex 1.13.1・memchr 2.8.3。uvf は 1.7.3.6.9)
  • 3章:uvp 1.8.1.3(試験版)・ripgrep 15.2.0・2026年10月6日。高機能 grep アプリは試用版で、時間だけを測った
  • すべての項目で、出力を ripgrep(または sed など)と突き合わせて一致を確かめた(不一致 0 件)

リンク

  • Linux カーネル 8.6万本で「開く回数」を数えた記事(Zenn): https://zenn.dev/amru195704/articles/5f2a6203ec7f61
  • Rust で書き直した ruvf と uvf・ripgrep の比較(Qiita): https://qiita.com/amru195704/items/149c10ff2be8c82c9bb7
  • ripgrep の mmap と --no-mmap を 50GB・3OS で比べた記事: https://uvp.y42u.net/blog/uvp-rg-no-mmap-50gb/
  • 改行の数え方が2回目の検索の4割を占めていた: https://uvp.y42u.net/blog/uwview-wf12-newline-hot/
  • 正規表現の比較(.NET と Rust で書けるもの・書けないもの): https://uvp.y42u.net/blog/uwview-wf09-regex-vs-ripgrep/
  • ripgrep にまだ負けているところを、全部並べる: https://uvp.y42u.net/blog/uwview-wf14-where-ripgrep-wins/
  • First Light の総集編(読む回数だけを減らした): https://uvp.y42u.net/blog/uwview-fl13-first-light-recap/
  • 実測の全データ(ベンチマーク一覧): https://uvp.y42u.net/benchmarks/
  • ソースコード(GitHub): https://github.com/amru195704/UwView

同じログに何度もたずねるなら、索引を持つ UwView Pro をどうぞ。2回目からの検索が一段速くなり、ログを約 1/9 の大きさで保管できます(全 OS 対応・買い切り/月額プランあり・14日間の無料試用)。


開発者より: アプリ・Kindle 本・公開プロジェクトの一覧は GitHub: amru195704 にまとめています。


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