Catching Up With Rust’s ripgrep in C# — Huge Files From 205 s to On Par, 86k Small Files From 19x Behind to On Par, and Complex Searches Where uvp Is 1.07–3.5x ripgrep

Tech

[Update, 16 October 2026] Speed today — complex searches went from 1.07–3.53× to 1.22–4.56×

The tables in this article are from v1.7.3.7 (4–5 October 2026) and the uvp 1.8.1.3 test build (6 October). The current release, v1.9.0 (14 October 2026), was measured on the same Mac.

ripgrep ÷ uvf / uvp (first + second run) This article (then) Now (v1.9.0)
Three complex searches, kernel source, about 60,000 files (uvp) 1.87–3.53× 2.35–4.56×
Three complex searches, one 1 GB file (uvp) 1.07–1.92× 1.22–2.13×
Seven searches, 3 GB / 10 GB (free uvf) per item 1.05–1.24× / 1/1.01–1.15× overall 1.08× / 1.08× (on par)
uvp first run (index build included, 3 GB plain string) 1/1.05 about 1/1.3 (within on par)

Over the seven searches uvp is 3.44× (3 GB) and 3.55× (10 GB), index build included. The 86,000-file and Rust-rewrite tables were not part of this re-measurement. Details: Benchmarks and the v1.9.0 “Full Spectrum” article.

It was not the language.

ripgrep is written in Rust; my uvf and uvp are written in C# (.NET). Three weeks ago, a search on a 50 GB file took 205 s against ripgrep’s 56 s — nearly four times slower. I kept asking myself whether Rust was simply faster.

Here is the record of chasing ripgrep in three arenas, plus what happened when I rewrote the same design in Rust. Where the gap really came from becomes clear partway through section 2.

The short answer: the gap was in how many times we read, how many times we opened files, and how we narrowed — not the language. After fixing those, the free uvf is on par with ripgrep and uvp, which keeps an index, is faster

Huge files (50 GB of OpenStreetMap XML): uvf went from 205 s to 50.82 s, on par with ripgrep (54.94–55.81 s). Many small files (the Linux kernel source, 86,000 files): the second run used to take 19.3 times as long as ripgrep’s; over eight searches, cold plus hot, it is now 1/1.08 — on par. The paid uvp is 4.0x. Complex searches (multi-line blocks, lines near each other, collecting into CSV): uvp is 1.07–3.53x ripgrep (with perl or awk added where ripgrep needs them). Against a feature-rich grep app for Windows, uvp was 8.6–43x faster, and even ripgrep was 7.9–17.9x faster than that app. Finally, a Rust rewrite with the same design ran at the same speed as the C# version (36.85 s vs 37.55 s), and the gap to ripgrep did not shrink. Mac (Apple M4, 10 cores), 32 GB RAM, external USB SSD. Details at the end.

uvf and uvp are the search commands of UwView, which I develop (uvf is the free edition, uvp belongs to the paid UwView Pro). The current stable release is v1.7.3.6.9; v1.7.3.7 is a Mac-only preview. The complex searches in section 3 use features coming in the next release, v1.8 (ranges -range, masking -mask, formatting -format), measured on a test build (uvp 1.8.1.3). Versions and dates for each figure are listed at the end.


1. Huge files: from 205 s to on par. What helped was how many times we read, and how

Time to find 東京 (94,979 hits) in 50 GB of XML (890 million lines), each right after dropping the cache (cold).

When / version uvf ripgrep Ratio
2026-09-15, v1.6.0 205 s 56 s ripgrep 3.7x faster
Two days later 50.82 s 54.94–55.81 s On par

None of the fixes had anything to do with Rust versus C#.

  • The file was being read two and a half times. Collecting line starts (about 100 s), searching (about 82 s), re-reading the matched lines (about 20 s). These became a single read
  • Sequential reads through mmap used only 57% of the disk’s bandwidth. mmap gave 551 MB/s; pread in 4 MB chunks gave 966 MB/s, level with a raw read (dd, 927 MB/s)
  • Counting newlines with SIMD did not save a single second when reading from disk. The disk was the bottleneck (it did pay off later, for the second run from cache — see 1.7.3.6.9 below)

Rust’s ripgrep is just as sensitive to how it reads. It reads large files through mmap by default, but on Windows 11, 50 GB with a regex went from 282.84 s to 97.83 s (2.89x) with --no-mmap (no change on macOS). How you read matters before which language you use.

The main changes after that:

  • v1.7.3.6.7: regex plus ignore-case (-E -i) went from 1/1.68 of ripgrep to 1/1.07. The anchor characters chosen were = and ", which match almost every attribute in XML. Anchors are now chosen from letters
  • v1.7.3.6.9: the second run from cache edged past ripgrep (3 GB: 0.29 s vs 0.31 s; 10 GB: 0.88 s vs 0.97 s). Counting newlines 16 bytes at a time took 3 GB from 0.15 s to 0.028 s
  • There was exactly one C#-specific issue. The shipped uvf is NativeAOT (compiled ahead of time), where .NET regular expressions run interpreted. Only for heavy patterns that must be applied to every line, uvf hands the search to the GUI executable in the same folder, where the JIT works

Where things stand now (v1.7.3.7, seven searches, cold plus hot):

One large file uvf ratio uvp ratio
3 GB 1.05–1.24x (only -E -v 1/1.10) 1/1.05–4.86x
10 GB 1/1.01–1.15x 1/1.10–5.29x
50 GB 1.06–1.23x 1.68–9.61x

uvp‘s 1/1.05 and 1/1.10 are the first search, which builds the index (.uwvz). From the second search on, it reads the index.

2. Many small files: from 19x behind to on par. What helped was how many times we opened files

Having caught up on huge files, I relaxed — until the Linux kernel source (86,000 files, 1.9 GB) showed a completely different face.

Version uvf second run (PageTransHuge) ripgrep Ratio
1.7.3.6.5 26.58 s 1.38 s ripgrep 19.3x faster
1.7.3.6.7 3.40 s 1.44 s ripgrep 2.4x faster

The cause was not the search but how many times files were opened. Just opening and closing 86,000 files one by one takes 7.7 s. Before searching, the old version opened every file three times in sequence to check “is this a pbf?” and “is this compressed?”. Now only files whose name settles the format are checked up front; the rest are decided from the first bytes when the search opens them.

Next, five searches, cold plus hot combined:

Version uvf ripgrep Ratio What changed
1.7.3.6.7 52.18 s 31.50 s 1/1.66 Fewer opens (above)
1.7.3.6.9 36.21 s 30.22 s 1/1.20 (on par) Idle workers fetch the next file; files are opened by absolute path (just opening and reading with 8 workers: 2.33 s to 1.59 s); files of 256 KB or less are read in one go
1.7.3.7 (eight searches) 60.85 s 56.25 s 1/1.08 (on par) —

uvp took a longer road. Its first version (1.7.3.6) crashed trying to bundle 86,000 files into one index (the list of source files grew to 20.9 MB, over a 1 MB limit). With the limit fixed it could bundle them, but building the index took 56.31 s. It now takes 8.4 s (including the first search), and eight searches cold plus hot take 14.02 s — 4.0x ripgrep.

Rewriting it in Rust

This is where I tested what I most wanted to know: would the same design, written in Rust, catch ripgrep?

I built two Rust versions of uvf‘s search, called ruvf: port, a one-to-one translation, and idiom, which hands the searching to Rust’s regex crate. After confirming that their output matches uvf byte for byte, I measured them on the 86,000-file kernel tree with five searches (cold plus hot, median of three).

ripgrep uvf (C#) ruvf-idiom (Rust) ruvf-port (Rust)
30.62 s 37.55 s 36.85 s 42.89 s

The Rust version ran at the same speed as the C# version (idiom vs uvf: 1.02x). The gap to ripgrep (uvf at 1.23x ripgrep’s time) did not shrink in Rust. port is slower because it copied uvf‘s habit of rebuilding the regex for every file (Rust’s regex is heavier to build than .NET’s); building it once makes it match idiom. Startup is 1.7 ms for ruvf, 2.7 ms for ripgrep and 4.7 ms for uvf — about 3 ms apart.

The gap was in the design, not the language. My working theory: ripgrep starts searching while it walks the directories, whereas uvf finishes walking first so it can print files in name order. That can be fixed without leaving C#.

There was one place where Rust was clearly faster: regular expressions with no required literal string to extract (2–5x on 200 MB). The reasons: .NET regex runs interpreted under NativeAOT, and Rust’s regex narrows lines first even with a single character or a byte sequence. Adding the second to the C# uvf made those patterns 2.3–6.1x faster; [0-9]{4}-[0-9]{2} now matches ripgrep (1.34 s vs 1.33 s) and [ぁ-ん]{3,} beats the Rust version (0.45 s vs 1.17 s). What I learned from Rust was not the language but the way to narrow.

The full details — startup times, all ten regex patterns, and a case where writing it “the Rust way” was slower — are in a Qiita article (Japanese): Rust で書き直せば ripgrep に並ぶのか

3. Complex searches: ripgrep works line by line, uvp chains stages

The last arena is three searches presented as typical uses of a feature-rich grep app for Windows. None can be written as a single-line match.

  • Example 1: take the multi-line blocks from [ERROR] SessionStart to [INFO] SessionEnd, keep only those that do not contain Error_Code: 503, and mask the IP addresses inside as x.x.x.x
  • Example 2: places where DatabaseTimeout and User_ID: 9999 appear within 10 lines, with no IgnoredWarning
  • Example 3: pull User, Action and Time out of lines where they come in any order, and collect them as CSV

With ripgrep, example 1 is a multi-line rg -U -P regex piped into perl, example 2 collects lines with rg and checks nearness with awk, and example 3 is rg -P -o -r '$1,$2,$3'. From v1.8, uvp writes each as one command.

# Example 1: write the session blocks without 503, with IPs masked
uvp app.log -seq '[ERROR] SessionStart,[INFO] SessionEnd' -range \
            -grep 'Error_Code: 503' -v \
            -mask '\b(\d{1,3}\.){3}\d{1,3}\b' 'x.x.x.x'

# Example 3: extract User, Action and Time into CSV (any order)
uvp '*.log' '^(?=.*User: (\S+))(?=.*Action: (\S+))(?=.*Time: (.+?)\s*$)' -E \
            -format '$1,$2,$3' --csv --csv-header 'name,action,time' -out result.csv

Two arenas: kernel is the kernel’s *.c and *.h files (about 60,000 files, 1.8 GB) with example snippets inserted into about 6% of them; big is a single 1 GB log. Cold plus hot combined; uvp and ripgrep produced the same results every time.

Arena Example Mac: ripgrep ÷ uvp Windows: ripgrep ÷ uvp Windows: grep app ÷ uvp
kernel Ex. 1 range, exclude, mask 1.87 1.85 20.26
kernel Ex. 2 nearby, AND, NOT 2.14 1.65 22.92
kernel Ex. 3 collect to CSV 3.53 2.39 42.94
big Ex. 1 1.07 1/1.03 9.07
big Ex. 2 1.46 1.09 8.62
big Ex. 3 1.92 2.27 38.50

(Above 1 means uvp is faster. 1/1.03 means uvp takes 1.03 times as long — on par.)

For speed, the feature-rich grep app was not usable. It runs on Windows only, and its trial cannot write results, so only time was measured. It was also asked to do only the search with the same pattern — no collecting, masking or excluding. Even so it took 8.6–43 times as long as uvp and 7.9–17.9 times as long as ripgrep, and a single kernel example took two to three minutes cold. GUI conveniences such as checking replacements on screen were not part of this comparison.

ripgrep really is fast. Even with perl or awk attached, the gap to uvp stays within 1.1–3.5x. uvp comes out ahead because its index shortens the second read and it runs the stages (range, exclude, mask, format) in one pass.

To be honest: looking at the second (hot) run alone, uvp is 1/1.2 and 1/1.1 on the Mac kernel examples 1 and 2 (on par), because opening the index adds a fixed cost. On Windows, the first kernel run (including building the index) takes 65–74 s, against 6–9 s on the Mac. The cause has not been investigated yet.

What I learned: the difference between Rust and C# was almost entirely a difference in design

  1. Speed was decided by how many times we read, how many times we opened files, and how we narrowed. Reads for huge files, opens for small files, and narrowing clues for regex. All of them were fixable without changing language
  2. The only C#-specific issue was interpreted regex under NativeAOT. Handing heavy patterns to the JIT executable and narrowing with clues first left no gap
  3. Do not assume “it’s fast because it’s Rust”. Rust’s ripgrep itself varies 2.89x depending on how it reads (mmap or pread)
  4. For a one-off search, ripgrep is still the best tool. uvp, with its index, comes out ahead when you ask the same place many times and when you chain stages for complex searches

Being honest

  • On small files, uvf‘s second run is still slower (1/1.4 of ripgrep on the kernel tree; about 2.1 s of fixed cost for opening each file). Cold plus hot it is on par (1/1.08)
  • uvp‘s first pass when bundling many files is slow (12 mixed plain and gz files: 24.95 s vs 39.78 s — ripgrep 1.59x faster)
  • uvp‘s second run carries a fixed cost of about 0.12 s (one XML split into 1,000 files: 0.043 s vs 0.126 s — ripgrep 2.93x faster)
  • The uvp in section 3 is a test build (1.8.1.3); ranges, masking and formatting are planned for v1.8
  • ruvf is an experiment for comparison and will not be distributed
  • Timings vary between environments. Do not compare seconds across operating systems

Conditions behind the numbers

  • Mac (Apple M4, 10 cores, 32 GB RAM), external USB SSD. The Windows columns come from a different machine (Windows 11, x64, 8 logical cores, 16 GB RAM)
  • Cold = first run after sudo purge and a 10 s wait; hot = the run straight after. Ratio = ripgrep’s time ÷ ours (cold plus hot ratios use the combined time)
  • Section 1: v1.6.0 and two days later, 15–17 September 2026 (cold, uvf). ripgrep mmap: September 2026 (ripgrep 15.2.0). Table: v1.7.3.7 (4–5 October 2026)
  • Section 2: 1.7.3.6.5 and 1.7.3.6.7 on 2–3 October 2026, 1.7.3.6.9 on 4 October, eight searches on 1.7.3.7 (5 October). ruvf on 4 October (rustc 1.99.0, regex 1.13.1, memchr 2.8.3; uvf 1.7.3.6.9)
  • Section 3: uvp 1.8.1.3 (test build), ripgrep 15.2.0, 6 October 2026. The grep app was a trial version, timed only
  • Every item’s output was compared with ripgrep (or sed and similar): zero mismatches
  • Counting “file opens” on the 86,000-file kernel tree (Zenn, Japanese): https://zenn.dev/amru195704/articles/5f2a6203ec7f61
  • ruvf (the Rust rewrite) vs uvf and ripgrep (Qiita, Japanese): https://qiita.com/amru195704/items/149c10ff2be8c82c9bb7
  • ripgrep’s mmap vs --no-mmap on 50 GB across three OSes: https://uvp.y42u.net/en/blog/uvp-rg-no-mmap-50gb-en/
  • Counting newlines took 40% of the second search: https://uvp.y42u.net/en/blog/uwview-wf12-newline-hot-en/
  • Regex compared (what .NET and Rust can and cannot write): https://uvp.y42u.net/en/blog/uwview-wf09-regex-vs-ripgrep-en/
  • Every place where ripgrep still wins: https://uvp.y42u.net/en/blog/uwview-wf14-where-ripgrep-wins-en/
  • First Light recap (only cutting the number of reads): https://uvp.y42u.net/en/blog/uwview-fl13-first-light-recap-en/
  • All measured data (benchmarks): https://uvp.y42u.net/en/benchmarks-en/
  • Source code (GitHub): https://github.com/amru195704/UwView

If you ask the same logs again and again, try UwView Pro with its index. Searches from the second one on get a step faster, and logs are stored at about 1/9 the size (all OSes, one-time or monthly plans, 14-day free trial).


From the developer: apps, Kindle books and public projects are listed at GitHub: amru195704.


Copied title and URL