258GB・45億行に、5つの質問をする — grepとGUI、どこで逆転するか

比較記事

1本のファイルに、続けて5つ質問をする。そのあと絞り込んで、集計して、置換して、書き出す。

その一連の仕事を丸ごと測ったら、CLIで51分17秒、GUIで15分35秒でした。実運用どおり .uwvz で締めるなら 12分29秒です。

ただし——質問が1つだけなら、ripgrepのほうが速いです。どこで逆転するのか、その分岐点まで出しました。

測ったもの

項目
ファイル us-260726.osm(OpenStreetMap アメリカ全土をXML展開・分割していない1本
サイズ 258,679,440,228 byte = 258.68 GB
行数 4,509,830,821行(45億行)
マシン Apple M4 / メモリ32GB / macOS 26.3.1
置き場所 外付けSSD
CLI BSD grep 2.6.0(macOS標準)/GNU grep(Homebrew)/ripgrep 15.2.0/BSD sed/GNU sed 4.10
GUI 私が開発している UwView Pro v1.5.0 + Edit Upgrade

測り方の作法(相手に最良の条件を与えるため):

  • grepには LC_ALL=C を付ける(UTF-8ロケールより速いため)
  • -c(件数のみ)で測る。ヒット行を端末に出すと、測っているのが検索ではなく描画になる
  • 各計測の前に sudo purge(ページキャッシュ破棄)、計測間に150秒のクールダウン(M4のサーマル対策)
  • 時間は /usr/bin/time -p の real

まず、物理下限を測る

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

268.49秒=963 MB/s。「ただ1回読み切るだけ」の時間です。どのツールもこれより速くはなりません。以降の数字は、すべてこの268.49秒との距離として読んでください。

単発検索 — ripgrepは下限の+2%で走る

「New York」ほか3語で測りました。

ヒット BSD grep GNU grep ripgrep amber BSD grep(UTF-8)
New York 100,492 342.19 276.77 355.24 363.54
Statue of Liberty 88 373.34 277.06 357.69 416.78
k="highway" 60,471,216 474.40 273.66 275.04 503.43

ここで3つ、はっきりしたことがあります。

① ripgrepは定数時間で走る。 ヒットが88件でも6,047万件でも 275〜277秒、振れ幅±2秒。CPU時間は11〜16秒しか使っておらず、完全にI/O律速です。物理下限268.49秒の+2%——もうこれ以上速くなりようがないところに張り付いています。

② macOS標準のBSD grepは、GNU grepの1.73倍遅い。 474.40秒 対 273.66秒。CPU時間は383.53秒 対 66.54秒で、効率が5.8倍違います。BSD grepだけがディスクを使い切れていません。

brew install grep で入るGNU grep(ggrep)は、ripgrepとほぼ互角でした。巨大ファイルをmacOSで扱うなら、これは知っておく価値があります。

③ Rust製でも速いとは限らない。 amberambs)も測りました。1本のファイルを複数スレッドに分割して走査する、今回で唯一のツールです(grepもripgrepも単一ファイルには1スレッド)。結果は357.69秒——ripgrepより29%遅く、BSD grepとほぼ同じでした。並列化自体は効いています(--max-threads 1 なら421.12秒なので15%短縮)。ただしCPU時間が401秒で、ripgrepの25〜36倍。土台の効率差が並列化の利得を飲み込みました。「並列化されているか」より「1バイトあたり何命令使うか」のほうが効くという例です。ヒット数は88・100,492でgrep/ripgrepと完全一致でした。

前回、48GBのファイルでは grep 64.64秒 < ripgrep 71.49秒で、grepが勝っていました。今回は逆です。理由は説明できます——前回のストレージは0.41GB/sで、全ツールがI/O律速となり横並びになっていた。ストレージが963MB/sになると、ツールのCPU効率が露出する。「単一巨大ファイルではgrepが速い」は、遅いストレージでのみ成立する条件付きの結論でした。訂正します。

5つの質問をする

New York / Brooklyn / Central Park / Statue of Liberty / coffee_shop を順に検索します。

ツール 5問の合計 1問あたり
BSD grep 1,789.22秒(29分49秒) 357.84
ripgrep 1,389.55秒(23分10秒) 277.91
UwView Pro 161.90秒(2分42秒) 32.38

ヒット数は 100,492 / 30,871 / 14,306 / 88 / 36,506 ——CLIとGUIで1件も違いませんでした。

CLIは質問のたびに258GBを読み直します。GUIは索引を1度作れば、あとは索引を引くだけです。8.58倍

絞り込みと集計

絞り込み4段New York から hospitalschool を除外して Brooklyn を含むものへ。

grep 'New York' us-260726.osm | grep -v hospital | grep -v school | grep -c Brooklyn
→ 35件 / 343.52秒

UwView Proの多段階検索では 35.2秒。しかも内訳が構造の差を見せています。

1段目 New York        32.2秒   → 100,492件
2段目 含まない hospital  1秒未満 → 100,491件
3段目 含まない school    1秒未満 → 100,484件
4段目 Brooklyn          1秒未満 →      35件

2段目以降は、ストップウォッチでは測れませんでした。押した瞬間に件数が変わります。ここでの「1秒未満」は私の計測の分解能の限界であって、実際の所要はもっと短いはずです。上の35.2秒は、測れなかった3段をそれぞれ1秒として積んだ上限値です。

なぜこうなるかは構造で説明できます。2段目以降は前段の10万件だけを見ているからです。CLIのパイプは全体を1回流すしかないので343.52秒。9.76倍(これも上限側の値なので、実際の差はもう少し開きます)。

(この比較のため、GUI側の文脈は±0に設定しました。CLIのパイプは行ストリームなので、条件を揃えないと件数が合いません。揃えた結果、最終35件が完全一致しました。)

集計 — ヒット行から k="..." を切り出して頻度順に並べます。

grep 'New York' us-260726.osm | grep -oE 'k="[^"]+"' | sort | uniq -c | sort -rn | head -20
→ 344.38秒

UwView Proの「集計」では 34.0秒10.13倍)。うち検索が32.0秒で、集計そのものと結果からの復帰はそれぞれ1秒未満・測れずでした(34.0秒はこれを1秒ずつとして積んだ上限値です)。結果はこうです。

件数 構成比
k="source" 29,976 29.83%
k="operator" 17,815 17.73%
k="addr:city" 14,756 14.68%
k="addr:street" 8,458 8.42%
k="name" 8,147 8.11%

上位10件までCLIと完全一致。さらに、上位20件+その他2,583を合計すると 100,492 ——New Yorkのヒット行数とぴったり同じでした。OSMのXMLは1行1タグなので、「行を数える」と「出現回数を数える」が一致したわけです。

置換して、書き出す

sed 's/New York/NYC/g' us-260726.osm > /dev/null      → 741.48秒(変換のみ)
sed 's/New York/NYC/g' us-260726.osm > us-nyc.osm     → 999.38秒(258GB書き出し込み)

置換は「読むだけ」の2.76倍かかります(741.48 ÷ 268.49)。CPU時間680秒で、ディスクを半分も使えていません。

GNU sed(gsed 4.10)も測りました。896.39秒——BSDのほうが速い。grepではGNUが1.73倍速かったのに、sedでは逆でした。「GNUのほうが速い」は一般則ではありません。

UwView Pro+Edit Upgrade 側:

工程 所要
全置換 New YorkNYC 62.0秒(100,992箇所)
Save(.uwvz・28.61GB) 138.1秒
Save(プレーンテキスト) 323.9秒

置換そのものは62.0秒。sedの変換741.48秒と比べて11.96倍です。原本を書き換えず、変更を差分として積むだけだからです。書き出しは最後に1回だけ行います。

全体作業の総計

保存はテキストか .uwvz のどちらか一方なので、総計も2段に分けます。

工程 CLI(ripgrep基準) UwView Pro+Edit
開く・索引 317.8 GUIのみ発生(1回だけ
5問の検索 1,389.55 161.9 8.58倍
絞り込み4段 343.52 35.2(上限) 9.76倍
集計 344.38 34.0(上限) 10.13倍
置換 (保存に含む) 62.0
保存(テキスト・258.68GB) 999.38 323.9 置換込みで2.59倍
保存(.uwvz・28.61GB) 相当機能なし 138.1
総計(テキストで書き出す) 3,076.83秒=51分17秒 934.8秒=15分35秒 3.29倍
総計(.uwvz で締める・実運用) 3,076.83秒=51分17秒 749.0秒=12分29秒 4.11倍

BSD grep基準なら、それぞれ 3.72倍 と 4.64倍です。

(上限)は、ストップウォッチで測れなかった工程を1秒として積んだ値です。GUI側の総計も同じく上限値なので、実際の差はこの表より大きいことはあっても、小さいことはありません。

2段に分けた理由

上段はCLIに合わせて同じ成果物(プレーンテキスト258GB)を作った場合です。下段は、実際にUwView Proを使うときの締め方です——編集結果を28.61GBに畳んで残せて、次に開くときは索引を作り直さずに再開できるので、こちらを選びます。

成果物が違うことは書いておきます。.uwvz はプレーンテキストではありません。他のツールに渡すならテキストで書き出す必要があり、そのケースが上段の3.29倍です。逆に、この続きを自分で見るなら下段が合理的です——28.61GBで済み、次回は索引の317.8秒を払い直さずに済みます。CLIには状態を残す仕組みがないので、翌日の1問目はまた277.91秒からです。

で、何問目で逆転するのか

CLIは質問数に比例し、GUIは索引の1回きり+小さい増分です。交差点を出します。

CLI(N)  = N × 277.91                (ripgrep・実測)
GUI(N)  = 317.8 + N × 32.38         (索引317.8秒・1問32.38秒・実測)

N* = 317.8 ÷ (277.91 − 32.38) = 1.29
質問数 ripgrep BSD grep UwView Pro 倍率(ripgrep ÷ UwView Pro)
1 277.9 357.8 350.2 0.79倍(ripgrepの勝ち)
2 555.8 715.7 382.6 1.45倍
3 833.7 1,073.5 415.0 2.01倍
5 1,389.6 1,789.2 479.7 2.90倍
10 2,779.1 3,578.4 641.6 4.33倍
20 5,558.2 7,156.8 965.4 5.76倍

質問が1つだけなら、ripgrepの勝ちです(277.9秒 対 350.2秒)。索引を作る意味がありません。BSD grepとはほぼ互角。

2問目から逆転します。そこからは質問を重ねるほど開きます——3問で2.01倍、5問で2.90倍、20問で5.76倍。線引きはずっと同じで、今回それを数字で引けました。

同じ仕事をしたことの証明 — 照合6項目

速度を比べる前に、両者が本当に同じ仕事をしたのかを確かめる必要があります。6項目とも一致しました。

# 項目
1 総行数 4,509,830,821(wc -l = GUIの索引)
2 5問のヒット数 100,492 / 30,871 / 14,306 / 88 / 36,506
3 絞り込み最終 35件
4 集計 上位10件と総計 k="source" 29,976 … 総計100,492
5 置換箇所 100,992
6 テキスト出力サイズ 258,678,935,268 byte

6が決定的だと思っています。GUIで非破壊編集した結果を書き出したファイルが、sed の出力と1バイトも違いませんでした。「差分ファイル方式で本当に同じ結果になるのか」という当然の疑問に、バイト単位で答えられます。

5も面白いところです。grep -c が数えた 100,492は「行数」sed が置換したのは 100,992箇所。差の500は、1行に2箇所以上「New York」があった行です。GUI側の置換件数も100,992で一致しました。

数字にならない差

開いた瞬間から読める。 索引作成は317.8秒かかりますが、本文が見えるまではストップウォッチで測れませんでした(1秒未満)。索引を作っている最中から、末尾でも中間でも読めます。この1秒未満は索引の317.8秒の「前」ではなく「中」にあるので、総計は変わりません。grepは終わるまで無音です。

戻れる。 絞り込みの4段目から1段目のタブに戻る操作は、ストップウォッチで測れませんでした(1秒未満)。CLIならパイプを組み直してもう一度343秒です。「消しすぎたかもしれない」と思ったときのコストが、桁で違います。

中断できる。 作業途中のCloseも、開き直しての再開も、測れませんでした(どちらも1秒未満)。258GBの編集セッションを一瞬で畳んで、翌日一瞬で戻れます。原本を書き換えていないので、書き出すものが残っていないからです。

正直に書いておくこと

  • すべて私の環境での実測です(Apple M4・32GB・外付けSSD)。設定の問題かもしれません——ご存じの方は教えてください。訂正します
  • 私は比較対象の一方(UwView Pro)の開発者です。その分は割り引いてお読みください
  • GUI側はストップウォッチによる画面操作ベースの計測を含みます。CLI側は /usr/bin/time -p です
  • GUI側で「1秒未満」と書いた工程は、ストップウォッチでは測れなかったという意味です。総計・倍率は、それらを1秒として積んだ上限値で計算しています。つまり実際の差は、ここに書いた数字より大きいことはあっても、小さいことはありません
  • 絞り込みと集計のCLI側はBSD grepでの実測です。GNU grepならさらに短縮される見込みですが、未計測です
  • 1回きりの検索、スクリプト、cron、パイプで他へ渡す処理——ここはCLIが正解です。GUIに置き換えるものではありません
  • GUIが効くのは、何を捨てればいいかがまだ分かっていない対話的な調査です。障害調査は、たいていそちらです

使ったツール

UwView Pro は販売中です(Windows・macOS・Linux対応、1ライセンスで3OS。買い切りまたは月額、14日間の無料試用あり——編集機能も使えます)。多段階検索・集計はPro機能、全置換は Edit Upgrade の機能です。

関連記事


開発者より: 私のアプリ・Kindle本・OSSの一覧は GitHub: amru195704 にあります。


ご注意
本記事の情報は参考情報として提供するものであり、正確性・完全性を保証するものではありません。すべての計測値は記載の特定環境におけるものであり、他のハードウェアでは異なる場合があります。誤りにお気づきの場合はコメントでお知らせください。確認のうえ訂正いたします。

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