ripgrep が 2.9倍 速くなることがある — 50GB で `–no-mmap` を測ったら、OS で答えが3通りに割れた

技術解説

51GB のファイルを ripgrep で検索するとき、--no-mmap を付けると速くなることがあります。Windows では最大 2.89倍。Mac では何も変わりません。Linux では、検索の中身によって速くも遅くもなります。

これは ripgrep の欠陥の話ではありません。既定のメモリマップ読みは、多くの場合いちばん良い選択です。 ただ「多くの場合」であって、いつでもではなかった、という話です。

先に結論

同じ 51.25GB の1本のファイル(OpenStreetMap 日本を XML に展開したもの・51,254,526,392 バイト)に、
同じ2種類の検索を掛けました。すべてキャッシュを捨てた直後(コールド)の値です。

環境 検索 既定(--mmap --no-mmap 効き目
Windows 11 固定文字列 83.74秒 68.50秒 1.22倍 速い
Windows 11 正規表現+除外 282.84秒 97.83秒 2.89倍 速い
macOS 固定文字列 55.38秒 55.70秒 変わらない
macOS 正規表現+除外 59.40秒 58.47秒 変わらない
Linux(仮想機・メモリ8GB) 固定文字列 約83秒 約100秒 約1.2倍 遅い
Linux(仮想機・メモリ8GB) 正規表現+除外 約172秒 約105秒 1.64倍 速い

6通りのうち、3つで速くなり、2つで変わらず、1つで遅くなりました。

読み取れることは3つです。

  • Mac は何も考えなくていい。 どちらでも同じです。既定のままで最善が出ています。
  • Windows の 51GB は、既定が不利。 とくに重い検索では3倍近く損をします。
  • Linux は検索の中身で逆転する。 軽い検索なら既定が速く、重い検索なら --no-mmap が速い。

なぜ測ることになったのか

もともとは自分たちの CLI(UwView の無料版に付いている uvf)を ripgrep と比べていました。
3GB・10GB・50GB を Mac・Windows・Linux で測っていたところ、Windows の 50GB だけが妙だったのです。

同じ7種類の検索を掛けた合計で、Windows と Mac の比は 3GB が 1.09倍、10GB が 1.45倍。
機械の差として自然な範囲です。ところが 50GB だけ 2.25倍に跳ね上がりました。
しかも内訳を見ると、-E -v(正規表現で「当てはまらない行」を出す)の1項目が 283秒——
Mac の 58秒に対して 4.9倍です。

「機械が遅いから」では説明がつきません。サイズが上がるほど比が悪化する理由がないからです。
それで、切り分けの計測を書きました。

測り方

  • ファイル: OpenStreetMap 日本の XML、51,254,526,392 バイトの1本
  • 検索1(固定文字列): rg -n -F '東京' <file> — ヒット 94,979行
  • 検索2(正規表現+除外): rg -n -v '^ +<' <file> — ヒット 3行
  • ripgrep 15.2.0(Linux のみ 15.1.0)
  • 毎回キャッシュを捨てて10秒待ってから測る(macOS は sudo purge、Linux は drop_caches、Windows は RAMMap の -Et を管理者権限で)
  • 時間は同じスクリプトで秒まで記録。出力の行数も毎回記録し、--mmap--no-mmap で一致することを確認

環境は次のとおりです。秒数を環境またぎで比べないでください。 見るのは同じ機械の中の2本です。

macOS Apple M4 / メモリ32GB / 外付け USB SSD
Windows 11 HP Spectre x360(Core i7-1165G7・4コア8スレッド・15W級)/ メモリ15.6GB / Intel Optane H20 with SSD 512GB
Linux 上記 Mac 上の VMware・仮想2コア / メモリ8GB

犯人はディスクでも CPU でもなかった

Windows で先に疑ったのは、ファイルの置き場所と断片化でした。違いました。
ツールを通さずに同じ 3GiB を読むと、51GB ファイルの先頭 1159 MB/s・中間 1134 MB/s・末尾 1168 MB/s
きれいに揃っています。このディスクは素で 1.1GB/s 出るので、51.25GB を読むだけなら約45秒が下限です。

次に疑ったのは CPU の熱だれです。15W の薄型ノートですから、5分近い連続負荷なら落ちてもおかしくない。
これも違いました。 クロックは開始前も終了後も 2803MHz。3GB の同じ検索を5回続けても、
2回目以降は 7.1秒前後で一定です。落ちません。

残ったのが読み方でした。そして --no-mmap を付けた瞬間、282.84秒が 97.83秒になりました。

なぜこうなるのか(ここからは推測です)

メモリマップは、ファイルをメモリのように見せる仕組みです。実際にはページに触るたびに OS が
ディスクから読み込みます。ファイルがメモリに収まっていれば、これは非常に効率的です。
コピーが1回減りますから。

問題は収まらないときです。51.25GB は、Windows 機の 15.6GB にも Mac の 32GB にも収まりません。
すると読んでいるあいだ中、ページを入れては捨て、入れては捨て、を繰り返すことになります。
この出し入れの代償が、OS によって違うのだろう——というのが、いまのところの見立てです。

ただし正直に書いておくと、これは測定結果から逆算した説明で、確かめたわけではありません。
とくに Linux で「軽い検索だと既定が速く、重い検索だと --no-mmap が速い」と逆転する理由は、
きれいに説明できていません。ご存じの方がいたら教えてください。

実用の指針

ここまでの結果を、そのまま使える形にすると次のようになります。

Windows で 10GB を超える1本のファイルを ripgrep で検索するなら、--no-mmap を一度試してください。
付けるのは1語です。損はしません。

rg --no-mmap -n 'パターン' huge.log

Mac では何もしなくて構いません。 差がありません。

Linux では、重い検索(正規表現・-v-i の組み合わせ)のときだけ試す価値があります。
単純な固定文字列なら、既定のままのほうが速いという結果でした。

繰り返しますが、これは ripgrep を責める話ではありません。 既定が mmap なのは、
ふつうの使い方——ソースツリーを再帰的に検索する、数MBから数GBのログを見る——では正しい選択です。
今回の条件は「メモリに収まらない1本のファイル」という、かなり端のほうの使い方です。

うちの道具の話(短く)

比較の相手側として、自分たちの uvf にも触れておきます。

uvf はメモリマップを使いません。決まった大きさの入れ物に、順番に読み込んでいくだけです。
そのため、3つの環境で同じように動きます。当たり外れがありません。
Windows の 10GB・固定文字列では、コールドで ripgrep 15.91秒に対して uvf 7.22秒。
2.2倍の差が付きました。Mac では 3GB のコールドが 3.26秒 対 3.32秒でほぼ互角ですから、
この差は「Windows で mmap が不利だったぶん」だと考えています。

ただし、裏を返すと不利もあります。 Linux では軽い検索で mmap のほうが速いのに、
uvf はそれを使っていません。使えばもう一段速くなる余地がある、ということです。
いまは3環境で挙動が揃っていることを優先していますが、宿題として残っています。

uvf は無料で、Windows / macOS / Linux 版があります。
GitHub Releases から単体で動きます。
索引を作って同じファイルを何度も調べたい場合は有料版の uvp ですが、
探すだけなら uvf で足ります。

自分の環境で試すには

2行です。同じファイルに対して、キャッシュを捨ててから順に走らせてください。

# 1回目(既定)
rg --mmap -n -F '探す語' huge.log > /dev/null

# キャッシュを捨てる
#   macOS : sudo purge
#   Linux : sync && sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
#   Windows: RAMMap64.exe -accepteula -Et (管理者権限)

# 2回目
rg --no-mmap -n -F '探す語' huge.log > /dev/null

キャッシュを捨てずに測ると、2回目が速く出るだけで何も分かりません。 ここだけは省かないでください。
そして出力の行数が両方で一致することを必ず確かめてください。違っていたら、測り方が違っています。

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