Visitar URL original
Improve performance of `String#dump` using lookup table by FletcherDares · Pull Request #17287 · ruby/ruby · GitHub
Skip to content

Improve performance of String#dump using lookup table - #17287

Open
FletcherDares wants to merge 1 commit into
ruby:masterfrom
FletcherDares:fletcher/string-dump-ascii-fastpath
Open

FletcherDares wants to merge 1 commit into
ruby:masterfrom
FletcherDares:fletcher/string-dump-ascii-fastpath

Conversation

@FletcherDares

@FletcherDares FletcherDares commented Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Background

String#dump currently walks the input twice: once to compute the exact result length, and once to write the escaped output.

For printable ASCII bytes, both passes still advance one byte at a time through the full switch/branch structure:

if (ISPRINT(c)) {
    clen = 1;
}

That is cheap for small strings, but it leaves a lot of overhead on common long strings where most bytes are safe ASCII and do not need escaping.

Proposal:

Reuse the existing String#inspect safe-ASCII lookup table introduced in #17126 for String#dump, and make it a shared string escaping table:

while (p < pend && ascii_no_escape[(unsigned char)*p]) p++;

The existing handling for quotes, backslashes, control bytes, UTF-8 non-ASCII bytes, invalid bytes, and non-ASCII-compatible encodings stays on the current paths.

Justification:

The common case for String#dump is printable ASCII text with only occasional bytes that need escaping. Before this change, a long run like:

"The quick brown fox jumps over the lazy dog. " * 1000

still paid per-byte dispatch in both the length calculation and the copy pass. After this change, those runs are skipped/copied in bulk, while unsafe bytes still fall back to the existing logic.

Tradeoffs:

This adds a lookup-table scan after the first printable byte in the generic printable branch. Inputs with long safe ASCII runs benefit substantially.

Inputs with few or no safe ASCII runs can be slower because they pay the added table check without getting much bulk-copy benefit.

Results:

Benchmark Control Experiment Speedup
dump_ascii 82.382k i/s 383.097k i/s 4.65x
dump_utf8 12.289k i/s 12.006k i/s 0.98x
dump_mixed 64.189k i/s 85.939k i/s 1.34x
dump_binary_ascii 186.546k i/s 204.016k i/s 1.09x
dump_binary_nonascii 176.486k i/s 169.944k i/s 0.96x
dump_escaped_chars 642.436k i/s 694.995k i/s 1.08x
dump_evstr 198.574k i/s 388.391k i/s 1.96x
  • Apple M4 Pro, macOS 26.3.1.
  • Used built-in benchmark tooling
  • Experiment: built ruby 4.1.0dev from this branch

@FletcherDares
FletcherDares force-pushed the fletcher/string-dump-ascii-fastpath branch 4 times, most recently from aa8d435 to 921100f Compare June 16, 2026 18:21
@FletcherDares
FletcherDares force-pushed the fletcher/string-dump-ascii-fastpath branch from c05cf14 to 32c383b Compare June 17, 2026 15:57
@FletcherDares
FletcherDares marked this pull request as ready for review June 17, 2026 16:05
@ioquatix
ioquatix requested a review from ima1zumi June 17, 2026 21:00
@sampokuokkanen

sampokuokkanen commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

I tried running the benchmark and got similar results. The tradeoffs might not be as big as you state, as the ISPRINT(c) check is existing and before you enter the loop. Could you try running the bench with GC.disable? I see no change in dump_binary_nonascii with it and dump_utf8 is 1 to 3 percent slower. (I guess it's only because the method is getting bigger with the loops added?)

The tradeoff is real for interleaved printable and unsafe bytes though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants