A 1-word (8-byte) sized representation of immutable UTF-8 strings that in-lines up to 8 bytes.
- Strings ≤ 8 bytes require no allocation
- Compact variable-sized length encoding
- Up to 67% smaller than other short-string optimizations (see Memory Comparisons)
- Atomic reference counting support
- Niche optimized:
Option<ColdString>is the same size asColdString no_stdcompatible, MSRV 1.60- Uses strict provenance API. Miri, loom, and property tested (see Safety)
Use it like a String:
use cold_string::ColdString;
let s = ColdString::new("qwerty");
assert_eq!(s.as_str(), "qwerty");use cold_string::ColdString;
use std::mem::size_of;
assert_eq!(size_of::<ColdString>(), size_of::<usize>());
assert_eq!(size_of::<Option<ColdString>>(), size_of::<ColdString>());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 and inline up to 8 bytes.
ColdString is an 8-byte tagged NonNull pointer (4 bytes on 32-bit machines) that has one of 3 representations:
- the first byte is
10xxxxxx: The pointer encodes a heap address pointing to the length followed by the UTF-8 bytes. Rotating the10to the least significant position and setting to00decodes to the address (the heap allocation uses 4-byte alignment). The length of string is encoded with a variable length int that requires 1 bytes for 9–256 length, 2 bytes for lengths 257–511, 3 bytes for 512–65,791, etc. - otherwise, the string is inlined: the pointer represent 0-8 UTF-8 bytes, starting from the first byte. Trailing bytes are
0xFF, and the length is calculated fromtrailing_ones. - the one exception to the rule above is if the whole pointer is exactly
(usize::MAX >> 4) << 2, in which the string represents"\0\0\0\0\0\0\0\0"(an invalid value forNonNull). This number is chosen because- the first 2 bits are 0, classifying it as inlined,
- it's an impossible UTF-8 representation, so doesn't collide with other inlined strings.
- it's symmetrical, so it's the same on big or little endian.
This design minimizes per-string overhead for both short and large strings (counting everything except the UTF-8 payload bytes):
| Representation | 0–8 | 9–256 | 257–511 | 512–65,791 | 65,792–16,777,471 | usize::MAX |
|---|---|---|---|---|---|---|
ColdString |
8 bytes | 9 bytes | 10 bytes | 11 bytes | 12 bytes | 17 bytes |
String |
24 bytes | 24 bytes | 24 bytes | 24 bytes | 24 bytes | 24 bytes |
ArcColdString stores [atomic reference count][variable-length length][UTF-8 bytes] in one allocation and does not support weak references.
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.
Overall, ColdString can lead to substantial memory savings over both String and other short-string crates:
- 12% – 67% smaller
Vec<S> - 35% – 67% smaller
HashSet<S> - 30% – 75% smaller
BTreeSet<S>(vsString) - 13% – 63% smaller
BTreeSet<S>(vs other crates)
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 |
| Type | len=0..=4 | len=0..=8 | len=0..=16 | len=0..=32 | len=0..=255 | len=4..=4 | len=8..=8 | len=16..=16 | len=32..=32 | len=255..=255 |
|---|---|---|---|---|---|---|---|---|---|---|
String |
0.69 | 0.69 | 0.69 | 0.69 | 0.69 | 0.69 | 0.69 | 0.69 | 0.69 | 0.69 |
cold_string::ColdString |
1.66 | 1.61 | 1.20 | 1.00 | 0.94 | 1.62 | 1.63 | 0.77 | 0.80 | 0.83 |
compact_str::CompactString |
0.75 | 0.76 | 0.75 | 0.72 | 0.74 | 0.76 | 0.74 | 0.75 | 0.71 | 0.70 |
compact_string::CompactString |
0.73 | 0.72 | 0.72 | 0.73 | 0.76 | 0.72 | 0.72 | 0.72 | 0.75 | 0.76 |
smallstr::SmallString<[u8; 8]> |
0.67 | 0.67 | 0.85 | 0.69 | 0.73 | 0.67 | 0.68 | 0.67 | 0.66 | 0.66 |
smartstring::alias::String |
2.43 | 2.43 | 2.43 | 2.32 | 1.49 | 2.43 | 2.43 | 2.43 | 1.33 | 1.34 |
smol_str::SmolStr |
0.67 | 0.67 | 0.67 | 0.66 | 0.81 | 0.67 | 0.68 | 0.67 | 0.68 | 0.67 |
Average comparison speed with a 0%, 50%, and 100% chance of them being equal.
| Type | len=4, eq=0 | len=4, eq=0.5 | len=4, eq=1 | len=8, eq=0 | len=8, eq=0.5 | len=8, eq=1 | len=16, eq=0 | len=16, eq=0.5 | len=16, eq=1 | len=32, eq=0 | len=32, eq=0.5 | len=32, eq=1 | len=255, eq=0 | len=255, eq=0.5 | len=255, eq=1 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
String |
2.33 | 2.98 | 2.74 | 2.51 | 2.94 | 2.60 | 2.39 | 2.97 | 2.81 | 2.20 | 2.84 | 2.62 | 2.18 | 11.61 | 13.55 |
cold_string::ColdString |
1.31 | 1.31 | 1.31 | 1.31 | 1.31 | 1.31 | 1.42 | 4.79 | 5.72 | 1.44 | 5.73 | 6.68 | 1.53 | 12.25 | 15.54 |
compact_str::CompactString |
2.90 | 3.95 | 3.24 | 3.04 | 3.85 | 3.25 | 3.04 | 4.44 | 3.47 | 2.84 | 3.55 | 3.30 | 2.84 | 12.80 | 14.56 |
compact_string::CompactString |
2.45 | 3.03 | 2.83 | 2.05 | 3.17 | 3.96 | 2.06 | 4.21 | 4.53 | 2.03 | 5.02 | 5.11 | 2.11 | 12.82 | 14.62 |
smallstr::SmallString<[u8; 8]> |
2.45 | 3.25 | 3.03 | 2.81 | 3.03 | 2.81 | 3.04 | 3.87 | 3.47 | 2.83 | 4.56 | 3.26 | 2.87 | 11.99 | 13.97 |
smartstring::alias::String |
6.55 | 7.42 | 6.89 | 6.13 | 7.33 | 7.38 | 6.11 | 9.05 | 7.99 | 4.97 | 6.40 | 5.42 | 4.97 | 13.69 | 14.18 |
smol_str::SmolStr |
4.21 | 3.29 | 1.96 | 3.77 | 3.13 | 1.96 | 3.84 | 3.33 | 2.08 | 4.33 | 5.72 | 4.59 | 4.19 | 13.54 | 15.20 |
| Type | len=4 | len=8 | len=16 | len=255 |
|---|---|---|---|---|
String |
0.43 | 0.43 | 0.43 | 0.43 |
cold_string::ColdString |
0.42 | 0.43 | 0.43 | 0.43 |
compact_str::CompactString |
0.42 | 0.42 | 0.43 | 0.43 |
compact_string::CompactString |
0.43 | 0.43 | 0.43 | 0.43 |
smallstr::SmallString<[u8; 8]> |
0.43 | 0.42 | 0.42 | 0.43 |
smartstring::alias::String |
1.29 | 1.28 | 1.30 | 1.29 |
smol_str::SmolStr |
0.43 | 0.42 | 0.45 | 0.45 |
| Type | len=0..=4 | len=0..=8 | len=0..=16 | len=0..=32 | len=0..=255 | len=4..=4 | len=8..=8 | len=16..=16 | len=32..=32 | len=255..=255 |
|---|---|---|---|---|---|---|---|---|---|---|
String |
29.64 | 31.99 | 35.71 | 33.84 | 41.24 | 28.58 | 28.80 | 29.55 | 30.34 | 33.59 |
cold_string::ColdString |
10.77 | 10.22 | 25.68 | 30.44 | 43.24 | 6.25 | 5.04 | 33.79 | 33.74 | 37.48 |
compact_str::CompactString |
11.52 | 12.06 | 12.31 | 18.07 | 53.39 | 10.88 | 10.84 | 10.95 | 38.46 | 45.86 |
compact_string::CompactString |
34.95 | 36.84 | 40.28 | 38.24 | 44.93 | 29.88 | 30.26 | 31.84 | 32.20 | 35.72 |
smallstr::SmallString<[u8; 8]> |
9.52 | 10.01 | 22.50 | 29.03 | 41.24 | 4.81 | 2.38 | 29.06 | 31.15 | 33.29 |
smartstring::alias::String |
15.60 | 15.82 | 15.66 | 31.46 | 88.72 | 15.67 | 15.65 | 10.89 | 160.37 | 72.01 |
smol_str::SmolStr |
17.61 | 17.93 | 15.99 | 22.94 | 55.73 | 18.27 | 13.86 | 13.75 | 39.46 | 42.07 |
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 | 4..=4 | 8..=8 | 16..=16 | 32..=32 | 64..=64 |
|---|---|---|---|---|---|
cold_string::ArcColdString |
8.0 | 8.0 | 56.3 | 70.2 | 102.4 |
cold_string::ArcColdString32 |
8.0 | 8.0 | 38.2 | 54.5 | 86.3 |
arcstr::ArcStr |
38.3 | 38.4 | 54.4 | 70.5 | 102.5 |
Arc<str> |
46.3 | 46.4 | 62.4 | 78.5 | 110.6 |
| Type | 0..=4 | 0..=8 | 0..=16 | 0..=32 | 0..=64 |
|---|---|---|---|---|---|
cold_string::ArcColdString |
8.0 | 8.0 | 23.2 | 38.0 | 58.0 |
cold_string::ArcColdString32 |
8.0 | 8.0 | 21.7 | 35.9 | 55.5 |
arcstr::ArcStr |
32.0 | 34.8 | 44.1 | 53.0 | 69.6 |
Arc<str> |
46.4 | 46.4 | 54.0 | 62.1 | 77.9 |
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 |
Clone and drop contention across 4 threads sharing strings in a fixed size pool:
| Type | Light (1024) | Medium (16) | Heavy (1) |
|---|---|---|---|
Arc<str> |
26.55 | 54.69 | 144.81 |
arcstr::ArcStr |
29.29 | 119.39 | 214.31 |
cold_string::ArcColdString32 |
25.69 | 90.53 | 262.83 |
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
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.
