From a2b668b3d15830ef14c9c5024cb4383c651daec6 Mon Sep 17 00:00:00 2001 From: KushalMeghani1644 Date: Tue, 7 Oct 2025 12:39:40 +0530 Subject: [PATCH 1/2] Add baseline String benchmarks without CowStr --- Cargo.toml | 6 ++ benches/string_baseline.rs | 156 +++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 benches/string_baseline.rs diff --git a/Cargo.toml b/Cargo.toml index 7390c86bc9..cec21cafdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ stable_deref_trait = { version = "1", default-features = false } [dev-dependencies] critical-section = { version = "1.1", features = ["std"] } static_assertions = "1.1.0" +criterion = "0.5" [package.metadata.docs.rs] features = [ @@ -89,3 +90,8 @@ features = [ # for the pool module targets = ["i686-unknown-linux-gnu"] rustdoc-args = ["--cfg", "docsrs"] + +[[bench]] +name = "string_baseline" +harness = false + diff --git a/benches/string_baseline.rs b/benches/string_baseline.rs new file mode 100644 index 0000000000..afc3f56445 --- /dev/null +++ b/benches/string_baseline.rs @@ -0,0 +1,156 @@ +//! Baseline benchmarks for heapless String operations. +//! +//! This benchmark suite measures the performance characteristics of basic +//! String operations to establish a baseline for comparison. + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use heapless::String; + +fn create_test_string(content: &str) -> String { + String::try_from(content).expect("String too long for capacity") +} + +/// Baseline function that always copies into a new String. +fn baseline_always_copy(input: &String) -> String { + input.clone() +} + +/// Function that sometimes mutates the string (requires clone). +fn baseline_conditional_mutation( + input: &String, + needs_mutation: bool, +) -> String { + if needs_mutation { + let mut owned = input.clone(); + let _ = owned.push_str("_mutated"); + owned + } else { + input.clone() + } +} + +fn bench_string_baseline(c: &mut Criterion) { + let mut group = c.benchmark_group("string_baseline"); + + let test_str: String<128> = create_test_string("test string for comparison"); + + group.bench_function("always_copy", |b| { + b.iter(|| { + let result = baseline_always_copy(black_box(&test_str)); + black_box(result) + }); + }); + + group.bench_function("no_mutation", |b| { + b.iter(|| { + let result = baseline_conditional_mutation(black_box(&test_str), false); + black_box(result) + }); + }); + + group.bench_function("with_mutation", |b| { + b.iter(|| { + let result = baseline_conditional_mutation(black_box(&test_str), true); + black_box(result) + }); + }); + + group.finish(); +} + +fn bench_string_creation(c: &mut Criterion) { + let mut group = c.benchmark_group("string_creation"); + + let short_str: String<16> = create_test_string("hello"); + let medium_str: String<64> = create_test_string("This is a medium length test string"); + 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"); + + group.bench_function("clone_short", |b| { + b.iter(|| black_box(short_str.clone())); + }); + + group.bench_function("clone_medium", |b| { + b.iter(|| black_box(medium_str.clone())); + }); + + group.bench_function("clone_long", |b| { + b.iter(|| black_box(long_str.clone())); + }); + + group.finish(); +} + +fn bench_string_as_str(c: &mut Criterion) { + let mut group = c.benchmark_group("string_as_str"); + + let test_str: String<64> = create_test_string("test string for as_str"); + + group.bench_function("as_str", |b| { + b.iter(|| black_box(&test_str).as_str()); + }); + + group.finish(); +} + +fn bench_string_clone_for_size( + group: &mut criterion::BenchmarkGroup, + content: &str, + size: usize, +) { + let test_str: String = create_test_string(content); + + group.bench_with_input(BenchmarkId::new("clone", size), &size, |b, _| { + b.iter(|| black_box(&test_str).clone()); + }); +} + +fn bench_string_clone_sizes(c: &mut Criterion) { + let mut group = c.benchmark_group("string_clone_sizes"); + + bench_string_clone_for_size::<16>(&mut group, "short", 16); + bench_string_clone_for_size::<64>( + &mut group, + "This is a medium length string for testing", + 64, + ); + bench_string_clone_for_size::<256>( + &mut group, + "This is a very long string that we use to test the performance with different string sizes and capacities to ensure proper benchmarking", + 256, + ); + + group.finish(); +} + +fn bench_string_vs_clone(c: &mut Criterion) { + let mut group = c.benchmark_group("string_vs_clone"); + + let test_str: String<128> = + create_test_string("This is a string that would normally be cloned in every operation"); + + group.bench_function("clone_read_only", |b| { + b.iter(|| { + let s = black_box(&test_str).clone(); + let _result = black_box(s.as_str()); + }); + }); + + group.bench_function("clone_when_needed", |b| { + b.iter(|| { + let s = black_box(&test_str).clone(); + let _owned = black_box(s); + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_string_baseline, + bench_string_creation, + bench_string_as_str, + bench_string_clone_sizes, + bench_string_vs_clone +); +criterion_main!(benches); From 34a5046f6daaa43a031eb6affb973009881ff314 Mon Sep 17 00:00:00 2001 From: KushalMeghani1644 Date: Tue, 7 Oct 2025 12:48:38 +0530 Subject: [PATCH 2/2] Add CowStr type and modify benchmarks to use CowStr for comparison --- Cargo.toml | 5 + benches/cow_str.rs | 250 +++++++++++++++++++++++++++++++++++++ benches/string_baseline.rs | 12 +- src/cow.rs | 106 ++++++++++++++++ src/lib.rs | 1 + 5 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 benches/cow_str.rs create mode 100644 src/cow.rs diff --git a/Cargo.toml b/Cargo.toml index cec21cafdb..ca105b5e2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,11 @@ features = [ targets = ["i686-unknown-linux-gnu"] rustdoc-args = ["--cfg", "docsrs"] +[[bench]] +name = "cow_str" +harness = false + + [[bench]] name = "string_baseline" harness = false diff --git a/benches/cow_str.rs b/benches/cow_str.rs new file mode 100644 index 0000000000..85f3c19847 --- /dev/null +++ b/benches/cow_str.rs @@ -0,0 +1,250 @@ +//! Criterion benchmarks for `CowStr` enum. + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use heapless::{cow::CowStr, String}; + +fn create_test_string(content: &str) -> String { + String::try_from(content).expect("String too long for capacity") +} + +fn cowstr_clone_on_mutation<'a, const N: usize>( + input: &'a String, + needs_mutation: bool, +) -> CowStr<'a, N> { + if needs_mutation { + let mut owned = input.clone(); + let _ = owned.push_str("_mutated"); + CowStr::Owned(owned) + } else { + CowStr::Borrowed(input.as_view()) + } +} + +fn bench_baseline_vs_cowstr(c: &mut Criterion) { + let mut group = c.benchmark_group("baseline_vs_cowstr"); + + let test_str: String<128> = create_test_string("test string for comparison"); + + group.bench_function("cowstr_no_mutation", |b| { + b.iter(|| { + let result = cowstr_clone_on_mutation(black_box(&test_str), false); + black_box(result) + }); + }); + + group.bench_function("cowstr_with_mutation", |b| { + b.iter(|| { + let result = cowstr_clone_on_mutation(black_box(&test_str), true); + black_box(result) + }); + }); + + group.finish(); +} + +fn bench_cowstr_creation(c: &mut Criterion) { + let mut group = c.benchmark_group("cowstr_creation"); + + let short_str: String<16> = create_test_string("hello"); + let medium_str: String<64> = create_test_string("This is a medium length test string"); + 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"); + + group.bench_function("borrowed_short", |b| { + b.iter(|| { + let view = black_box(&short_str).as_view(); + black_box(CowStr::<16>::Borrowed(view)) + }); + }); + + group.bench_function("borrowed_medium", |b| { + b.iter(|| { + let view = black_box(&medium_str).as_view(); + black_box(CowStr::<64>::Borrowed(view)) + }); + }); + + group.bench_function("borrowed_long", |b| { + b.iter(|| { + let view = black_box(&long_str).as_view(); + black_box(CowStr::<256>::Borrowed(view)) + }); + }); + + group.bench_function("owned_short", |b| { + b.iter(|| { + let s = black_box(short_str.clone()); + black_box(CowStr::<16>::Owned(s)) + }); + }); + + group.bench_function("owned_medium", |b| { + b.iter(|| { + let s = black_box(medium_str.clone()); + black_box(CowStr::<64>::Owned(s)) + }); + }); + + group.bench_function("owned_long", |b| { + b.iter(|| { + let s = black_box(long_str.clone()); + black_box(CowStr::<256>::Owned(s)) + }); + }); + + group.finish(); +} + +fn bench_cowstr_as_str(c: &mut Criterion) { + let mut group = c.benchmark_group("cowstr_as_str"); + + let test_str: String<64> = create_test_string("test string for as_str"); + let borrowed = CowStr::<64>::Borrowed(test_str.as_view()); + let owned = CowStr::<64>::Owned(test_str.clone()); + let static_string: &'static String<64> = Box::leak(Box::new(test_str.clone())); + let static_cow = CowStr::<64>::Static(static_string.as_view()); + + group.bench_function("borrowed", |b| { + b.iter(|| black_box(&borrowed).as_str()); + }); + + group.bench_function("static", |b| { + b.iter(|| black_box(&static_cow).as_str()); + }); + + group.bench_function("owned", |b| { + b.iter(|| black_box(&owned).as_str()); + }); + + group.finish(); +} + +fn bench_cowstr_to_owned_for_size( + group: &mut criterion::BenchmarkGroup, + content: &str, + size: usize, +) { + let test_str: String = create_test_string(content); + let borrowed = CowStr::::Borrowed(test_str.as_view()); + let owned = CowStr::::Owned(test_str.clone()); + + group.bench_with_input(BenchmarkId::new("borrowed", size), &size, |b, _| { + b.iter(|| black_box(&borrowed).to_owned()); + }); + + group.bench_with_input(BenchmarkId::new("owned", size), &size, |b, _| { + b.iter(|| black_box(&owned).to_owned()); + }); +} + +fn bench_cowstr_to_owned(c: &mut Criterion) { + let mut group = c.benchmark_group("cowstr_to_owned"); + + bench_cowstr_to_owned_for_size::<16>(&mut group, "short", 16); + bench_cowstr_to_owned_for_size::<64>( + &mut group, + "This is a medium length string for testing", + 64, + ); + bench_cowstr_to_owned_for_size::<256>( + &mut group, + "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", + 256, + ); + + group.finish(); +} + +fn bench_cowstr_type_checks(c: &mut Criterion) { + let mut group = c.benchmark_group("cowstr_type_checks"); + + let test_str: String<64> = create_test_string("test"); + let borrowed = CowStr::<64>::Borrowed(test_str.as_view()); + let owned = CowStr::<64>::Owned(test_str.clone()); + let static_string: &'static String<64> = Box::leak(Box::new(test_str.clone())); + let static_cow = CowStr::<64>::Static(static_string.as_view()); + + group.bench_function("is_borrowed", |b| { + b.iter(|| black_box(&borrowed).is_borrowed()); + }); + + group.bench_function("is_static", |b| { + b.iter(|| black_box(&static_cow).is_static()); + }); + + group.bench_function("is_owned", |b| { + b.iter(|| black_box(&owned).is_owned()); + }); + + group.finish(); +} + +fn bench_cowstr_vs_clone(c: &mut Criterion) { + let mut group = c.benchmark_group("cowstr_vs_clone"); + + let test_str: String<128> = + create_test_string("This is a string that would normally be cloned in every operation"); + + group.bench_function("cowstr_borrowed_read_only", |b| { + b.iter(|| { + let cow = CowStr::<128>::Borrowed(black_box(&test_str).as_view()); + let _result = black_box(cow.as_str()); + }); + }); + + group.bench_function("string_clone_read_only", |b| { + b.iter(|| { + let s = black_box(&test_str).clone(); + let _result = black_box(s.as_str()); + }); + }); + + group.bench_function("cowstr_to_owned_when_needed", |b| { + b.iter(|| { + let cow = CowStr::<128>::Borrowed(black_box(&test_str).as_view()); + let _owned = black_box(cow.to_owned()); + }); + }); + + group.bench_function("string_clone_when_needed", |b| { + b.iter(|| { + let s = black_box(&test_str).clone(); + let _owned = black_box(s); + }); + }); + + group.finish(); +} + +fn bench_cowstr_from(c: &mut Criterion) { + let mut group = c.benchmark_group("cowstr_from"); + + let test_str: String<64> = create_test_string("test string"); + + group.bench_function("from_stringview", |b| { + b.iter(|| { + let view = black_box(&test_str).as_view(); + black_box(CowStr::<64>::from(view)) + }); + }); + + group.bench_function("from_string", |b| { + b.iter(|| { + let s = black_box(&test_str).clone(); + black_box(CowStr::<64>::from(s)) + }); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_baseline_vs_cowstr, + bench_cowstr_creation, + bench_cowstr_as_str, + bench_cowstr_to_owned, + bench_cowstr_type_checks, + bench_cowstr_vs_clone, + bench_cowstr_from +); +criterion_main!(benches); diff --git a/benches/string_baseline.rs b/benches/string_baseline.rs index afc3f56445..1b6010a53a 100644 --- a/benches/string_baseline.rs +++ b/benches/string_baseline.rs @@ -4,28 +4,28 @@ //! String operations to establish a baseline for comparison. use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; -use heapless::String; +use heapless::{cow::CowStr, String}; fn create_test_string(content: &str) -> String { String::try_from(content).expect("String too long for capacity") } /// Baseline function that always copies into a new String. -fn baseline_always_copy(input: &String) -> String { - input.clone() +fn baseline_always_copy(input: &String) -> CowStr<'_, N> { + CowStr::Owned(input.clone()) } /// Function that sometimes mutates the string (requires clone). fn baseline_conditional_mutation( input: &String, needs_mutation: bool, -) -> String { +) -> CowStr<'_, N> { if needs_mutation { let mut owned = input.clone(); let _ = owned.push_str("_mutated"); - owned + CowStr::Owned(owned) } else { - input.clone() + CowStr::Borrowed(input.as_view()) } } diff --git a/src/cow.rs b/src/cow.rs new file mode 100644 index 0000000000..7113430136 --- /dev/null +++ b/src/cow.rs @@ -0,0 +1,106 @@ +//! Clone-on-write string type for heapless. +//! +//! Provides `CowStr`, a heapless clone-on-write string that can be borrowed or owned. + +use crate::len_type::LenType; +use crate::string::StringView; +use crate::String; +use core::borrow::Borrow; + +/// A clone-on-write (COW) string type specialized for heapless strings. +/// +/// `CowStr` can be either: +/// - `Borrowed(&'a StringView)` for a non-`'static` borrowed view, +/// - `Static(&'static StringView)` for a `'static` borrowed view, +/// - `Owned(String)` for an owned heapless `String`. +#[derive(Debug, Clone)] +pub enum CowStr<'a, const N: usize, LenT: LenType = usize> +where + LenT: 'static, +{ + /// A borrowed view with lifetime `'a`. + Borrowed(&'a StringView), + /// A `'static` borrowed view. + Static(&'static StringView), + /// An owned `String` with inline storage of size `N`. + Owned(String), +} + +impl<'a, const N: usize, LenT: LenType> CowStr<'a, N, LenT> +where + LenT: 'static, +{ + /// Convert the `CowStr` into an owned `String`. + /// + /// Panics if the string does not fit in `N`. + pub fn to_owned(&self) -> String { + match self { + CowStr::Borrowed(sv) => String::try_from(sv.as_str()) + .expect("capacity too small for CowStr::to_owned"), + CowStr::Static(sv) => String::try_from(sv.as_str()) + .expect("capacity too small for CowStr::to_owned"), + CowStr::Owned(s) => s.clone(), + } + } + + /// Return the inner value as `&str`. + pub fn as_str(&self) -> &str { + match self { + CowStr::Borrowed(sv) => sv.as_str(), + CowStr::Static(sv) => sv.as_str(), + CowStr::Owned(s) => s.as_str(), + } + } + + /// Is this a non-`'static` borrowed view? + pub fn is_borrowed(&self) -> bool { + matches!(self, CowStr::Borrowed(_)) + } + + /// Is this a `'static` borrowed view? + pub fn is_static(&self) -> bool { + matches!(self, CowStr::Static(_)) + } + + /// Is this an owned string? + pub fn is_owned(&self) -> bool { + matches!(self, CowStr::Owned(_)) + } +} + +impl<'a, const N: usize, LenT: LenType> From<&'a StringView> for CowStr<'a, N, LenT> +where + LenT: 'static, +{ + fn from(sv: &'a StringView) -> Self { + CowStr::Borrowed(sv) + } +} + +impl From> for CowStr<'_, N, LenT> +where + LenT: 'static, +{ + fn from(s: String) -> Self { + CowStr::Owned(s) + } +} + +impl CowStr<'static, N, LenT> +where + LenT: 'static, +{ + /// Construct a `CowStr` that holds a `'static` `StringView`. + pub const fn from_static(sv: &'static StringView) -> Self { + CowStr::Static(sv) + } +} + +impl<'a, const N: usize, LenT: LenType> Borrow for CowStr<'a, N, LenT> +where + LenT: 'static, +{ + fn borrow(&self) -> &str { + self.as_str() + } +} diff --git a/src/lib.rs b/src/lib.rs index fd506a92b7..ffd93872b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,6 +175,7 @@ pub mod linear_map; mod slice; pub mod storage; pub mod string; +pub mod cow; pub mod vec; // FIXME: Workaround a compiler ICE in rust 1.83 to 1.86