Skip to content

Commit 3e6d11d

Browse files
committed
Inline 8 bytes: #1
1 parent 09ace1d commit 3e6d11d

4 files changed

Lines changed: 58 additions & 25 deletions

File tree

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,18 @@ ColdString is an 8 byte array (4 bytes on 32-bit machines):
3434
```rust,ignore
3535
pub struct ColdString([u8; 8]);
3636
```
37-
The array acts as either a pointer to heap data for strings longer than 7 bytes or is the inlined data itself.
38-
## Inline Mode
39-
`self.0[1]` to `self.0[7]` store the bytes of string. In the least significant byte, `self.0[0]`, the least significant bit signifies the inline/heap flag, and is set to "1" for inline mode. The next bits encode the length (always between 0 and 7).
40-
```text,ignore
41-
b0 b1 b2 b3 b4 b5 b6 b7
42-
b0 = <7 bit len> | 1
43-
```
44-
For example, `"qwerty" = [13, 'q', 'w', 'e', 'r', 't', 'y', 0]`, where 13 is `"qwerty".len() << 1 | 1`.
37+
The array acts as either a pointer to heap data for strings longer than 8 bytes or is the inlined data itself. The first byte indicates one of 3 encodings:
38+
39+
## Inline Mode (0 to 7 Bytes)
40+
The first byte has bits 11111xxx, where xxx is the length. `self.0[1]` to `self.0[7]` store the bytes of string.
41+
42+
## Inline Mode (8 Bytes)
43+
`self.0` stores the bytes of string. Since the string is UTF-8, the first byte is guaranteed to not be 10xxxxx or 11111xxx.
4544

4645
## Heap Mode
47-
The bytes act as a pointer to heap. The data on the heap has alignment 2, causing the least significant bit to always be 0 (since alignment 2 implies `addr % 2 == 0`), signifying heap mode. On the heap, the data starts with a variable length integer encoding of the length, followed by the bytes.
46+
`self.0` are an encoded pointer to heap, where first byte is 10xxxxxx. 10xxxxxx is chosen because it's a UTF-8 continuation byte and therefore an impossible first byte for inline mode. Since a heap-alignment of 4 is chosen, the pointer's least significant 2 bits are guaranteed to be 0 ([See more](https://doc.rust-lang.org/beta/std/alloc/struct.Layout.html#method.from_size_align)). These bits are swapped with the 10 "tag" bits when de/coding between `self.0` and the address value.
47+
48+
On the heap, the data starts with a variable length integer encoding of the length, followed by the bytes.
4849
```text,ignore
4950
ptr --> <var int length> <data>
5051
```
@@ -61,7 +62,7 @@ ptr --> <var int length> <data>
6162
| `smol_str` | 24.0 B | 24.0 B | 24.0 B | 41.1 B | 72.2 B |
6263
| `compact_str` | 24.0 B | 24.0 B | 24.0 B | 35.4 B | 61.0 B |
6364
| `compact_string` | 24.1 B | 25.8 B | 32.6 B | 40.5 B | 56.5 B |
64-
| **`cold-string`** | **8.0 B** | **11.2 B** | **24.9 B** | **36.5 B** | **53.5 B** |
65+
| **`cold-string`** | **8.0 B** | **8.0 B** | **23.2 B** | **35.7 B** | **53.0 B** |
6566

6667
**Note:** Columns represent string length (bytes/chars). Values represent average Resident Set Size (RSS) in bytes per string instance. Measurements taken with 10M iterations.
6768

src/lib.rs

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use core::{
2121
mod vint;
2222
use crate::vint::VarInt;
2323

24-
const HEAP_ALIGN: usize = 2;
24+
const HEAP_ALIGN: usize = 4;
2525
const WIDTH: usize = mem::size_of::<usize>();
2626

2727
/// Compact representation of immutable UTF-8 strings. Optimized for memory usage and struct packing.
@@ -100,27 +100,30 @@ impl ColdString {
100100
/// If the string is short enough, then it will be inlined on the stack.
101101
pub fn new<T: AsRef<str>>(x: T) -> Self {
102102
let s = x.as_ref();
103-
if s.len() < WIDTH {
103+
if s.len() <= WIDTH {
104104
Self::new_inline(s)
105105
} else {
106106
Self::new_heap(s)
107107
}
108108
}
109109

110+
/// Returns `true` if the string bytes are inlined.
110111
#[inline]
111-
const fn is_inline(&self) -> bool {
112-
self.0[0] & 1 == 1
112+
pub const fn is_inline(&self) -> bool {
113+
(self.0[0] & 0b11000000) != 0b10000000
113114
}
114115

115116
#[inline]
116117
const fn new_inline(s: &str) -> Self {
117-
debug_assert!(s.len() < WIDTH);
118+
debug_assert!(s.len() <= WIDTH);
118119
let mut buf = [0u8; WIDTH];
119120
unsafe {
120-
let dest_ptr = buf.as_mut_ptr().add(1);
121+
let dest_ptr = buf.as_mut_ptr().add((s.len() < WIDTH) as usize);
121122
ptr::copy_nonoverlapping(s.as_ptr(), dest_ptr, s.len());
122123
}
123-
buf[0] = ((s.len() as u8) << 1) | 1;
124+
if s.len() < WIDTH {
125+
buf[0] = 0b11111000 | (s.len() as u8);
126+
}
124127
Self(buf)
125128
}
126129

@@ -144,22 +147,28 @@ impl ColdString {
144147

145148
let addr = ptr.expose_provenance();
146149
debug_assert!(addr % 2 == 0);
150+
let mut addr = addr.rotate_left(6);
151+
addr |= 0b10000000;
147152
Self(addr.to_le_bytes())
148153
}
149154
}
150155

151156
#[inline]
152157
fn heap_ptr(&self) -> *mut u8 {
153-
// Can be const in 1.91
154158
debug_assert!(!self.is_inline());
155-
let addr = usize::from_le_bytes(self.0);
159+
let mut addr = usize::from_le_bytes(self.0);
160+
addr ^= 0b10000000;
161+
let addr = addr.rotate_right(6);
156162
debug_assert!(addr % 2 == 0);
157-
with_exposed_provenance_mut::<u8>(addr)
163+
with_exposed_provenance_mut::<u8>(addr) // const in 1.91
158164
}
159165

160166
#[inline]
161167
const fn inline_len(&self) -> usize {
162-
self.0[0] as usize >> 1
168+
match self.0[0] & 0b11111000 {
169+
0b11111000 => (self.0[0] & 0b00000111) as usize,
170+
_ => 8,
171+
}
163172
}
164173

165174
/// Returns the length of this `ColdString`, in bytes, not [`char`]s or
@@ -195,7 +204,7 @@ impl ColdString {
195204
#[inline]
196205
unsafe fn decode_inline(&self) -> &[u8] {
197206
let len = self.inline_len();
198-
let ptr = self.0.as_ptr().add(1);
207+
let ptr = self.0.as_ptr().add((len < WIDTH) as usize);
199208
slice::from_raw_parts(ptr, len)
200209
}
201210

@@ -433,11 +442,11 @@ mod tests {
433442

434443
#[test]
435444
fn it_works() {
436-
for s in ["test", "", "1234567", "longer test"] {
445+
for s in ["test", "", "1234567", "12345678", "longer test"] {
437446
let cs = ColdString::new(s);
438-
assert_eq!(cs.as_str(), s);
447+
assert_eq!(s.len() <= 8, cs.is_inline());
439448
assert_eq!(cs.len(), s.len());
440-
assert_eq!(cs.len() < 8, cs.is_inline());
449+
assert_eq!(cs.as_str(), s);
441450
assert_eq!(cs.clone(), cs);
442451
#[cfg(feature = "std")]
443452
{
@@ -451,4 +460,18 @@ mod tests {
451460
assert_eq!(*s, cs);
452461
}
453462
}
463+
464+
#[test]
465+
fn test_regression() {
466+
for s in [
467+
str::from_utf8(&[103, 39, 240, 145, 167, 156, 194, 165]).unwrap(),
468+
"AaAa0 ® ",
469+
str::from_utf8(&[240, 158, 186, 128, 240, 145, 143, 151]).unwrap(),
470+
] {
471+
let cs = ColdString::new(s);
472+
assert_eq!(s.len() <= 8, cs.is_inline());
473+
assert_eq!(s.len(), cs.len());
474+
assert_eq!(cs.as_str(), s);
475+
}
476+
}
454477
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc cdedf38c4537d00fabcf8d676be43279a12ad39216d111c1e40f877bf44a8e01 # shrinks to s = "AaAa0 ® "
8+
cc 9f00d39889fa4d629320694a19a244655e9ab8dbe399ad7a9dbb9ce34b7f3d2c # shrinks to s = "𞺀𑏗"

tests/property.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ proptest! {
3232
#[test]
3333
fn arb_string(s in any::<String>()) {
3434
let cold = ColdString::new(s.as_str());
35+
assert_eq!(s.len() <= 8, cold.is_inline());
3536
assert_eq!(cold.len(), s.len());
3637
assert_eq!(cold.as_str(), s.as_str());
3738
assert_eq!(cold, ColdString::from(s.as_str()));

0 commit comments

Comments
 (0)