Skip to content

Store short strings inline instead of interning them (#24)#42

Merged
Diggsey merged 2 commits into
masterfrom
feature/issue-24-inline-short-strings
Jul 15, 2026
Merged

Store short strings inline instead of interning them (#24)#42
Diggsey merged 2 commits into
masterfrom
feature/issue-24-inline-short-strings

Conversation

@Diggsey

@Diggsey Diggsey commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Closes #24.

Implements the inline short-string optimization sketched in the issue thread: strings up to size_of::<usize>() - 1 bytes — 7 on 64-bit, 3 on 32-bit — are encoded directly inside the pointer-sized IValue, 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 StringOrNull tag and adds a discriminator in the low control byte:

bits 0–1 bit 2 bits 3–5 remaining bytes
tag 01 1 = inline length up to INLINE_CAPACITY UTF-8 bytes
  • Bit 2 distinguishes inline from a heap string pointer. For that to be reliable, the heap Header is now 8-aligned so heap pointers always have bit 2 clear. On 64-bit that was already true (the AtomicUsize refcount 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 (value 1) has bit 2 clear, so it stays distinct from inline strings; the niche is preserved (inline values are non-zero), so Option<IValue> stays pointer-sized.

Why it's sound

Everything rests on one invariant: representation is a pure function of contentlen ≤ INLINE_CAPACITY ⇒ inline, otherwise interned. So two equal strings always share a representation, and IString's pointer-based ==/hash stay 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 with cfg(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:

Target How
x86-64 LE (native) cargo test (36 pass) + cargo miri test (no UB)
i686 LE (32-bit) cargo test --target i686-pc-windows-msvc (36 pass)
s390x BE (64-bit) cargo miri test --target s390x-… (emulated, pass)
powerpc BE (32-bit) cargo miri test --target powerpc-… (emulated, pass)

Plus clippy --all-targets --all-features and fmt --check clean. 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 an IObject.

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.

Diggsey added 2 commits July 7, 2026 19:21
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.
@Diggsey
Diggsey merged commit 86bddc0 into master Jul 15, 2026
16 checks passed
@Diggsey
Diggsey deleted the feature/issue-24-inline-short-strings branch July 15, 2026 21:05
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.

Avoid intern short Strings

1 participant