App-Greple

 view release on metacpan or  search on metacpan

RIPGREP-STUDY.md  view on Meta::CPAN

| コマンド | 時間 |
|---|---:|
| `rg -c <マッチ0件> tree/`(並列) | 0.034s |
| `rg -j1 -c <マッチ0件> tree/`(単一スレッド) | 0.036s |
| `rg -l 高速 tree/`(全ファイルマッチ) | 0.047s |
| `greple -Mdig -c <マッチ0件> --dig tree` | **0.88s** |
| `greple -Mdig -c 高速 --dig tree` | **1.6s** |

**プリフィルタ効果(3000 ファイル中 5 ファイルのみにマッチする希少パターン)**

| コマンド | 時間 |
|---|---:|
| `greple -Mdig -l zebra --dig tree` | 0.30s |
| `rg -l zebra tree/ \| greple --readlist -l zebra` | **0.058s** |

### 2.3 ボトルネックの分解

上の数値から greple の 1 実行あたりのコスト内訳を分解できる。

| フェーズ | 実測値 | 備考 |
|---|---|---|
| 起動(モジュールロード・オプション処理) | 44ms | Getopt::EX 等。どの方式でも残る下限 |
| デコード + 正規表現スキャン | 約 80ms / 26MB | マッチ 0 件時 0.124s − 起動 44ms |
| **マッチ後処理**(Match/Block 構築・領域演算・カウント) | **約 7µs / マッチ** | 10 万マッチで 0.7〜0.9s。**支配的コスト** |
| ファイルごとのオーバーヘッド | 約 0.25ms / ファイル | open・binmode・slurp 等 |

**含意**: rg が 26MB を 10ms で走査するのに対し greple のスキャンフェーズは 80ms — 差は 8 倍だが
絶対値は小さい。マッチが多い場合の 0.8〜1.0s の大半はスキャンではなく
**マッチ後の Perl 側処理**であり、これは初期マッチ列挙を外部化しても消えない。
つまり「rg に位置を出させる」方式の理論上の改善上限は、このワークロードでは 1 割程度しかない。

## 3. ripgrep 側の事実確認

### 3.1 `--json` 出力(実測確認済み)

```json
{"type":"match","data":{"path":{"text":"sample.txt"},
 "lines":{"text":"foo 高速 bar\n"},"line_number":1,"absolute_offset":0,
 "submatches":[{"match":{"text":"高速"},"start":4,"end":10}]}}
```

- `absolute_offset` は行頭の絶対バイトオフセット、`submatches` の `start/end` は行内バイトオフセット。
- **トランスコード時(`-E euc-jp` や BOM 検出時)のオフセットは、変換後 UTF-8 バイト列基準**
  (EUC-JP ファイルで実測確認: `高速` → start:4, end:10 = UTF-8 バイト位置)。
  `--pre` フィルタや解凍も同様に変換後基準。
- 非 UTF-8 データは base64(`bytes` キー)で表現される。

したがって「rg の出すオフセットは常に UTF-8 バイト単位」とみなせ、greple の文字オフセットへの
変換は数学的には整合的に定義できる(オフセットは昇順に届くので O(N) の一回走査で変換可能)。
**位置変換そのものは技術的障害ではない**。障害は次節以降で述べるコスト構造にある。

### 3.2 案 B の実測: JSON 経由のオーバーヘッド

| 処理 | 時間 |
|---|---:|
| `rg --json fox corpus.txt`(96k マッチ、JSON 26MB 出力) | 0.09s |
| ↑を Perl (JSON::PP) でパース | **8.0s** |
| (参考)現行 greple の同検索全体 | 0.82s |

マッチ多数時、JSON のパースだけで現行 greple の 10 倍の時間がかかる(コア モジュール JSON::PP の場合。
JSON::XS を追加依存にすれば数百 ms 程度まで縮むが、それでも byte→char 変換・
ブロック構築を加えると現行との差はほぼ消える)。
マッチ少数時はそもそもスキャンフェーズ(80ms)しか削れない。**どちらのケースでも割に合わない。**

### 3.3 libripgrep(grep crates)

ripgrep は `grep-matcher` / `grep-searcher` / `grep-printer` 等のクレート群に分割されており
([libripgrep PR #1017](https://github.com/BurntSushi/ripgrep/pull/1017))、
ライブラリとして再利用可能。正規表現エンジンは Rust regex(有限オートマトン + SIMD)と
PCRE2(`--pcre2` / `-P`、後読み・後方参照対応)の 2 系統。

- Rust regex は後読み・後方参照・`(?{...})` 非対応。greple ユーザーのパターン資産とは方言差がある。
- PCRE2 モードは Perl にかなり近いが、完全互換ではない。

## 4. 各案の詳細検討

### 案 A: rg をファイルレベルのプリフィルタに使う 【推奨】

**仕組み**: `rg -l pattern dir` で「マッチを含むファイル一覧」だけを rg に高速列挙させ、
greple は該当ファイルのみを従来どおり処理する。位置情報を受け渡さないので
バイト/文字オフセット問題・正規表現方言問題の影響を最小化できる。

```
rg -l --null pattern dir | greple --readlist pattern
```

既存機構がほぼそのまま使える:

- `--readlist`(`script/greple:1200-1201`): STDIN からファイルリストを受け取る
- `find.pm` の `!command` 機構(`find.pm:80`): 任意コマンドでファイルリストを生成
  (`dig.pm` が find の薄いラッパであるのと同じ構図で、`App::Greple::rg` モジュールを書ける)

**実装案**: `App::Greple::rg` モジュールを新設し、`greple -Mrg --rg <dir> pattern` で

1. greple のオプション処理後、検索パターンを取得
2. パターンを rg に渡せるか判定(関数パターン `&func` や Perl 固有機能は不可)
3. 渡せる場合 `rg -lP --sort path` でファイルリスト生成(`-P` で PCRE2 = Perl 方言に接近、
   `--sort` は並列性を殺すので出力順が問題になる場合のみ)
4. 渡せない場合は `rg --files`(ファイル列挙のみ)にフォールバック — それでも
   .gitignore 尊重・バイナリスキップの恩恵はある

**正しさの条件**: プリフィルタは「greple がマッチするファイルの上位集合」を返す必要がある。
注意点と対策:

| 懸念 | 対策 |
|---|---|
| 正規表現方言差(後読み等) | `-P` (PCRE2) を使う。変換不能なら素通しフォールバック |
| 行跨ぎパターン | `rg -U`(マルチライン)を付ける |
| 非 UTF-8 ファイル(--icode) | `rg -E <encoding>` にマッピング。guess 時は素通し |
| `--if` フィルタ対象(zip 等) | rg では判定不能 → 素通し(従来どおり greple が処理) |
| 大文字小文字 (`-i`) | `rg -i` を連動 |
| must/need の複数パターン | 各陽性パターンの OR で `-e` を並べれば上位集合になる |

安全側に倒す(迷ったら素通し)設計なら結果の正しさは保たれ、性能だけが段階的に向上する。

**効果**: マッチが疎な大規模ツリーで支配的な「マッチしないファイルの処理」を rg の速度
(並列走査 + .gitignore/バイナリスキップ)で消せる。実測 3000 ファイルで 5 倍、
実プロジェクトの巨大ツリーでは桁違いになる。greple の主用途(ドキュメント・コード検索)で
最も体感が大きいのはこのケース。

**得失まとめ**: 工数小(モジュール 1 個、コア変更ほぼ不要)。rg はオプショナルな外部依存
(無ければ従来動作)。greple の機能・モジュール互換性への影響ゼロ。
単一巨大ファイル・マッチ多数のケースには効かない(→ 案 E で補完)。

### 案 B: rg --json の位置情報を greple 内部位置に変換する 【非推奨】

ユーザーが以前検討した方式。技術的には §3.1 のとおり「rg のオフセットは変換後 UTF-8 バイト基準」
なので、昇順オフセットを 1 パスで文字位置に変換すれば greple の内部表現に載せること自体は可能。
しかし:

1. **改善上限が小さい**: 削れるのはスキャンフェーズ(26MB あたり ~80ms)のみ。
   マッチ後処理(支配的コスト)は greple 側に残る。
2. **JSON オーバーヘッドが逆効果**: マッチ多数時、JSON::PP でパースに 8 秒(実測)。
   JSON::XS を依存に加えても変換・構築コストで利得はほぼ相殺。
3. **機能制約が多い**: 関数パターン(`&func`)不可、`--inside/--outside` の領域パターンも
   同様に外部化しないと片手落ち、`--if` フィルタとの合成、正規表現方言差、
   マッチ 0 幅・重なり領域のセマンティクス差、等の互換性穴が多数。
4. 表示のためどのみち該当ファイルのデコードと slurp は必要で、I/O 面の節約もない。

**結論**: 実装しても現行比でほぼ速くならず、互換性リスクだけ増える。

### 案 C: Rust ライブラリ (grep crates) を FFI::Platypus で呼ぶ 【非推奨】

**技術的成立性は確認できる**:
[FFI::Platypus::Lang::Rust](https://metacpan.org/pod/FFI::Platypus::Lang::Rust)(0.17, 2023)と
[FFI::Build::File::Cargo](https://metacpan.org/pod/FFI::Build::File::Cargo) により、
CPAN ディストリビューションに Rust クレートを同梱し `cargo` でビルドする仕組みは確立している。
in-process なので JSON オーバーヘッドはなく、byte→char 変換テーブルを Rust 側で構築する等の
自由度も高い。

しかし:

1. **効果は案 B と同じ場所に限定される**: 初期マッチ列挙の高速化のみ。本気で速くするには
   領域代数(Regions.pm)とブロック構築(Grep.pm)ごと Rust に移す必要があり、そうなると
   関数パターンや `--postgrep` 等の Perl コールバックとの往復が発生して設計が複雑化する。
2. **配布コストが跳ね上がる**: インストールに Rust toolchain が必要になる。
   `cpanm App::Greple` で入る手軽さが失われる(Alien 化や事前ビルドバイナリ配布は保守負荷大)。
   オプショナル XS 的な二段構え(あれば使う)は二重実装の保守を意味する。
3. Perl↔Rust 境界での文字列コピー(26MB 級)も無視できない。

**結論**: 「工数大・利得小・配布リスク大」。greple の規模のツールに対して割に合わない。

### 案 D: greple 全体を Rust で書き直す 【greple としては非推奨】

技術的には可能で、性能は本物になる(マッチ後処理も含め 10〜100 倍級)。
問題はユーザー認識のとおり **Perl モジュールエコシステム**にある。

greple の価値の中核は `-Mdig`, `-Msubst`, `-Mmd`, `~/.greplerc`, Getopt::EX の
オプション展開機構であり、これらは「Perl コードが検索パイプラインに直接介入できる」
こと(`$_` にテキスト、`[from,to]` を返す関数、`__DATA__` のオプション定義、`__PERL__` 節)
に依存している。互換を保つ選択肢は:

| 方式 | 評価 |
|---|---|
| (a) Rust に Perl を埋め込む(libperl / [RuPerl](https://lib.rs/crates/ruperl)) | 実験的・ビルド最悪級。モジュール実行部分は結局 Perl 速度。Getopt::EX 互換のためほぼフル Perl が要る |
| (b) モジュールを別プロセス Perl で動かし IPC(領域リストを往復) | 設計としては最も筋が良いが、Getopt::EX 相当の再実装 + プロトコル設計で数人月級。26MB 級テキストの往復コストもç™...
| (c) モジュールを Rust で書き直す | ユーザー認識どおり非現実的。サードパーティ・個人設定資産を切り捨てることになる |

さらに Getopt::EX 自体(オプション展開、autoload、.greplerc 解釈)の再現が必要で、
「greple 互換の Rust 版」は実質フルスクラッチの大規模プロジェクトになる。

**結論**: greple の後継としてやるなら「互換性を捨てた新ツール」として設計すべきで、
それは本資料のスコープ(greple の高速化)とは別の判断。現行ユーザー資産を守るなら選ばない。

### 案 E: Perl 内部の改善 【併用推奨】

ripgrep とは独立に、実測で見えたボトルネックへの直接対処。

- **E1: ファイル並列化**(効果: コア数倍、工数: 中)
  ファイルループ(`script/greple:1119-1162`)は各ファイル独立なので、
  fork ベース(MCE 等、あるいは自前 fork + 出力順序制御)で並列化できる。
  rg -j1 と rg(並列)の差が示すとおり、多ファイル時の並列化は確実に効く。
  案 A と直交なので併用可能。
- **E2: マッチ後処理の最適化**(効果: マッチ多数時に大、工数: 小〜中)



( run in 0.523 second using v1.01-cache-2.11-cpan-6aa56a78535 )