「秒に出ない」と書いた改行の数え方が、2回目の検索の4割を占めていました — Wide Field⑫:数え方を5倍速くして、hot でも ripgrep を追い越す(3GB 0.40→0.27秒)

技術解説

【追記 2026-10-16】いまの速さ — v1.9.0 でも、2回目は ripgrep と同等のままです

本文の数字は 1.7.3.6.8・1.7.3.6.9(2026年10月3〜4日)のものです。同じ Mac で、いまの版 v1.9.0(2026年10月13日)を測りました。

ripgrep ÷ 無料版 uvf(2回目) 本文(1.7.3.6.8〜1.7.3.6.9) いま(v1.9.0)
3GB 文字列 1.07〜1.11倍(0.27秒) 1.07倍(0.29秒 対 0.31秒)
検索7種の2回目の合計(3GB/10GB) 1.20/1.19倍 1.22/1.19倍
参考:uvp・3GB 文字列 — 1.8倍(0.17秒)

本文の結論は変わりません。2回目から大きく速くしたいときは、索引を持つ uvp です(検索7種で ripgrep の 3.44倍・3GB)。詳しくはベンチマークとv1.9.0「Full Spectrum」の記事。

改善記⑪で、こう書きました。

「改行を数える速さは、外付け SSD から読む限り秒数に出ない。媒体が速くなったときに初めて効く準備だ」

その「速い媒体」は、新しい機材ではありませんでした。**同じファイルを2回目に探すときの、メモリ(ページキャッシュ)**です。

先に結論: 同じファイルを続けて探す2回目(hot)で、uvf は ripgrep より 1.3〜1.4倍遅いままでした。内訳を測ると、3GB で 0.40秒のうち 0.15秒が「行番号のために改行を数える」処理でした。数え方を、16バイトずつ改行と比べた結果をそのまま足し込む形に替えて 0.15秒 → 0.028秒(5倍)。2回目は **3GB 0.40 → 0.27秒(ripgrep 0.29)、10GB 1.28 → 0.85秒(ripgrep 0.95)**になり、ripgrep を追い越しました。1回目(ディスクから読む)は変わりません

uvf は私が作っている UwView(v1.7.3.6.9 “Wide Field”・2026年10月4日)の無料の検索コマンドです。この改修は 1.7.3.6.8 で入りました。Mac(Apple M4・10コア)・メモリ 32GB・外付け USB SSD・OpenStreetMap の XML で測っています。


先に、負けていたところを書きます

1回目(キャッシュを捨てた直後)は、前の版から uvf のほうが ripgrep より少し速いのです。ディスクの速さで頭打ちになるので、読み方(pread で 4MB ずつ)の差が出ます。

ところが2回目(ファイルがメモリに載った状態)は、ずっと負けていました。

2回目(hot)・3回の中央値・秒 ripgrep uvf 1.7.3.6.7 ripgrep との比
3GB 東京 0.28 0.38 1/1.36
3GB 当たらない語(0件) 0.28 0.40 1/1.43
10GB 東京 0.94 1.29 1/1.37
10GB 当たらない語(0件) 0.92 1.25 1/1.36

差は 0.1〜0.35秒です。人には分かりません。1.5倍未満なので、この連載の決まりでは「同等」です。

それでも気になりました。3GB と 10GB で比が同じなので、起動の時間ではなく読む速さそのものの差です。10GB で ripgrep は毎秒 約 10GB、uvf は 約 7.4GB。しかも当たりが 0 件でも同じ差なので、出力ではなく「読んで探す」部分にあります。

見立ては4つ

# 見立て 確かめ方
H1 行番号のために、読んだバイトの改行を全部数えている(当たりが無くても) 数えないで走らせる
H2 pread でページキャッシュからコピーする分(ripgrep は mmap で直接読む) mmap で読ませる
H3 4MB のブロックが CPU のキャッシュより大きく、読む・探す・数えるの間に落ちる ブロックを小さくする
H4 起動・後始末の固定分 空のファイルを探す

先に直し方を考え始めると、どれも「効きそう」に見えます。なので、直す前に、時間の内訳を出す仕掛けを入れました(UV_TRACE=1 で、読む・探す・数える・出すの合計を表示します)。

測ってみると、当たったのは H1 だけでした

3GB・当たらない語・2回目・5回の中央値です。

測ったもの 3GB 10GB 分かったこと
空のファイル 0.02 — H4 は外れ
前の版 0.40 1.28 内訳:読む 0.13・探す 0.09・数える 0.15
数えない(UV_NOCOUNT=1・測るためだけ) 0.24 0.76 数える分を抜くと ripgrep より速い
mmap で読む(UV_SOURCE=mmap) 0.43 2.03 遅くなる。H2 は外れ
ブロック 256K・1M・4M・16M 0.27〜0.30 — 大きさで変わらない。H3 は外れ
ripgrep / rg --no-mmap 0.29 / 0.29 0.95 / 0.95 ripgrep も mmap の有無で変わらない

H2 は、私がいちばん「ありそう」と思っていた見立てでした。 「ripgrep は mmap でコピーせずに読むから速い」。もっともらしい話です。ところが rg --no-mmap でも 0.29秒で同じでした。こちらを mmap にすると、むしろ遅くなりました(毎回ページを割り当て直す分)。もっともらしい話を、測る前に直していたら、遅くする改修を入れていたところです。

そして、数える処理は 0.15秒。2回目の検索全体の 4割でした。

ripgrep も、行番号の分を払っている

3GB 東京・2回目 行番号あり 行番号なし
ripgrep 0.30 0.24

ripgrep も、行番号を出すと約2割遅くなります。どちらも払っている費用で、払い方が違っていた、ということです。

遅かったのは「数えること」より「数え方」

uvf は改行を .NET の Span.Count で数えていました。これ自体、内部でまとめて比べる(SIMD の)作りです。改善記⑪のときは 1バイトずつの数え方から、IndexOf で跳ぶ形にしましたが、こちらの道(uvf の検索)は前から Span.Count でした。

それでも 3GB で 0.15秒。毎秒 約 20GB です。外付け SSD(毎秒 約 0.97GB)から読む1回目なら、20倍の余裕があって秒には出ません。ページキャッシュから読む2回目は、その余裕が無かったわけです。

替えた数え方は、こうです。

  1. 16バイトずつ、改行(\n)と比べる。合ったバイトは「−1」(全ビットが 1)、合わないバイトは 0 になる
  2. その結果を、バイトのまま足し込み先から引く(−1 を引く=1 を足す)。足し込み先は 16個の 1バイトの数
  3. 1バイトの数は 255 であふれるので、255回ごとにまとめて合計する
// 64 バイトずつ、4 つの足し込み先に分けて引く。255 周ごとに合計する
a0 -= Vector128.Equals(Vector128.LoadUnsafe(ref start, (nuint)i), newline);
a1 -= Vector128.Equals(Vector128.LoadUnsafe(ref start, (nuint)(i + 16)), newline);
...
total += Sum(a0) + Sum(a1) + Sum(a2) + Sum(a3);

比べた結果を毎回「何個合ったか」に直す処理を、255回に1回にしただけです。**3GB で 0.15秒 → 0.028秒(5倍)**になりました。数える分は、数えないとき(0.24秒)との差で 0.03秒しか残っていません。

「探すのと数えるのを1回の読みで済ませる」という、もっと大きな直し方も候補にありました。読むのが2回のままでも、数えるのが5倍速くなった時点で目標に届いたので、手間と危うさの小さいほうで止めました。

数字: 2回目は ripgrep を追い越し、1回目は変わらない

2回目・5回の中央値・秒(開発機・2026年10月3日)

検索 前(1.7.3.6.7) 後(1.7.3.6.8) ripgrep ripgrep との比(後)
3GB 当たらない語 0.40 0.27 0.29 1.07
3GB 東京 0.40 0.27 0.30 1.11
3GB -E 'k="highway[^"]*"' 0.53 0.42 0.50 1.19
3GB -E '^ *<tag k="name"' 0.45 0.33 0.45 1.36
10GB 当たらない語 1.28 0.85 0.95 1.12
10GB 東京 1.40 0.88 0.96 1.09
  • 1回目は変わりません(3GB 東京:前 3.04秒・後 3.03秒)。ディスクから読む速さで決まるからです。
  • -v(当たらない行を出す)は別の道を通るので、変わりません(3GB 1.32秒)。
  • 出力は前後で1バイトも変わらず、行番号を含めて ripgrep と完全に一致しています。数え方のテストには、乱数のデータで素朴な数え方と一致すること、64バイトと 255回の切れ目、全部が改行のデータ、CR は数えないこと、を入れました。

オーナーのテスト一式(キャッシュを捨てて 10秒待つ → 1回目 → 続けて2回目・2026年10月4日・1.7.3.6.9)でも、同じ傾向でした。

7つの検索の2回目の合計・秒 ripgrep uvf
3GB 7.60 6.31
10GB 12.58 10.61

正直に書いておくこと

  • 差は 0.1秒前後で、1.5倍未満です。 前も後も、この連載の基準では「同等」です。人が待って分かる差ではありません。
  • 改善記⑪の「数え方の改善は秒に出ない」は、1回目(ディスクから読む)については今も正しいです。外れていたのは「媒体が速くなるのは、機材が変わるとき」という思い込みのほうでした。同じ機械でも、2回目はメモリが媒体になります。
  • 改善記⑪で入れた IndexOf は画面の索引作りの道で、今回の Span.Count → 足し込みは uvf の検索の道です。別の処理です。
  • 50GB はメモリ(32GB)に収まらないので、2回目もディスクから読みます。この改修は効きません。
  • UwView Pro の uvp では、1本のファイルを読みながら索引を作る1回目と、複数のファイルを束ねるときの行数の数えに効きます。索引(.uwvz)を探す道と画面の検索は、この数え方を使っていないので変わりません。
  • 前後の比較は開発機での5回の中央値、テスト一式はオーナーの1回ずつの測定です。数字はすべて Mac(Apple M4・10コア)・メモリ 32GB・外付け USB SSD・OpenStreetMap XML での例です。秒数を環境またぎで比べないでください。
  • 私は自分の製品について書いています。その分は割り引いてお読みください。

持ち帰れる一般論: 「秒に出ない」は、条件つきの言葉

(1)律速は、条件ごとに測り直す。 1回目はディスク、2回目はメモリ、50GB の2回目はまたディスク。同じコマンドでも、どこで詰まっているかは条件で入れ替わります。改善記⑪の結論は、1回目の条件で測ったものでした。

(2)直す前に、内訳を出す。 4つの見立てのうち、私がいちばん信じていた H2(mmap)は外れで、直していたら遅くなっていました。内訳を出す仕掛けは、出荷ビルドに残しています(UV_TRACE=1)。

(3)相手の「速い理由」を、推測で決めない。 「ripgrep は mmap だから速い」は、rg --no-mmap を1回走らせれば確かめられることでした。

(4)いちばん小さい直し方で、目標に届いたら止める。 読みを1回にまとめる大きな直し方は、行番号がずれる危うさと引き換えです。数え方の差し替えなら、比べる相手(素朴な数え方)がはっきりしています。

使っている道具

UwView は巨大テキスト・巨大ログのビューアです(無料版 UwView/有料版 UwView Pro)。

# 2回目の内訳を見る(読む・探す・数える・出すの合計が標準エラーに出る)
UV_TRACE=1 uvf huge.log NO_SUCH_WORD

# ripgrep と同じ答えかを先に確かめる
uvf huge.log 'ERROR' > mine.txt
rg -n 'ERROR' huge.log > theirs.txt

終了コードは grep と同じで、0=あり・1=なし・2=エラーまたは結果が不完全です。

無料版は GitHub Releases から。Mac は brew install --cask amru195704/uwview/uwview、Windows は Scoop(scoop bucket add uwview https://github.com/amru195704/scoop-uwview → scoop install uwview)でも入ります。

リンク

  • 改善記⑪: 改行を1.88倍速く数えられるようにしたのに、秒数は1秒も動かなかった: https://uvp.y42u.net/blog/uwview-fl11-newline-vs-media/
  • 改善記⑩: mmap は順次読みで帯域の6割しか出ない: https://uvp.y42u.net/blog/uwview-fl10-mmap-vs-pread/
  • 測って分かった「差」の下限(1.5 倍未満を差と呼ばない理由): https://uvp.y42u.net/blog/uwview-fl09-difference-threshold/
  • ripgrep と正規表現の書き方はどこまで同じか: https://uvp.y42u.net/blog/uwview-wf09-regex-vs-ripgrep/
  • --json と --tune: https://uvp.y42u.net/blog/uwview-wf11-json-and-tune/
  • v1.7.3.6.9 のリリースノート: https://github.com/amru195704/UwView/blob/main/2-doc/release-body-v1.7.3.6.9.md
  • 実測の全データ(ベンチマーク一覧): https://uvp.y42u.net/benchmarks/
  • ソースコード(GitHub): https://github.com/amru195704/UwView

そして、ストレージを専有している巨大ログを圧縮して保管し、さらに高速に検索したいなら UwView Pro をどうぞ。永続索引・圧縮キャッシュ検索・約 1/9 保管で、開き直しも検索も一段速くなります(全 OS 対応・買い切り/月額プランあり)。


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


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