A 1-word (8-byte) sized representation of immutable UTF-8 strings. In-lines up to 8 bytes.
See the codeA 1-word (8-byte) sized representation of immutable UTF-8 strings that in-lines up to 8 bytes.
ColdString minimizes per-string overhead for both short and large strings.
This leads to substantial memory savings over both String and other short-string crates (see Memory Comparison (System RSS)):
String in HashSetHashSetString in BTreeSetBTreeSetColdString's MSRV is 1.60, is no_std compatible, and is a drop in replacement for immutable Strings.
Use it like a String:
use cold_string::ColdString;
let s = ColdString::new("qwerty");
assert_eq!(s.as_str(), "qwerty");
Use ArcColdString when clones should share long-string storage:
use cold_string::ArcColdString;
let first = ArcColdString::new("a string longer than one machine word");
let second = first.clone();
assert_eq!(first, second);
ArcColdString uses a native-width reference count. ArcColdString8,
ArcColdString16, and ArcColdString32 use 1-, 2-, and 4-byte counts when a
smaller heap header is preferable. All variants remain one word in size.
Both types keep strings up to one machine word inline. For longer strings,
ArcColdString stores [atomic reference count][variable-length length][UTF-8 bytes]
in one allocation and does not support weak references.
Packs well with other types:
use cold_string::ColdString;
use std::mem::size_of;
assert_eq!(size_of::<ColdString>(), size_of::<usize>());
// ColdString has a null-niche:
assert_eq!(size_of::<Option<ColdString>>(), size_of::<ColdString>());
ColdString is an 8-byte tagged pointer (4 bytes on 32-bit machines):
Internally, both string types use a one-word tagged pointer representation.
ColdString points to a [variable-length (length - inline capacity)][UTF-8 bytes] allocation,
while ArcColdString adds an atomic reference count before the same payload.
The 8 bytes encode one of three representations indicated by the 1st byte:
10xxxxxx: encoded contains a tagged heap pointer. To decode the address, clear the tag bits (10 → 00) and rotate so the 00 bits become the least-significant bits. The heap allocation uses 4-byte alignment, guaranteeing the least-significant 2 bits of the address are 00. On the heap, the UTF-8 characters are preceded by a variable-length encoding of length - size_of::<usize>(), since strings up to one word are stored inline. Encoded values 1–127 use one byte, 128–16,383 use two bytes, and so on. On a 64-bit target, those ranges correspond to actual string lengths 9–135 and 136–16,391.11111xxx: xxx is the length and the remaining 0-7 bytes are UTF-8 characters.xxxxxxxx: All 8 bytes are UTF-8.The exception is if encoded is usize::MAX, which represents one word of NUL bytes.
10xxxxxx and 11111xxx are chosen because they cannot be valid first bytes of UTF-8.
The heap representation stores the length on the heap, not inline in the struct. This saves memory in the struct itself but slightly increases the cost of len() since it requires a heap read. In practice, the len() cost is only marginally slower than inline storage and is typically negligible compared to memory savings, cache density improvements, and 3x faster operations on inlined strings.
ColdString uses unsafe to implement its packed representation and pointer tagging. Usage of unsafe is narrowly scoped to where layout control is required, and each instance is documented with // SAFETY: <invariant>. To further ensure soundness, ColdString is written using Rust's strict provenance API, handles unaligned access internally, maintains explicit heap alignment guarantees, and is validated with property testing and MIRI.
Memory usage per string, measured by tracking the memory requested by the allocator:
Resident set size in bytes per insertion of various collections. Insertions are strings with random length 0..=N:
| Vec | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 8.0 | 8.0 | 23.2 | 33.7 | 53.4 |
| compact_str | 24.0 | 24.0 | 24.0 | 34.6 | 60.6 |
| compact_string | 22.9 | 24.9 | 31.6 | 39.7 | 55.7 |
| smallstr | 24.0 | 24.0 | 38.0 | 50.3 | 68.4 |
| smartstring | 24.0 | 24.0 | 24.0 | 40.4 | 65.4 |
| smol_str | 24.0 | 24.0 | 24.0 | 39.9 | 71.2 |
| std | 35.8 | 37.4 | 45.8 | 54.2 | 70.5 |
| HashSet | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 18.9 | 18.9 | 34.5 | 45.5 | 64.0 |
| compact_str | 52.4 | 52.4 | 52.4 | 62.2 | 88.9 |
| compact_string | 23.2 | 30.0 | 39.6 | 49.1 | 65.9 |
| smallstr | 52.4 | 52.4 | 66.5 | 78.6 | 96.9 |
| smartstring | 52.4 | 52.4 | 52.4 | 68.2 | 94.0 |
| smol_str | 52.4 | 52.4 | 52.4 | 68.3 | 99.4 |
| std | 56.8 | 61.9 | 72.2 | 81.7 | 98.5 |
| BTreeSet | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 10.1 | 18.9 | 49.3 | 79.1 | 117.2 |
| compact_str | 24.8 | 48.4 | 61.5 | 90.5 | 145.7 |
| compact_string | 19.7 | 43.7 | 67.0 | 88.3 | 122.4 |
| smallstr | 24.8 | 48.1 | 89.7 | 121.9 | 162.0 |
| smartstring | 24.5 | 48.6 | 61.1 | 102.3 | 155.8 |
| smol_str | 25.0 | 48.3 | 61.6 | 100.7 | 166.7 |
| std | 35.8 | 70.4 | 102.9 | 128.9 | 165.5 |
| Crate | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 10.0 | 9.2 | 25.3 | 30.0 | 37.2 |
| compact_str | 8.8 | 10.1 | 10.0 | 14.4 | 49.4 |
| compact_string | 34.5 | 34.8 | 37.5 | 34.9 | 38.3 |
| smallstr | 8.9 | 9.4 | 23.1 | 44.9 | 32.7 |
| smartstring | 14.8 | 15.1 | 15.0 | 26.9 | 49.5 |
| smol_str | 19.2 | 19.8 | 20.1 | 23.4 | 33.7 |
| std | 28.6 | 31.4 | 34.9 | 32.0 | 33.1 |
| Crate | 4..=4 | 8..=8 | 16..=16 | 32..=32 | 64..=64 |
|---|---|---|---|---|---|
| cold-string | 6.5 | 4.2 | 34.2 | 34.3 | 36.2 |
| compact_str | 7.5 | 7.5 | 7.6 | 31.0 | 32.4 |
| compact_string | 29.2 | 28.9 | 29.2 | 29.9 | 32.3 |
| smallstr | 4.5 | 2.6 | 28.7 | 28.5 | 29.9 |
| smartstring | 14.7 | 14.8 | 8.6 | 61.6 | 63.4 |
| smol_str | 15.2 | 12.8 | 15.7 | 41.7 | 42.0 |
| std | 28.2 | 27.6 | 28.6 | 29.3 | 30.4 |
Theoretical overhead on a 64-bit target, excluding one pointer and the UTF-8 payload. Inline values have no pointer or heap allocation, so their overhead is shown relative to the string bytes alone. Allocator rounding is not included.
| Type | 8 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 24 | 24 | 24 |
arcstr::ArcStr | 16 | 16 | 16 |
ArcColdString | 0 (inline) | 9 | 10 |
ArcColdString32 | 0 (inline) | 5 | 6 |
RSS bytes per unique string in a pre-sized Vec, measured on 64-bit Windows
with 1,000,000 strings per subprocess. Each cell is the median of three isolated
release-mode runs and includes the Vec element, heap allocation, and allocator
rounding. RSS results will vary by operating system and allocator.
| Type | 8 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 47.1 | 175.4 | 560.0 |
arcstr::ArcStr | 39.1 | 167.5 | 552.2 |
ArcColdString | 8.0 | 167.5 | 552.1 |
ArcColdString32 | 8.0 | 151.4 | 536.8 |
Criterion point estimates in nanoseconds per operation on 64-bit Windows (release mode, 100 samples). Lower is better. Allocation and clone outputs are dropped outside the timed region; drop measures a non-final reference-count decrement.
Fresh allocation and string copy:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 28.03 | 29.57 | 36.98 |
arcstr::ArcStr | 27.19 | 28.70 | 38.46 |
ArcColdString | 30.12 | 32.61 | 43.70 |
Clone/reference-count increment:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 3.86 | 3.82 | 3.82 |
arcstr::ArcStr | 3.39 | 3.41 | 3.43 |
ArcColdString | 3.44 | 3.45 | 3.43 |
String access:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 1.02 | 1.03 | 1.04 |
arcstr::ArcStr | 1.02 | 1.02 | 1.02 |
ArcColdString | 1.38 | 1.68 | 1.70 |
Non-final drop/reference-count decrement:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 2.43 | 2.49 | 2.43 |
arcstr::ArcStr | 2.52 | 2.53 | 2.52 |
ArcColdString | 2.17 | 2.18 | 2.19 |
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
190 followers · starred Feb 2026
338 followers · starred Feb 2026
111 followers · starred Feb 2026
27 followers · starred Apr 2026
Rust
94.7%
Python
5.3%
A 1-word (8-byte) sized representation of immutable UTF-8 strings. In-lines up to 8 bytes.
See the codeA 1-word (8-byte) sized representation of immutable UTF-8 strings that in-lines up to 8 bytes.
ColdString minimizes per-string overhead for both short and large strings.
This leads to substantial memory savings over both String and other short-string crates (see Memory Comparison (System RSS)):
String in HashSetHashSetString in BTreeSetBTreeSetColdString's MSRV is 1.60, is no_std compatible, and is a drop in replacement for immutable Strings.
Use it like a String:
use cold_string::ColdString;
let s = ColdString::new("qwerty");
assert_eq!(s.as_str(), "qwerty");
Use ArcColdString when clones should share long-string storage:
use cold_string::ArcColdString;
let first = ArcColdString::new("a string longer than one machine word");
let second = first.clone();
assert_eq!(first, second);
ArcColdString uses a native-width reference count. ArcColdString8,
ArcColdString16, and ArcColdString32 use 1-, 2-, and 4-byte counts when a
smaller heap header is preferable. All variants remain one word in size.
Both types keep strings up to one machine word inline. For longer strings,
ArcColdString stores [atomic reference count][variable-length length][UTF-8 bytes]
in one allocation and does not support weak references.
Packs well with other types:
use cold_string::ColdString;
use std::mem::size_of;
assert_eq!(size_of::<ColdString>(), size_of::<usize>());
// ColdString has a null-niche:
assert_eq!(size_of::<Option<ColdString>>(), size_of::<ColdString>());
ColdString is an 8-byte tagged pointer (4 bytes on 32-bit machines):
Internally, both string types use a one-word tagged pointer representation.
ColdString points to a [variable-length (length - inline capacity)][UTF-8 bytes] allocation,
while ArcColdString adds an atomic reference count before the same payload.
The 8 bytes encode one of three representations indicated by the 1st byte:
10xxxxxx: encoded contains a tagged heap pointer. To decode the address, clear the tag bits (10 → 00) and rotate so the 00 bits become the least-significant bits. The heap allocation uses 4-byte alignment, guaranteeing the least-significant 2 bits of the address are 00. On the heap, the UTF-8 characters are preceded by a variable-length encoding of length - size_of::<usize>(), since strings up to one word are stored inline. Encoded values 1–127 use one byte, 128–16,383 use two bytes, and so on. On a 64-bit target, those ranges correspond to actual string lengths 9–135 and 136–16,391.11111xxx: xxx is the length and the remaining 0-7 bytes are UTF-8 characters.xxxxxxxx: All 8 bytes are UTF-8.The exception is if encoded is usize::MAX, which represents one word of NUL bytes.
10xxxxxx and 11111xxx are chosen because they cannot be valid first bytes of UTF-8.
The heap representation stores the length on the heap, not inline in the struct. This saves memory in the struct itself but slightly increases the cost of len() since it requires a heap read. In practice, the len() cost is only marginally slower than inline storage and is typically negligible compared to memory savings, cache density improvements, and 3x faster operations on inlined strings.
ColdString uses unsafe to implement its packed representation and pointer tagging. Usage of unsafe is narrowly scoped to where layout control is required, and each instance is documented with // SAFETY: <invariant>. To further ensure soundness, ColdString is written using Rust's strict provenance API, handles unaligned access internally, maintains explicit heap alignment guarantees, and is validated with property testing and MIRI.
Memory usage per string, measured by tracking the memory requested by the allocator:
Resident set size in bytes per insertion of various collections. Insertions are strings with random length 0..=N:
| Vec | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 8.0 | 8.0 | 23.2 | 33.7 | 53.4 |
| compact_str | 24.0 | 24.0 | 24.0 | 34.6 | 60.6 |
| compact_string | 22.9 | 24.9 | 31.6 | 39.7 | 55.7 |
| smallstr | 24.0 | 24.0 | 38.0 | 50.3 | 68.4 |
| smartstring | 24.0 | 24.0 | 24.0 | 40.4 | 65.4 |
| smol_str | 24.0 | 24.0 | 24.0 | 39.9 | 71.2 |
| std | 35.8 | 37.4 | 45.8 | 54.2 | 70.5 |
| HashSet | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 18.9 | 18.9 | 34.5 | 45.5 | 64.0 |
| compact_str | 52.4 | 52.4 | 52.4 | 62.2 | 88.9 |
| compact_string | 23.2 | 30.0 | 39.6 | 49.1 | 65.9 |
| smallstr | 52.4 | 52.4 | 66.5 | 78.6 | 96.9 |
| smartstring | 52.4 | 52.4 | 52.4 | 68.2 | 94.0 |
| smol_str | 52.4 | 52.4 | 52.4 | 68.3 | 99.4 |
| std | 56.8 | 61.9 | 72.2 | 81.7 | 98.5 |
| BTreeSet | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 10.1 | 18.9 | 49.3 | 79.1 | 117.2 |
| compact_str | 24.8 | 48.4 | 61.5 | 90.5 | 145.7 |
| compact_string | 19.7 | 43.7 | 67.0 | 88.3 | 122.4 |
| smallstr | 24.8 | 48.1 | 89.7 | 121.9 | 162.0 |
| smartstring | 24.5 | 48.6 | 61.1 | 102.3 | 155.8 |
| smol_str | 25.0 | 48.3 | 61.6 | 100.7 | 166.7 |
| std | 35.8 | 70.4 | 102.9 | 128.9 | 165.5 |
| Crate | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
| cold-string | 10.0 | 9.2 | 25.3 | 30.0 | 37.2 |
| compact_str | 8.8 | 10.1 | 10.0 | 14.4 | 49.4 |
| compact_string | 34.5 | 34.8 | 37.5 | 34.9 | 38.3 |
| smallstr | 8.9 | 9.4 | 23.1 | 44.9 | 32.7 |
| smartstring | 14.8 | 15.1 | 15.0 | 26.9 | 49.5 |
| smol_str | 19.2 | 19.8 | 20.1 | 23.4 | 33.7 |
| std | 28.6 | 31.4 | 34.9 | 32.0 | 33.1 |
| Crate | 4..=4 | 8..=8 | 16..=16 | 32..=32 | 64..=64 |
|---|---|---|---|---|---|
| cold-string | 6.5 | 4.2 | 34.2 | 34.3 | 36.2 |
| compact_str | 7.5 | 7.5 | 7.6 | 31.0 | 32.4 |
| compact_string | 29.2 | 28.9 | 29.2 | 29.9 | 32.3 |
| smallstr | 4.5 | 2.6 | 28.7 | 28.5 | 29.9 |
| smartstring | 14.7 | 14.8 | 8.6 | 61.6 | 63.4 |
| smol_str | 15.2 | 12.8 | 15.7 | 41.7 | 42.0 |
| std | 28.2 | 27.6 | 28.6 | 29.3 | 30.4 |
Theoretical overhead on a 64-bit target, excluding one pointer and the UTF-8 payload. Inline values have no pointer or heap allocation, so their overhead is shown relative to the string bytes alone. Allocator rounding is not included.
| Type | 8 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 24 | 24 | 24 |
arcstr::ArcStr | 16 | 16 | 16 |
ArcColdString | 0 (inline) | 9 | 10 |
ArcColdString32 | 0 (inline) | 5 | 6 |
RSS bytes per unique string in a pre-sized Vec, measured on 64-bit Windows
with 1,000,000 strings per subprocess. Each cell is the median of three isolated
release-mode runs and includes the Vec element, heap allocation, and allocator
rounding. RSS results will vary by operating system and allocator.
| Type | 8 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 47.1 | 175.4 | 560.0 |
arcstr::ArcStr | 39.1 | 167.5 | 552.2 |
ArcColdString | 8.0 | 167.5 | 552.1 |
ArcColdString32 | 8.0 | 151.4 | 536.8 |
Criterion point estimates in nanoseconds per operation on 64-bit Windows (release mode, 100 samples). Lower is better. Allocation and clone outputs are dropped outside the timed region; drop measures a non-final reference-count decrement.
Fresh allocation and string copy:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 28.03 | 29.57 | 36.98 |
arcstr::ArcStr | 27.19 | 28.70 | 38.46 |
ArcColdString | 30.12 | 32.61 | 43.70 |
Clone/reference-count increment:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 3.86 | 3.82 | 3.82 |
arcstr::ArcStr | 3.39 | 3.41 | 3.43 |
ArcColdString | 3.44 | 3.45 | 3.43 |
String access:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 1.02 | 1.03 | 1.04 |
arcstr::ArcStr | 1.02 | 1.02 | 1.02 |
ArcColdString | 1.38 | 1.68 | 1.70 |
Non-final drop/reference-count decrement:
| Type | 16 bytes | 128 bytes | 512 bytes |
|---|---|---|---|
Arc<str> | 2.43 | 2.49 | 2.43 |
arcstr::ArcStr | 2.52 | 2.53 | 2.52 |
ArcColdString | 2.17 | 2.18 | 2.19 |
Licensed under either of
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
190 followers · starred Feb 2026
338 followers · starred Feb 2026
111 followers · starred Feb 2026
27 followers · starred Apr 2026
Rust
94.7%
Python
5.3%