Skip to content

Commit 6cac969

Browse files
authored
Merge pull request #119 from mxsm/codex/pr-006-mutation-fast-paths
Enh: Reuse mutation fast paths
2 parents 6b9958a + f676562 commit 6cac969

6 files changed

Lines changed: 170 additions & 64 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ harness = false
4242
name = "layout"
4343
harness = false
4444

45+
[[bench]]
46+
name = "mutation"
47+
harness = false
48+
4549
[[bench]]
4650
name = "pattern"
4751
harness = false

benches/mutation.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use cheetah_string::CheetahString;
2+
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
3+
4+
fn bench_push_str(c: &mut Criterion) {
5+
let mut group = c.benchmark_group("push_str");
6+
7+
group.bench_function("inline_in_place", |b| {
8+
b.iter(|| {
9+
let mut s = CheetahString::from("hello");
10+
s.push_str(black_box(" world"));
11+
black_box(s)
12+
})
13+
});
14+
15+
group.bench_function("owned_spare_capacity", |b| {
16+
b.iter(|| {
17+
let mut s = CheetahString::with_capacity(128);
18+
s.push_str("hello");
19+
s.push_str(black_box(" world"));
20+
black_box(s)
21+
})
22+
});
23+
24+
group.bench_function("static_fallback", |b| {
25+
b.iter(|| {
26+
let mut s = CheetahString::from_static_str("hello");
27+
s.push_str(black_box(" world"));
28+
black_box(s)
29+
})
30+
});
31+
32+
group.finish();
33+
}
34+
35+
fn bench_add(c: &mut Criterion) {
36+
let mut group = c.benchmark_group("add");
37+
38+
for rhs_len in [1, 8, 32, 128] {
39+
let rhs = "x".repeat(rhs_len);
40+
41+
group.bench_with_input(
42+
BenchmarkId::new("owned_capacity_str", rhs_len),
43+
&rhs,
44+
|b, rhs| {
45+
b.iter(|| {
46+
let mut s = CheetahString::with_capacity(256);
47+
s.push_str("hello");
48+
black_box(s + black_box(rhs.as_str()))
49+
})
50+
},
51+
);
52+
53+
group.bench_with_input(BenchmarkId::new("inline_str", rhs_len), &rhs, |b, rhs| {
54+
b.iter(|| black_box(CheetahString::from("h") + black_box(rhs.as_str())))
55+
});
56+
}
57+
58+
group.finish();
59+
}
60+
61+
fn bench_reserve(c: &mut Criterion) {
62+
let mut group = c.benchmark_group("reserve");
63+
64+
for additional in [0, 8, 128] {
65+
group.bench_with_input(
66+
BenchmarkId::from_parameter(additional),
67+
&additional,
68+
|b, extra| {
69+
b.iter(|| {
70+
let mut s = CheetahString::with_capacity(64);
71+
s.push_str("hello");
72+
s.reserve(black_box(*extra));
73+
black_box(s)
74+
})
75+
},
76+
);
77+
}
78+
79+
group.finish();
80+
}
81+
82+
criterion_group!(benches, bench_push_str, bench_add, bench_reserve);
83+
criterion_main!(benches);

scripts/bench-all.ps1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ cargo bench --bench layout |
1212
cargo bench --bench comprehensive |
1313
Tee-Object -FilePath (Join-Path $ResultDir "comprehensive.txt")
1414

15+
cargo bench --bench mutation |
16+
Tee-Object -FilePath (Join-Path $ResultDir "mutation.txt")
17+
1518
cargo bench --bench pattern |
1619
Tee-Object -FilePath (Join-Path $ResultDir "pattern.txt")
1720

scripts/bench-all.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ mkdir -p "$RESULT_DIR"
99
cargo test layout_snapshot --all-features -- --nocapture | tee "$RESULT_DIR/layout-test.txt"
1010
cargo bench --bench layout | tee "$RESULT_DIR/layout-bench.txt"
1111
cargo bench --bench comprehensive | tee "$RESULT_DIR/comprehensive.txt"
12+
cargo bench --bench mutation | tee "$RESULT_DIR/mutation.txt"
1213
cargo bench --bench pattern | tee "$RESULT_DIR/pattern.txt"
1314
cargo bench --bench simd --features simd | tee "$RESULT_DIR/simd.txt"

src/cheetah_string.rs

Lines changed: 19 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,10 @@ impl CheetahString {
972972

973973
#[inline]
974974
fn push_str_internal(&mut self, string: &str) {
975+
if string.is_empty() {
976+
return;
977+
}
978+
975979
match &mut self.inner {
976980
InnerString::Inline { len, data } => {
977981
let total_len = *len as usize + string.len();
@@ -1039,6 +1043,10 @@ impl CheetahString {
10391043
/// ```
10401044
#[inline]
10411045
pub fn reserve(&mut self, additional: usize) {
1046+
if additional == 0 {
1047+
return;
1048+
}
1049+
10421050
match &mut self.inner {
10431051
InnerString::Inline { len, .. } if *len as usize + additional <= INLINE_CAPACITY => {
10441052
return;
@@ -1202,28 +1210,9 @@ impl Add<&str> for CheetahString {
12021210
/// assert_eq!(result, "Hello World");
12031211
/// ```
12041212
#[inline]
1205-
fn add(self, rhs: &str) -> Self::Output {
1206-
let total_len = self.len() + rhs.len();
1207-
1208-
// Fast path: result fits in inline storage
1209-
if total_len <= INLINE_CAPACITY {
1210-
let mut data = [0u8; INLINE_CAPACITY];
1211-
let self_bytes = self.as_bytes();
1212-
data[..self_bytes.len()].copy_from_slice(self_bytes);
1213-
data[self_bytes.len()..total_len].copy_from_slice(rhs.as_bytes());
1214-
return CheetahString {
1215-
inner: InnerString::Inline {
1216-
len: total_len as u8,
1217-
data,
1218-
},
1219-
};
1220-
}
1221-
1222-
// Slow path: allocate for long result
1223-
let mut result = String::with_capacity(total_len);
1224-
result.push_str(self.as_str());
1225-
result.push_str(rhs);
1226-
CheetahString::from_string(result)
1213+
fn add(mut self, rhs: &str) -> Self::Output {
1214+
self.push_str_internal(rhs);
1215+
self
12271216
}
12281217
}
12291218

@@ -1243,28 +1232,9 @@ impl Add<&CheetahString> for CheetahString {
12431232
/// assert_eq!(result, "Hello World");
12441233
/// ```
12451234
#[inline]
1246-
fn add(self, rhs: &CheetahString) -> Self::Output {
1247-
let total_len = self.len() + rhs.len();
1248-
1249-
// Fast path: result fits in inline storage
1250-
if total_len <= INLINE_CAPACITY {
1251-
let mut data = [0u8; INLINE_CAPACITY];
1252-
let self_bytes = self.as_bytes();
1253-
data[..self_bytes.len()].copy_from_slice(self_bytes);
1254-
data[self_bytes.len()..total_len].copy_from_slice(rhs.as_bytes());
1255-
return CheetahString {
1256-
inner: InnerString::Inline {
1257-
len: total_len as u8,
1258-
data,
1259-
},
1260-
};
1261-
}
1262-
1263-
// Slow path: allocate for long result
1264-
let mut result = String::with_capacity(total_len);
1265-
result.push_str(self.as_str());
1266-
result.push_str(rhs.as_str());
1267-
CheetahString::from_string(result)
1235+
fn add(mut self, rhs: &CheetahString) -> Self::Output {
1236+
self.push_str_internal(rhs.as_str());
1237+
self
12681238
}
12691239
}
12701240

@@ -1283,28 +1253,13 @@ impl Add<String> for CheetahString {
12831253
/// assert_eq!(result, "Hello World");
12841254
/// ```
12851255
#[inline]
1286-
fn add(self, rhs: String) -> Self::Output {
1287-
let total_len = self.len() + rhs.len();
1288-
1289-
// Fast path: result fits in inline storage
1290-
if total_len <= INLINE_CAPACITY {
1291-
let mut data = [0u8; INLINE_CAPACITY];
1292-
let self_bytes = self.as_bytes();
1293-
data[..self_bytes.len()].copy_from_slice(self_bytes);
1294-
data[self_bytes.len()..total_len].copy_from_slice(rhs.as_bytes());
1295-
return CheetahString {
1296-
inner: InnerString::Inline {
1297-
len: total_len as u8,
1298-
data,
1299-
},
1300-
};
1256+
fn add(mut self, rhs: String) -> Self::Output {
1257+
if self.is_empty() {
1258+
return CheetahString::from_string(rhs);
13011259
}
13021260

1303-
// Slow path: allocate for long result
1304-
let mut result = String::with_capacity(total_len);
1305-
result.push_str(self.as_str());
1306-
result.push_str(&rhs);
1307-
CheetahString::from_string(result)
1261+
self.push_str_internal(&rhs);
1262+
self
13081263
}
13091264
}
13101265

tests/mutation.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use cheetah_string::CheetahString;
2+
3+
#[test]
4+
fn inline_push_str_appends_in_place() {
5+
let mut s = CheetahString::from("hello");
6+
let before = s.as_bytes().as_ptr();
7+
8+
s.push_str(" world");
9+
10+
assert_eq!(s, "hello world");
11+
assert_eq!(s.as_bytes().as_ptr(), before);
12+
}
13+
14+
#[test]
15+
fn owned_push_str_reuses_spare_capacity() {
16+
let mut s = CheetahString::with_capacity(128);
17+
s.push_str("hello");
18+
let before = s.as_bytes().as_ptr();
19+
20+
s.push_str(" world");
21+
22+
assert_eq!(s, "hello world");
23+
assert_eq!(s.as_bytes().as_ptr(), before);
24+
}
25+
26+
#[test]
27+
fn add_reuses_owned_lhs_capacity() {
28+
let mut s = CheetahString::with_capacity(128);
29+
s.push_str("hello");
30+
let before = s.as_bytes().as_ptr();
31+
32+
let s = s + " world";
33+
34+
assert_eq!(s, "hello world");
35+
assert_eq!(s.as_bytes().as_ptr(), before);
36+
}
37+
38+
#[test]
39+
fn reserve_zero_keeps_existing_buffer() {
40+
let mut s = CheetahString::with_capacity(128);
41+
s.push_str("hello");
42+
let before = s.as_bytes().as_ptr();
43+
44+
s.reserve(0);
45+
46+
assert_eq!(s, "hello");
47+
assert_eq!(s.as_bytes().as_ptr(), before);
48+
}
49+
50+
#[test]
51+
fn add_assign_uses_push_str_path() {
52+
let mut s = CheetahString::with_capacity(128);
53+
s.push_str("hello");
54+
let before = s.as_bytes().as_ptr();
55+
56+
s += " world";
57+
58+
assert_eq!(s, "hello world");
59+
assert_eq!(s.as_bytes().as_ptr(), before);
60+
}

0 commit comments

Comments
 (0)