The fastest grep in the world
Welcome to the rawgrep homepage. rawgrep began as a performance experiment but ended up being a genuinely nice tool to use. Primarily, its philosophy is to delete unnecessary work and simplify the problem space.
Mainstream greps are higher-level programs and largely work against this, since they stand on top of the operating system’s general-purpose file layers, such as the Virtual File System (VFS). rawgrep avoids the kernel wherever it can. It reads data straight from the block device, and its own highly specialized caches take the place of the kernel’s inode and dentry ones.
Supported platforms: currently only ext4 (on Linux).
Mean wall time. 75 warm runs after 5 warmup runs, and 10 cold runs, with OS cache dropped before each.
Tool cache is each tool's own persistent cache. For rawgrep it is the Fragment Cache, for fff it is its bigram inverted index. ripgrep has no persistent cache, so its two rows per cache state are just repeat runs.
For rawgrep, Tool cache off (--no-cache) disables only the Fragment Cache.
The Directory Snapshot and Binary Verdicts caches stay on, since they are part of how rawgrep
stands in for the general-purpose caches of the kernel.
| OS cache | Tool cache | rawgrep | fff | ripgrep |
|---|---|---|---|---|
| Chromium (~500k files), pattern: TODO | ||||
| warm | on | 67.5 | 171.9 | 348.4 |
| warm | off | 210.0 | 300.5 | 347.2 |
| cold | off | 7979.6 | 9981.4 | 11836.8 |
| cold | on | 1460.9 | 1497.5 | 11808.2 |
| Chromium (~500k files), pattern: (?i)\bTODO\((?:crbug\.com/\d+|[a-zA-Z][\w.-]*)\) | ||||
| warm | on | 56.3 | 140.6 | 357.9 |
| warm | off | 212.7 | 332.5 | 358.5 |
| cold | off | 8039.0 | 10024.5 | 11951.7 |
| cold | on | 1142.8 | 481.8 | 11859.8 |
| Linux (~100k files), pattern: TODO | ||||
| warm | on | 20.1 | 70.6 | 109.3 |
| warm | off | 106.4 | 110.4 | 109.4 |
| cold | off | 3085.3 | 3270.8 | 3726.9 |
| cold | on | 302.4 | 281.3 | 3718.1 |
| Linux (~100k files), pattern: [A-Z]+_SUSPEND | ||||
| warm | on | 27.7 | 69.4 | 109.6 |
| warm | off | 116.8 | 112.5 | 109.7 |
| cold | off | 3072.7 | 3278.7 | 3728.5 |
| cold | on | 339.8 | 241.6 | 3732.1 |
| OS cache | Tool cache | rawgrep | ripgrep |
|---|---|---|---|
| Chromium (~500k files), pattern: TODO | |||
| warm | on | 65.7 | 82.5 |
| warm | off | 77.2 | 82.5 |
| cold | off | 68.7 | 75.4 |
| cold | on | 58.2 | 75.4 |
| Chromium (~500k files), pattern: (?i)\bTODO\((?:crbug\.com/\d+|[a-zA-Z][\w.-]*)\) | |||
| warm | on | 64.0 | 88.7 |
| warm | off | 80.7 | 88.7 |
| cold | off | 78.2 | 73.2 |
| cold | on | 60.1 | 73.2 |
| Linux (~100k files), pattern: TODO | |||
| warm | on | 33.1 | 11.5 |
| warm | off | 32.3 | 11.5 |
| cold | off | 31.6 | 11.6 |
| cold | on | 34.0 | 11.6 |
| Linux (~100k files), pattern: [A-Z]+_SUSPEND | |||
| warm | on | 36.9 | 11.7 |
| warm | off | 36.6 | 11.7 |
| cold | off | 34.0 | 11.9 |
| cold | on | 38.5 | 11.9 |
rawgrep won 12 of the 16 scenarios. With warm OS cache and Tool cache on, it is 2.5× to 3.5× faster than fff and 4.0× to 6.4× faster than ripgrep. With cold OS cache and Tool cache off, rawgrep’s lead over fff shrinks to 1.06× to 1.25×.
Of the four losses, three are to fff (cold OS cache, Tool cache on), by 1.08× to 2.37×. That is to be expected, since fff relies on much heavier caching (see Design). The fourth one is on the Linux regex pattern (warm OS cache, Fragment Cache off), where rawgrep trails ripgrep by about 7 ms and fff by about 4 ms. rawgrep will likely win this one too in a future patch...
On memory, rawgrep uses 6% to 28% less than ripgrep on the Chromium tree, but 172% to 224% more on the Linux tree, though its peak never exceeds 40 MiB. fff’s peak RSS is not in the table, since nearly all of it lives in the long-running fff[d] process, not the benchmarked fff[q] client. On the Chromium corpus, fff[d] sits at about 250 MB after indexing and climbs to 2.43 GB during search, where it stays, even when idle. Although, I should add that fff-grep configures fff-search with an unlimited memory budget, so that it runs as fast as it possibly can.
Also, even with --no-cache, fff[d]’s SharedFilePicker still indexes the existing
files, and from what I've seen, that indexing cannot really be turned off. This likely explains why fff is faster than ripgrep in
six of the eight runs with the Tool cache off.
| Kernel | Linux 6.12.57+deb13-amd64 |
| CPU | 13th Gen Intel(R) Core(TM) i5-13400F, 16 threads |
| Hardware | DDR4 3200 MHz, NVMe SSD (Crucial P2 250 GB, ~994 MB/s read / 736 MB/s write) |
| Memory | 15Gi total, 4.5Gi used, 7.9Gi page cache, 10Gi available; swap 12Gi total, 0B used |
| Disk | nvme0n1 (benchmarking on partition /dev/nvme0n1p2) |
| Model | CT250P2SSD8 |
| Capacity | 232.9G, nvme, ssd |
| Firmware | P2CR048 |
| Filesystem | ext4 on / (rw,relatime,errors=remount-ro) |
| Block size | 512 B logical, 512 B physical |
| IO | scheduler [none] mq-deadline, read-ahead 128 KB, queue depth 1023 |
| PCIe link | 8.0 GT/s PCIe x4 (max 8.0 GT/s PCIe x4) |
| Data | ../chromium on /dev/nvme0n1p2 ../linux on /dev/nvme0n1p2 |
| rawgrep | 0.3.0 (rev 814e766) 14 October 2026, 19:33:42 |
| ripgrep | 15.2.0 (rev 3fce3b5bb0) |
| fff | 1.0.0 (fff_search 0.11.0) the CLI version used, is available at rakivo/fff-grep |
| hyperfine | 2.0.0 |
| Linux tree | 7.3.0-rc1, built with make defconfig && make run |
| Flags |
rawgrep: --jump --color never -n --reserved-tool-dirs --large --threads 16 --no-ignore, plus --no-cache in the no-cache runsfff[q]: no extra flags ripgrep:--vimgrep --color never -n --no-heading --threads 16 --no-ignore --no-column |
Tools like fff, Zoekt, ugrep, Google’s codesearch and others keep their own persistent caches, and those
caches are large: often tens of times the size of rawgrep’s, and
tens to hundreds of times as slow to prepare. That space is paid for on disk,
and paid for again in RAM, because their pages usually tend to stay resident. Some of them, for instance fff,
also demand a background file watcher, that also keeps the persistent cache always in RAM. By design,
rawgrep has no file watcher. By design, rawgrep is a one-shot program.
By design, Rawgrep’s caches are kept as light as they can be kept, while still providing the most performance benefit. Here is
everything rawgrep stores inside ~/.cache/rawgrep/ after the benchmarks above, not counting a few empty lock files:
| File | Chromium (~500k files) | Linux (~100k files) |
|---|---|---|
| fragment-cache-*.bin | 11 MiB | 2.4 MiB |
| dir-snapshot-*.bin | 8.0 MiB | 1.1 MiB |
| binary-verdicts.bin | 28 KiB | 2.3 KiB |
| Total | 19 MiB | 3.5 MiB |
rawgrep has no subcommand for preparing its caches, and by design it never will. It is fast enough to build them in passing, as a side effect of searching. Every run updates the caches automatically, but only when an update is actually necessary. The very first write, starting from no caches at all, happens after all the searching is done and all the matches are printed, and with warm OS cache it stays well under 100 ms of wall time, even on the Chromium corpus.
Each of these caches, and the rest of rawgrep’s machinery, is explained below.
The Fragment Cache is a rather small and heavily tuned cache, and it exists to answer one question as cheaply as possible: given this pattern, can a file be skipped without being read at all? It is inspired by the fragment-based filtering of nowgrep and works much like a Bloom filter with explicit tracking.
A fragment is a small sliding window of 3 or 4 bytes taken from the pattern and hashed.
Searching for ERROR: produces three of them:
ERRO RROR ROR:
For every file, rawgrep keeps a compact bitset of the fragments it has seen, that is trusted only while the
file's ctime is unchanged. A later search
can then rule a file out the moment one of the fragments it needs is known to be absent.
Because the cache is built from fragments rather than whole patterns, even a different pattern
profits from earlier searches whenever the two share fragments.
Real patterns are rarely plain strings. The Literal Extractor digs the literal text out of more sophisticated kinds of patterns, such as regular expressions and alternations, and works in conjunction with the Fragment Cache, so that even a pattern that looks hopelessly dynamic can be filtered by the literals hiding inside it.
Binary files rarely deserve a search. Between runs, rawgrep accumulates what it learns about each file, from probing its contents to looking at its name and extension, and collects it into the Binary Verdicts cache. On the next run it already knows which files to skip, unless you pass
--binary, which would force rawgrep to skip this cache. Binary Verdicts are likewise keyed on ctime.
The Directory Snapshot Cache follows naturally from rawgrep being a raw partition scanner.
Bypassing the VFS also means bypassing the kernel’s directory cache, so every directory would
otherwise be decoded from its on-disk structures on every run. Internally, in rawgrep, a directory entry is packed down
to just 12 bytes, so for directories that have not changed, rawgrep skips that decoding: it packs
their entries into the Directory Cache. A snapshot is trusted only while the
directory’s mtime and ctime still match and, following the
racy-git rule, only if both are older than the
moment the snapshot was written.
A cold run and a warm run are different problems. The Disk Warmness Checker inspects which virtual memory pages are resident and adapts the hinting algorithms accordingly.
This isn’t really a feature, but it’s quite an interesting detail.
Reading the block device directly brings a problem that conventional greps never meet: the
device has a page cache of its own, separate from the per-file one. When a file is edited,
writeback goes around that cache, so blocks cached before the edit stay stale until they are
dropped. Calling sync() does not help, and dropping the whole device cache on
every run would make every run cold.
So rawgrep drops only what can actually be stale: the pages of files whose
ctime shows they changed since the last boot. A tiny memo file records which of those files’
pages it has already dropped and re-read, so it does not drop them again on every run, and files
that have not been written to stay warm.
You can download the binaries from the v0.3.1 release page:
rawgrep-x86_64, with runtime SIMD dispatchrawgrep-x86_64-v3, built for the v3 subset, with AVX2 baked in$ rawgrep TODO $ rawgrep '[A-Z]+_SUSPEND' ../linux
rawgrep reads raw block devices such as /dev/sda1, which the OS protects.
Instead of running it under sudo every time, you grant the binary a single Linux
capability, CAP_DAC_READ_SEARCH, which does exactly one thing: it bypasses file
read permission checks, so any user who can run the binary can read any file on the partition.
$ sudo setcap cap_dac_read_search=eip ./rawgrep
To check what the binary has, or to revoke it and go back to sudo:
$ getcap ./rawgrep ./rawgrep = cap_dac_read_search+eip $ sudo setcap -r ./rawgrep
Capabilities are tied to the binary file itself, so run setcap again whenever
you replace it with a new download. rawgrep deliberately ships no script that runs
sudo for you: the command is a single line, and you should see exactly what it
does.
You can skip this step. rawgrep keeps its on-disk Fragment Cache mlocked in
memory once it is mapped, so a large scan under memory pressure cannot evict it. Locking
memory is possible without any permissions, but only a small amount, which is not enough for
the caches, even on the Linux corpus. Locking all of it requires root or the
cap_ipc_lock capability. Without that, rawgrep still works fine and falls back
to a best-effort strategy that is usually just as fast, but can occasionally be slower on a
cold cache under heavy concurrent memory pressure. To grant both capabilities in one
command, use this instead of the one above:
$ sudo setcap 'cap_dac_read_search,cap_ipc_lock=eip' ./rawgrep