Skip to content

Commit 27ac5ef

Browse files
Add criterion benchmarks for CowStr
1 parent 85d05f5 commit 27ac5ef

File tree

3 files changed

+303
-0
lines changed

3 files changed

+303
-0
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ stable_deref_trait = { version = "1", default-features = false }
7575
[dev-dependencies]
7676
critical-section = { version = "1.1", features = ["std"] }
7777
static_assertions = "1.1.0"
78+
criterion = "0.5"
79+
80+
[[bench]]
81+
name = "cow_str"
82+
harness = false
7883

7984
[package.metadata.docs.rs]
8085
features = [

benches/cow_str.rs

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
//! Criterion benchmarks for `CowStr` enum.
2+
//!
3+
//! This benchmark suite measures the performance characteristics of `CowStr`
4+
//! and compares it against baseline implementations that always copy strings.
5+
6+
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
7+
use heapless::{cow::CowStr, String};
8+
9+
fn create_test_string<const N: usize>(content: &str) -> String<N> {
10+
String::try_from(content).expect("String too long for capacity")
11+
}
12+
13+
/// Baseline function that always copies into a new String<N>.
14+
fn baseline_always_copy<const N: usize>(input: &String<N>) -> String<N> {
15+
input.clone()
16+
}
17+
18+
/// Function returning CowStr that only clones on mutation.
19+
fn cowstr_clone_on_mutation<'a, const N: usize>(
20+
input: &'a String<N>,
21+
needs_mutation: bool,
22+
) -> CowStr<'a, N> {
23+
if needs_mutation {
24+
let mut owned = input.clone();
25+
owned.push_str("_mutated").ok();
26+
CowStr::Owned(owned)
27+
} else {
28+
CowStr::Borrowed(input.as_view())
29+
}
30+
}
31+
32+
fn bench_baseline_vs_cowstr(c: &mut Criterion) {
33+
let mut group = c.benchmark_group("baseline_vs_cowstr");
34+
35+
let test_str: String<128> = create_test_string("test string for comparison");
36+
37+
group.bench_function("baseline_always_copy", |b| {
38+
b.iter(|| {
39+
let result = baseline_always_copy(black_box(&test_str));
40+
black_box(result)
41+
});
42+
});
43+
44+
group.bench_function("cowstr_no_mutation", |b| {
45+
b.iter(|| {
46+
let result = cowstr_clone_on_mutation(black_box(&test_str), false);
47+
black_box(result)
48+
});
49+
});
50+
51+
group.bench_function("cowstr_with_mutation", |b| {
52+
b.iter(|| {
53+
let result = cowstr_clone_on_mutation(black_box(&test_str), true);
54+
black_box(result)
55+
});
56+
});
57+
58+
group.finish();
59+
}
60+
61+
fn bench_cowstr_creation(c: &mut Criterion) {
62+
let mut group = c.benchmark_group("cowstr_creation");
63+
64+
let short_str: String<16> = create_test_string("hello");
65+
let medium_str: String<64> = create_test_string("This is a medium length test string");
66+
let long_str: String<256> = create_test_string("This is a much longer test string that contains more characters to properly test the performance characteristics of the CowStr type with different sizes");
67+
68+
group.bench_function("borrowed_short", |b| {
69+
b.iter(|| {
70+
let view = black_box(&short_str).as_view();
71+
black_box(CowStr::<16>::Borrowed(view))
72+
});
73+
});
74+
75+
group.bench_function("borrowed_medium", |b| {
76+
b.iter(|| {
77+
let view = black_box(&medium_str).as_view();
78+
black_box(CowStr::<64>::Borrowed(view))
79+
});
80+
});
81+
82+
group.bench_function("borrowed_long", |b| {
83+
b.iter(|| {
84+
let view = black_box(&long_str).as_view();
85+
black_box(CowStr::<256>::Borrowed(view))
86+
});
87+
});
88+
89+
group.bench_function("owned_short", |b| {
90+
b.iter(|| {
91+
let s = black_box(short_str.clone());
92+
black_box(CowStr::<16>::Owned(s))
93+
});
94+
});
95+
96+
group.bench_function("owned_medium", |b| {
97+
b.iter(|| {
98+
let s = black_box(medium_str.clone());
99+
black_box(CowStr::<64>::Owned(s))
100+
});
101+
});
102+
103+
group.bench_function("owned_long", |b| {
104+
b.iter(|| {
105+
let s = black_box(long_str.clone());
106+
black_box(CowStr::<256>::Owned(s))
107+
});
108+
});
109+
110+
group.finish();
111+
}
112+
113+
fn bench_cowstr_as_str(c: &mut Criterion) {
114+
let mut group = c.benchmark_group("cowstr_as_str");
115+
116+
let test_str: String<64> = create_test_string("test string for as_str");
117+
let borrowed = CowStr::<64>::Borrowed(test_str.as_view());
118+
let owned = CowStr::<64>::Owned(test_str.clone());
119+
let static_string: &'static String<64> = Box::leak(Box::new(test_str.clone()));
120+
let static_cow = CowStr::<64>::Static(static_string.as_view());
121+
122+
group.bench_function("borrowed", |b| {
123+
b.iter(|| black_box(&borrowed).as_str());
124+
});
125+
126+
group.bench_function("static", |b| {
127+
b.iter(|| black_box(&static_cow).as_str());
128+
});
129+
130+
group.bench_function("owned", |b| {
131+
b.iter(|| black_box(&owned).as_str());
132+
});
133+
134+
group.finish();
135+
}
136+
137+
fn bench_cowstr_to_owned(c: &mut Criterion) {
138+
let mut group = c.benchmark_group("cowstr_to_owned");
139+
140+
for size in [16, 64, 256].iter() {
141+
let content = match size {
142+
16 => "short",
143+
64 => "This is a medium length string for testing",
144+
256 => "This is a very long string that we use to test the performance of the to_owned method on CowStr with different string sizes and capacities to ensure proper benchmarking",
145+
_ => unreachable!(),
146+
};
147+
148+
match size {
149+
16 => {
150+
let test_str: String<16> = create_test_string(content);
151+
let borrowed = CowStr::<16>::Borrowed(test_str.as_view());
152+
let owned = CowStr::<16>::Owned(test_str.clone());
153+
154+
group.bench_with_input(BenchmarkId::new("borrowed", size), size, |b, _| {
155+
b.iter(|| black_box(&borrowed).to_owned());
156+
});
157+
158+
group.bench_with_input(BenchmarkId::new("owned", size), size, |b, _| {
159+
b.iter(|| black_box(&owned).to_owned());
160+
});
161+
}
162+
64 => {
163+
let test_str: String<64> = create_test_string(content);
164+
let borrowed = CowStr::<64>::Borrowed(test_str.as_view());
165+
let owned = CowStr::<64>::Owned(test_str.clone());
166+
167+
group.bench_with_input(BenchmarkId::new("borrowed", size), size, |b, _| {
168+
b.iter(|| black_box(&borrowed).to_owned());
169+
});
170+
171+
group.bench_with_input(BenchmarkId::new("owned", size), size, |b, _| {
172+
b.iter(|| black_box(&owned).to_owned());
173+
});
174+
}
175+
256 => {
176+
let test_str: String<256> = create_test_string(content);
177+
let borrowed = CowStr::<256>::Borrowed(test_str.as_view());
178+
let owned = CowStr::<256>::Owned(test_str.clone());
179+
180+
group.bench_with_input(BenchmarkId::new("borrowed", size), size, |b, _| {
181+
b.iter(|| black_box(&borrowed).to_owned());
182+
});
183+
184+
group.bench_with_input(BenchmarkId::new("owned", size), size, |b, _| {
185+
b.iter(|| black_box(&owned).to_owned());
186+
});
187+
}
188+
_ => unreachable!(),
189+
}
190+
}
191+
192+
group.finish();
193+
}
194+
195+
fn bench_cowstr_type_checks(c: &mut Criterion) {
196+
let mut group = c.benchmark_group("cowstr_type_checks");
197+
198+
let test_str: String<64> = create_test_string("test");
199+
let borrowed = CowStr::<64>::Borrowed(test_str.as_view());
200+
let owned = CowStr::<64>::Owned(test_str.clone());
201+
let static_string: &'static String<64> = Box::leak(Box::new(test_str.clone()));
202+
let static_cow = CowStr::<64>::Static(static_string.as_view());
203+
204+
group.bench_function("is_borrowed", |b| {
205+
b.iter(|| black_box(&borrowed).is_borrowed());
206+
});
207+
208+
group.bench_function("is_static", |b| {
209+
b.iter(|| black_box(&static_cow).is_static());
210+
});
211+
212+
group.bench_function("is_owned", |b| {
213+
b.iter(|| black_box(&owned).is_owned());
214+
});
215+
216+
group.finish();
217+
}
218+
219+
fn bench_cowstr_vs_clone(c: &mut Criterion) {
220+
let mut group = c.benchmark_group("cowstr_vs_clone");
221+
222+
let test_str: String<128> =
223+
create_test_string("This is a string that would normally be cloned in every operation");
224+
225+
group.bench_function("cowstr_borrowed_read_only", |b| {
226+
b.iter(|| {
227+
let cow = CowStr::<128>::Borrowed(black_box(&test_str).as_view());
228+
let _result = black_box(cow.as_str());
229+
});
230+
});
231+
232+
group.bench_function("string_clone_read_only", |b| {
233+
b.iter(|| {
234+
let s = black_box(&test_str).clone();
235+
let _result = black_box(s.as_str());
236+
});
237+
});
238+
239+
group.bench_function("cowstr_to_owned_when_needed", |b| {
240+
b.iter(|| {
241+
let cow = CowStr::<128>::Borrowed(black_box(&test_str).as_view());
242+
let _owned = black_box(cow.to_owned());
243+
});
244+
});
245+
246+
group.bench_function("string_clone_when_needed", |b| {
247+
b.iter(|| {
248+
let s = black_box(&test_str).clone();
249+
let _owned = black_box(s);
250+
});
251+
});
252+
253+
group.finish();
254+
}
255+
256+
fn bench_cowstr_from(c: &mut Criterion) {
257+
let mut group = c.benchmark_group("cowstr_from");
258+
259+
let test_str: String<64> = create_test_string("test string");
260+
261+
group.bench_function("from_stringview", |b| {
262+
b.iter(|| {
263+
let view = black_box(&test_str).as_view();
264+
black_box(CowStr::<64>::from(view))
265+
});
266+
});
267+
268+
group.bench_function("from_string", |b| {
269+
b.iter(|| {
270+
let s = black_box(&test_str).clone();
271+
black_box(CowStr::<64>::from(s))
272+
});
273+
});
274+
275+
group.finish();
276+
}
277+
278+
criterion_group!(
279+
benches,
280+
bench_baseline_vs_cowstr,
281+
bench_cowstr_creation,
282+
bench_cowstr_as_str,
283+
bench_cowstr_to_owned,
284+
bench_cowstr_type_checks,
285+
bench_cowstr_vs_clone,
286+
bench_cowstr_from
287+
);
288+
criterion_main!(benches);

src/cow.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
//! Provides `CowStr`, a heapless clone-on-write string that can be
44
//! borrowed, static, or owned. Useful for efficiently handling
55
//! temporary string references and owned strings.
6+
//!
7+
//! NOTE: Unlike `std::borrow::Cow<'a, str>` this type does NOT provide a
8+
//! smaller in-memory representation for the borrowed variants. The enum is
9+
//! at least as large as the owned `String<N, LenT>` because the owned
10+
//! variant carries the inline buffer. The motivation is purely to avoid
11+
//! paying an O(len) copy when a string is very often reused unchanged, while
12+
//! still allowing the API to return an owned form when a (rare) mutation or
13+
//! normalization is required. If the caller always needs an owned `String`
14+
//! anyway, `CowStr` offers no benefit and should not be used.
15+
616
use crate::len_type::LenType;
717
use crate::string::StringView;
818
use crate::String;

0 commit comments

Comments
 (0)