Repository navigation
Shrink parsed strings that contained escape sequences - #1135
Closed
fuzzyrichie wants to merge 1 commit into
Closed
fuzzyrichie wants to merge 1 commit into
fuzzyrichie wants to merge 1 commit into
Conversation
json_string_unescape allocates its output buffer with the size of
the raw (escaped) input, then decodes into it: \uXXXX escapes
compress 6 raw bytes to at most 4 output bytes (12 for a surrogate
pair), \n and friends compress 2 to 1. rb_str_set_len only sets
the length, so every parsed string containing escapes retains the
raw size as buffer capacity for as long as the parsed document is
alive:
JSON.parse('["' + 'm\u00e9lange\u00e0 ' * 3000 + '"]')
# 200 such 50 KB strings: 4.8 MB of retained capacity hold
# 7.8 MB of content (61% slack)
Use rb_str_resize instead, which on shrink both embeds strings
that now fit inline (freeing their heap buffer) and reallocates
still-larger buffers to the exact length, following MRI's own
shrink policy. Measured on ruby 3.4.12 with a 12.5 MB document
of 50 KB escaped strings: retained string capacity drops from
12.52 MB to 7.76 MB (content 7.75 MB, slack 61% -> 0%), with parse
time unchanged (0.129 -> 0.131 ms per 130 KB document, within
noise) and no new allocations. Documents without escapes are
unaffected (fast path never enters unescape).
Member
|
This is behavior is intended, it's not work risking a |
This was referenced Nov 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
json_string_unescapeallocates its output buffer with the size of the raw (escaped) input — a correct upper bound, since unescape can only shrink (\uXXXX: 6 raw bytes → at most 4 output bytes, 12 → 4 for a surrogate pair,\n-style: 2 → 1) — but then finishes with:rb_str_set_lensets the length and never releases capacity, so every parsed string that contains escapes retains the raw size as buffer capacity for as long as the parsed document is alive. Documents produced by Python'sjson.dumps(ensure_ascii=True)(Python's default) hit this hardest, since all non-ASCII comes out as\uXXXX.Measured (ruby 3.4.12, json 3.1.0, a 12.5 MB document of 200 × 50 KB escaped strings):
Fix
One line: use
rb_str_resize, whose shrink path implements MRI's own policy — strings that now fit inline are embedded (their heap buffer freed), and still-larger buffers arerealloced down to the exact length when the waste exceeds MRI's threshold. No json-side threshold needed, no new allocations.I first tried the obvious alternative — build an exact-size copy when the waste is at least a malloc quantum — but measured it at +19% parse time on
\u-heavy documents (one extra malloc per escaped string) and dropped it.rb_str_resizeachieves the same retention win allocation-free: realloc-in-place / embed, no churn.Measurements (same machine, stock ruby 3.4.12)
Validation
rb_str_resizemaintains the terminator and encoding; the shrink clears coderange on a buffer whose coderange was unknown anywayCompatibility note
rb_str_resize's embed-on-shrink is the modern-MRI behavior I measured (3.4); on the gem's minimum (2.7) the shrink path is less aggressive (realloc only above MRI's waste threshold), so on older rubies the win is smaller — but never worse thanrb_str_set_len.Found during a memory-consumption pass over the parser (allocation-site review + instrumented benchmarks); the only other significant retained-memory sites (container presizing, name interning, generator buffer) were already exact.