Store short strings inline instead of interning them (#24)#42
Merged
Conversation
Strings up to `size_of::<usize>() - 1` bytes (7 on 64-bit, 3 on 32-bit) are now encoded directly inside the pointer-sized `IValue`, avoiding an allocation and a global-cache lookup for the common case of short JSON keys and values. Longer strings are interned as before. Encoding: an inline string carries the `StringOrNull` tag plus a new inline-discriminator bit (bit 2) in the low control byte, with the length in bits 3-5 and up to `INLINE_CAPACITY` UTF-8 bytes in the remaining bytes. To keep bit 2 a reliable discriminator, the heap string `Header` is now 8-aligned (already the case on 64-bit via its `AtomicUsize`; the explicit `align(8)` is what enables the optimization on 32-bit). Correctness rests on a single invariant: representation is a pure function of content (`len <= INLINE_CAPACITY` => inline, else interned), so `IString`'s pointer-based equality and hashing remain valid. The control byte is kept in the value's low byte on both endiannesses, so the tag/flag/length live in well-defined bits; only the in-memory char offset differs, handled via `cfg(target_endian)`. Behaviour change: `IString::as_str().as_ptr()` is no longer stable across equal short strings (they are no longer deduplicated to a shared allocation); value equality via `==` is unaffected. Validated across the platform matrix: native 64-bit LE tests + Miri, 32-bit LE (i686) tests, and big-endian 64-bit (s390x) and 32-bit (powerpc) tests under Miri emulation.
Replace the separate `new_inline_string` constructor with a `payload` parameter on `new_inline`, ORed with the type tag. The inline singletons (null/true/false) pass `0`; inline strings pass a payload carrying the inline flag, length, and characters with the tag bits left clear.
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.
Closes #24.
Implements the inline short-string optimization sketched in the issue thread: strings up to
size_of::<usize>() - 1bytes — 7 on 64-bit, 3 on 32-bit — are encoded directly inside the pointer-sizedIValue, skipping both the allocation and the global-cache lookup. This covers the common case of short JSON keys/values ("id","no","yes","type","x", …). Longer strings are interned exactly as before.Encoding
An inline string reuses the
StringOrNulltag and adds a discriminator in the low control byte:011= inlineINLINE_CAPACITYUTF-8 bytesHeaderis now 8-aligned so heap pointers always have bit 2 clear. On 64-bit that was already true (theAtomicUsizerefcount forces 8-alignment); the explicit#[repr(align(8))]is what makes the optimization work on 32-bit (costs ~4 bytes of header padding, only on interned strings).null(value1) has bit 2 clear, so it stays distinct from inline strings; the niche is preserved (inline values are non-zero), soOption<IValue>stays pointer-sized.Why it's sound
Everything rests on one invariant: representation is a pure function of content —
len ≤ INLINE_CAPACITY⇒ inline, otherwise interned. So two equal strings always share a representation, andIString's pointer-based==/hashstay correct (an inline value's bits are its content).Endianness
The control byte is kept in the value's low byte on both endiannesses (so tag/flag/length live in well-defined bits via
ptr_usize()); only the in-memory character offset differs, handled withcfg(target_endian).Behaviour change
IString::as_str().as_ptr()is no longer stable across equal short strings — they're no longer deduplicated to a shared allocation (they use no heap at all). Value equality via==is unaffected. Two existing tests that asserted pointer identity for short strings were updated to use long strings; new tests cover the inline path.Validation
Full platform/endianness matrix:
cargo test(36 pass) +cargo miri test(no UB)cargo test --target i686-pc-windows-msvc(36 pass)cargo miri test --target s390x-…(emulated, pass)cargo miri test --target powerpc-…(emulated, pass)Plus
clippy --all-targets --all-featuresandfmt --checkclean. New tests cover: inline round-trip for lengths 0..=CAP incl. multibyte UTF-8, the CAP/CAP+1 inline↔heap boundary, empty-string-is-inline-not-null, and mixed inline/heap keys in anIObject.Scope
Addresses request (1) fully and request (2) for the common case (short unique strings now cost nothing). Not interning long unique strings is a deliberate non-goal: without canonicalization,
IString's O(1) pointer equality/hash — the crate's core value — would break.