App-Greple

 view release on metacpan or  search on metacpan

RIPGREP-STUDY.md  view on Meta::CPAN


コーパス: UTF-8 日英混在テキスト 26MB / 30 万行(および同内容を 100 行 × 3000 ファイルに分割したツリー)。
warm cache、3 回計測の代表値。

**単一ファイル(26MB)**

| コマンド | マッチ行数 | 時間 |
|---|---:|---:|
| `rg -c fox` | 95,984 | 0.010s |
| `grep -c fox` | 95,984 | 0.009s |
| `perl -ne '$c++ if /fox/'`(UTF-8 デコード込み) | 95,984 | 0.035s |
| `greple -c fox` | 95,984 | **0.82s** |
| `greple -c 高速` | 115,860 | **0.96s** |
| `greple -c <マッチ0件>` | 0 | **0.12s** |
| `greple`(/dev/null、起動コストのみ) | — | 0.044s |

**多ファイル(3000 ファイル / 計 26MB)**

| コマンド | 時間 |
|---|---:|
| `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 がマッチするファイルの上位集合」を返す必要がある。
注意点と対策:



( run in 1.154 second using v1.01-cache-2.11-cpan-9581c071862 )