8000 [turbopack] Create a macro rcstr! for constructing RcStr from string … · vercel/next.js@f1899f3 · GitHub 8000
[go: up one dir, main page]

Skip to content

Commit f1899f3

Browse files
authored
[turbopack] Create a macro rcstr! for constructing RcStr from string literals. (#79759)
## Add `rcstr!` macro for efficient string literal handling ### What? This PR introduces a new `rcstr!` macro that creates `RcStr` instances from string literals, optimizing for inline storage when possible and using `LazyLock` for longer strings. ### Why? The `rcstr!` macro provides several benefits: - Allows compile-time evaluation of inline strings - Caches longer strings using `LazyLock` to avoid repeated allocations
1 parent 9d0081a commit f1899f3

File tree

4 files changed

+103
-7
lines changed

4 files changed

+103
-7
lines changed

turbopack/crates/turbo-rcstr/benches/mod.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main};
2-
use turbo_rcstr::RcStr;
2+
use turbo_rcstr::{RcStr, rcstr};
33

44
// map has a fast-path if the Arc is uniquely owned
55
fn bench_map(c: &mut Criterion) {
@@ -30,9 +30,26 @@ fn bench_map(c: &mut Criterion) {
3030
}
3131
}
3232

33+
/// Compare the performance of `from` and `rcstr!`
34+
fn bench_construct(c: &mut Criterion) {
35+
let mut g = c.benchmark_group("Rcstr::construct");
36+
g.bench_with_input("rcstr!/small", "small", |f, _| {
37+
f.iter(|| rcstr!("hello"));
38+
});
39+
g.bench_with_input("rcstr!/large", "large", |f, _| {
40+
f.iter(|| rcstr!("this is a long string that will take time to copy"));
41+
});
42+
43+
g.bench_with_input("from/small", "small", |f, _| {
44+
f.iter(|| RcStr::from("hello"));
45+
});
46+
g.bench_with_input("from/large", "large", |f, _| {
47+
f.iter(|| RcStr::from("this is a long string that will take time to copy"));
48+
});
49+
}
3350
criterion_group!(
3451
name = benches;
3552
config = Criterion::default();
36-
targets = bench_map,
53+
targets = bench_map,bench_construct,
3754
);
3855
criterion_main!(benches);

turbopack/crates/turbo-rcstr/src/dynamic.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
use std::ptr::NonNull;
1+
use std::{num::NonZeroU8, ptr::NonNull};
22

33
use triomphe::Arc;
44

55
use crate::{
6-
INLINE_TAG_INIT, LEN_OFFSET, RcStr, TAG_MASK,
6+
INLINE_TAG, INLINE_TAG_INIT, LEN_OFFSET, RcStr, TAG_MASK,
77
tagged_value::{MAX_INLINE_LEN, TaggedValue},
88
};
99

@@ -48,3 +48,26 @@ pub(crate) fn new_atom<T: AsRef<str> + Into<String>>(text: T) -> RcStr {
4848
unsafe_data: TaggedValue::new_ptr(ptr),
4949
}
5050
}
51+
52+
/// Attempts to construct an RcStr but only if it can be constructed inline.
53+
/// This is primarily useful in constant contexts.
54+
#[doc(hidden)]
55+
pub(crate) const fn inline_atom(text: &str) -> Option<RcStr> {
56+
let len = text.len();
57+
if len < MAX_INLINE_LEN {
58+
let tag = INLINE_TAG | ((len as u8) << LEN_OFFSET);
59+
let mut unsafe_data = TaggedValue::new_tag(NonZeroU8::new(tag).unwrap());
60+
61+
// This odd pattern is needed because we cannot create slices from ranges in constant
62+
// context.
63+
unsafe {
64+
unsafe_data
65+
.data_mut()
66+
.split_at_mut(len)
67+
.0
68+
.copy_from_slice(text.as_bytes());
69+
}
70+
return Some(RcStr { unsafe_data });
71+
}
72+
None
73+
}

turbopack/crates/turbo-rcstr/src/lib.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ impl Clone for RcStr {
250250

251251
impl Default for RcStr {
252252
fn default() -> Self {
253-
RcStr::from("")
253+
rcstr!("")
254254
}
255255
}
256256

@@ -301,6 +301,33 @@ impl Drop for RcStr {
301301
}
302302
}
303303

304+
#[doc(hidden)]
305 A3E2 +
pub const fn inline_atom(s: &str) -> Option<RcStr> {
306+
dynamic::inline_atom(s)
307+
}
308+
309+
/// Create an rcstr from a string literal.
310+
/// allocates the RcStr inline when possible otherwise uses a `LazyLock` to manage the allocation.
311+
#[macro_export]
312+
macro_rules! rcstr {
313+
($s:tt) => {{
314+
const INLINE: core::option::Option<$crate::RcStr> = $crate::inline_atom($s);
315+
// this condition should be able to be compile time evaluated and inlined.
316+
if INLINE.is_some() {
317+
INLINE.unwrap()
318+
} else {
319+
#[inline(never)]
320+
fn get_rcstr() -> $crate::RcStr {
321+
static CACHE: std::sync::LazyLock<$crate::RcStr> =
322+
std::sync::LazyLock::new(|| $crate::RcStr::from($s));
323+
324+
(*CACHE).clone()
325+
}
326+
get_rcstr()
327+
}
328+
}};
329+
}
330+
304331
/// noop
305332
impl ShrinkToFit for RcStr {
306333
#[inline(always)]
@@ -375,4 +402,33 @@ mod tests {
375402
let _ = str.clone().into_owned();
376403
assert_eq!(refcount(&str), 1);
377404
}
405+
406+
#[test]
407+
fn test_rcstr() {
408+
// Test enough to exceed the small string optimization
409+
assert_eq!(rcstr!(""), RcStr::default());
410+
assert_eq!(rcstr!(""), RcStr::from(""));
411+
assert_eq!(rcstr!("a"), RcStr::from("a"));
412+
assert_eq!(rcstr!("ab"), RcStr::from("ab"));
413+
assert_eq!(rcstr!("abc"), RcStr::from("abc"));
414+
assert_eq!(rcstr!("abcd"), RcStr::from("abcd"));
415+
assert_eq!(rcstr!("abcde"), RcStr::from("abcde"));
416+
assert_eq!(rcstr!("abcdef"), RcStr::from("abcdef"));
417+
assert_eq!(rcstr!("abcdefg"), RcStr::from("abcdefg"));
418+
assert_eq!(rcstr!("abcdefgh"), RcStr::from("abcdefgh"));
419+
assert_eq!(rcstr!("abcdefghi"), RcStr::from("abcdefghi"));
420+
}
421+
#[test]
422+
fn test_inline_atom() {
423+
// This is a silly test, just asserts that we can evaluate this in a constant context.
424+
const STR: RcStr = {
425+
let inline = inline_atom("hello");
426+
if inline.is_some() {
427+
inline.unwrap()
428+
} else {
429+
unreachable!();
430+
}
431+
};
432+
assert_eq!(STR, RcStr::from("hello"));
433+
}
378434
}

turbopack/crates/turbo-rcstr/src/tagged_value.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ impl TaggedValue {
7272
}
7373

7474
#[inline(always)]
75-
pub fn new_tag(value: NonZeroU8) -> Self {
75+
pub const fn new_tag(value: NonZeroU8) -> Self {
7676
let value = value.get() as RawTaggedValue;
7777
Self {
7878
value: unsafe { std::mem::transmute(value) },
@@ -129,7 +129,7 @@ impl TaggedValue {
129129
/// used when setting the untagged slice part of this value. If tag is
130130
/// zero and the slice is zeroed out, using this `TaggedValue` will be
131131
/// UB!
132-
pub unsafe fn data_mut(&mut self) -> &mut [u8] {
132+
pub const unsafe fn data_mut(&mut self) -> &mut [u8] {
133133
let x: *mut _ = &mut self.value;
134134
let mut data = x as *mut u8;
135135
// All except the lowest byte, which is first in little-endian, last in

0 commit comments

Comments
 (0)
0