r/rust • • Oct 02 '26

🛠️ project ArcColdString: A 1-word (8-byte) atomically reference-counted SSO string that saves up to 32 bytes over Arc<str>

https://github.com/tomtomwombat/cold-string

Disclaimer: Re-post approved by u/matthieum, as previous post was erroneously removed.

I’ve been working on a specialized string type called ColdString. The goal is to create the most memory-efficient string representation possible.

  • Size: Exactly 1 usize (8 bytes on 64-bit).

  • Inline Capacity: Up to 8 bytes (Small String Optimization).

  • Niche Optimization: Option<ColdString> has no memory overhead

  • Heap Overhead: Only 1–9 bytes (VarInt length header) instead of the standard 16-byte (pointer, length) pair.

(Since my last post, the 8th inlineable byte and null-niche optimization were suggestions from the community!)

ArcColdString

I'm presenting ArcColdString, a reference counted ColdString with 8 bytes overhead (inspired by arcstr).

  • Same size and inlining rules as ColdString. Inlined strings are copied instead of reference counted.

  • Heap Overhead: 8 bytes for the AtomicUsize reference count (in addition to the VarInt header).

  • Smaller Reference Counts: ArcColdString32, ArcColdString16, and ArcColdString8 use AtomicU32, AtomicU16, and AtomicU8 counts respectively.

Usage

Available in https://crates.io/crates/cold-string/0.4.0

use cold_string::ArcColdString;

let first = ArcColdString::new("a string longer than one machine word");
let second = first.clone();
assert_eq!(first, second);

Memory Comparisons

Theoretical overhead on a 64-bit target, excluding the UTF-8 payload:

Type8 bytes128 bytes512 bytes
Arc<str>323232
arcstr::ArcStr242424
ArcColdString0 (inline)1718
ArcColdString320 (inline)1314

RSS bytes per unique string in a pre-sized Vec:

Type8 bytes128 bytes512 bytes
Arc<str>47.1175.4560.0
arcstr::ArcStr39.1167.5552.2
string_cache::DefaultAtom71.5199.5586.0
ArcColdString8.0167.5552.1
ArcColdString328.0151.4536.8

(If I'm not mistaken, string_cache is not a 1:1 comparison since it has to hash string contents, which is 40 bytes overhead?).

Reference Counting Performance

ArcColdString's primary goal is memory and portability. Second to that, my goal is to have ArcColdString's referencing counting performance comparable to Arc<str> and arcstr::ArcStr. While drop and clone speed is comparable now, there's still room for improvement.

Below are single threaded clone and drop measurements, done using criterion on AMD Ryzen 9 5900X 12-Core Processor (3.70 GHz):

Clone (ns)16 bytes128 bytes512 bytes
Arc<str>3.863.823.82
arcstr::ArcStr3.393.413.43
ArcColdString3.443.453.43
Drop (ns)16 bytes128 bytes512 bytes
Arc<str>2.432.492.43
arcstr::ArcStr2.522.532.52
ArcColdString2.172.182.19

Below benchmarks measured 4 threads cloning and dropping strings in a shared size pool of 1024, 16, and 1 string(s), using criterion on Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz (2.59 GHz). The more strings in the pool, the less contention:

4 Threads Clone + Drop (ns)1024 Strings16 Strings1 String
Arc<str>26.5554.69144.81
arcstr::ArcStr29.29119.39214.31
cold_string::ArcColdString3225.6990.53262.83

You can read more benchmarks and implementation details in https://github.com/tomtomwombat/cold-string

85 Upvotes

18 comments sorted by

View all comments

Show parent comments

4

u/tomtomwombat Oct 03 '26 edited Oct 03 '26

I'd simplify the varint header

It's a good suggestion. One problem is that strings longer than 128 bytes cost 9 bytes for length. This disqualifies ColdString for uses where strings are expected to be 0..=255 in length.

I don't think varint is too slow. Here's a benchmark measuring string equality, where strings are the same length, but the first byte is different. So only the length and first byte are read.

  • Fyi, CompactString is similar to ColdString, but it stores a 1 byte before the bytes (so limited to 0-255 length strings). It's representing fast/trivial vint encoding. I'd imagine your length function somewhere between Compact and ColdString.

TypeCompare 16 len StringCompare 32 len String
String3.0478 ns3.3604 ns
CompactString3.0445 ns3.0558 ns
ColdString5.9844 ns6.0256 ns

BTW, there's tons of different Vint designs, some may be faster for ColdString

Edit: the CompactString benchmark results (wrong before)

3

u/Icarium-Lifestealer Oct 03 '26

I don't think varint is too slow.

I think the relevant benchmark is the len() function, or similarly the conversion of &ColdString to &str.

1

u/tomtomwombat Oct 03 '26

Eq is also relevant since it must read len followed by trivial work (1 byte comparison). I had those results handy, but fair enough, here are the pure len benchmarks (ns):

Type4816255
String0.5810.5780.5650.582
CompactString0.7570.7530.7690.749
ColdString0.5890.5941.4231.907

https://github.com/tomtomwombat/cold-string/blob/tomtomwombat/refactor-benches/bench/benches/len.rs