Skip to content

Commit 303fcaa

Browse files
committed
Precompute static hashes, use static allocation instead of dynamic optimize clones out of the static cache
1 parent de3998f commit 303fcaa

File tree

2 files changed

+64
-36
lines changed

2 files changed

+64
-36
lines changed

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

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
tagged_value::{MAX_INLINE_LEN, TaggedValue},
88
};
99

10-
pub(crate) struct PrehashedString {
10+
pub struct PrehashedString {
1111
pub value: String,
1212
/// This is not the actual `fxhash`, but rather it's a value that passed to
1313
/// `write_u64` of [rustc_hash::FxHasher].
@@ -61,23 +61,9 @@ pub(crate) fn new_atom<T: AsRef<str> + Into<String>>(text: T) -> RcStr {
6161
}
6262
}
6363

64-
pub(crate) fn new_static_atom(text: &'static str) -> RcStr {
65-
debug_assert!(
66-
text.len() >= MAX_INLINE_LEN,
67-
"should have use the rcstr! macro? this string is too short"
68-
);
69-
let hash = hash_bytes(text.as_bytes());
70-
71-
let entry: Box<PrehashedString> = Box::new(PrehashedString {
72-
value: text.into(),
73-
hash,
74-
});
75-
// Hello memory leak!
76-
// We are leaking here because the caller is asserting that this RcStr will have a static
77-
// lifetime Ideally we wouldn't be copying the static str into the PrehashedString as a
78-
// String but instead keeping the `&'static str` reference somehow, perhaps using a Cow?? or
79-
// perhaps switching to a ThinArc
80-
let mut entry = Box::into_raw(entry);
64+
#[inline(always)]
65+
pub(crate) fn new_static_atom(string: &'static PrehashedString) -> RcStr {
66+
let mut entry = string as *const PrehashedString;
8167
debug_assert!(0 == entry as u8 & TAG_MASK);
8268
// Tag it as a static pointer
8369
entry = ((entry as usize) | STATIC_TAG as usize) as *mut PrehashedString;
@@ -120,7 +106,7 @@ const SEED2: u64 = 0x13198a2e03707344;
120106
const PREVENT_TRIVIAL_ZERO_COLLAPSE: u64 = 0xa4093822299f31d0;
121107

122108
#[inline]
123-
fn multiply_mix(x: u64, y: u64) -> u64 {
109+
const fn multiply_mix(x: u64, y: u64) -> u64 {
124110
#[cfg(target_pointer_width = "64")]
125111
{
126112
// We compute the full u64 x u64 -> u128 product, this is a single mul
@@ -161,6 +147,26 @@ fn multiply_mix(x: u64, y: u64) -> u64 {
161147
}
162148
}
163149

150+
// Const compatible helper function to read a u64 from a byte array at a given offset
151+
const fn read_u64_le(bytes: &[u8], offset: usize) -> u64 {
152+
(bytes[offset] as u64)
153+
| ((bytes[offset + 1] as u64) << 8)
154+
| ((bytes[offset + 2] as u64) << 16)
155+
| ((bytes[offset + 3] as u64) << 24)
156+
| ((bytes[offset + 4] as u64) << 32)
157+
| ((bytes[offset + 5] as u64) << 40)
158+
| ((bytes[offset + 6] as u64) << 48)
159+
| ((bytes[offset + 7] as u64) << 56)
160+
}
161+
162+
// Const compatible helper function to read a u32 from a byte array at a given offset
163+
const fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
164+
(bytes[offset] as u32)
165+
| ((bytes[offset + 1] as u32) << 8)
166+
| ((bytes[offset + 2] as u32) << 16)
167+
| ((bytes[offset + 3] as u32) << 24)
168+
}
169+
164170
/// Copied from `hash_bytes` of `rustc-hash`.
165171
///
166172
/// See: https://github.com/rust-lang/rustc-hash/blob/dc5c33f1283de2da64d8d7a06401d91aded03ad4/src/lib.rs#L252-L297
@@ -179,19 +185,20 @@ fn multiply_mix(x: u64, y: u64) -> u64 {
179185
/// We don't bother avalanching here as we'll feed this hash into a
180186
/// multiplication after which we take the high bits, which avalanches for us.
181187
#[inline]
182-
fn hash_bytes(bytes: &[u8]) -> u64 {
188+
#[doc(hidden)]
189+
pub const fn hash_bytes(bytes: &[u8]) -> u64 {
183190
let len = bytes.len();
184191
let mut s0 = SEED1;
185192
let mut s1 = SEED2;
186193

187194
if len <= 16 {
188195
// XOR the input into s0, s1.
189196
if len >= 8 {
190-
s0 ^= u64::from_le_bytes(bytes[0..8].try_into().unwrap());
191-
s1 ^= u64::from_le_bytes(bytes[len - 8..].try_into().unwrap());
197+
s0 ^= read_u64_le(bytes, 0);
198+
s1 ^= read_u64_le(bytes, len - 8);
192199
} else if len >= 4 {
193-
s0 ^= u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as u64;
194-
s1 ^= u32::from_le_bytes(bytes[len - 4..].try_into().unwrap()) as u64;
200+
s0 ^= read_u32_le(bytes, 0) as u64;
201+
s1 ^= read_u32_le(bytes, len - 4) as u64;
195202
} else if len > 0 {
196203
let lo = bytes[0];
197204
let mid = bytes[len / 2];
@@ -203,8 +210,8 @@ fn hash_bytes(bytes: &[u8]) -> u64 {
203210
// Handle bulk (can partially overlap with suffix).
204211
let mut off = 0;
205212
while off < len - 16 {
206-
let x = u64::from_le_bytes(bytes[off..off + 8].try_into().unwrap());
207-
let y = u64::from_le_bytes(bytes[off + 8..off + 16].try_into().unwrap());
213+
let x = read_u64_le(bytes, off);
214+
let y = read_u64_le(bytes, off + 8);
208215

209216
// Replace s1 with a mix of s0, x, and y, and s0 with s1.
210217
// This ensures the compiler can unroll this loop into two
@@ -218,9 +225,8 @@ fn hash_bytes(bytes: &[u8]) -> u64 {
218225
off += 16;
219226
}
220227

221-
let suffix = &bytes[len - 16..];
222-
s0 ^= u64::from_le_bytes(suffix[0..8].try_into().unwrap());
223-
s1 ^= u64::from_le_bytes(suffix[8..16].try_into().unwrap());
228+
s0 ^= read_u64_le(bytes, len - 16);
229+
s1 ^= read_u64_le(bytes, len - 8);
224230
}
225231

226232
multiply_mix(s0, s1) ^ (len as u64)

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ impl PartialEq for RcStr {
300300
fn eq(&self, other: &Self) -> bool {
301301
// For inline RcStrs this is sufficient and for out of line values it handles a simple
302302
// identity cases
303-
if self.unsafe_data == self.unsafe_data {
303+
if self.unsafe_data == other.unsafe_data {
304304
return true;
305305
}
306306
// They can still be equal if they are both stored on the heap
@@ -367,32 +367,54 @@ impl Drop for RcStr {
367367
}
368368
}
369369

370+
// Exports for our macro
370371
#[doc(hidden)]
371372
pub const fn inline_atom(s: &str) -> Option<RcStr> {
372373
dynamic::inline_atom(s)
373374
}
374375

375376
#[doc(hidden)]
376-
pub fn from_static(s: &'static str) -> RcStr {
377+
pub fn from_static(s: &'static PrehashedString) -> RcStr {
377378
dynamic::new_static_atom(s)
378379
}
380+
#[doc(hidden)]
381+
pub use dynamic::{PrehashedString, hash_bytes};
382+
383+
#[doc(hidden)]
384+
impl RcStr {
385+
// Allow the rcstr! macro to skip a tag branch
386+
#[doc(hidden)]
387+
pub fn unsafe_copy_for_macro(&self) -> RcStr {
388+
debug_assert!(self.tag() == STATIC_TAG);
389+
Self {
390+
unsafe_data: self.unsafe_data,
391+
}
392+
}
393+
}
379394

380395
/// Create an rcstr from a string literal.
381396
/// allocates the RcStr inline when possible otherwise uses a `LazyLock` to manage the allocation.
382397
#[macro_export]
383398
macro_rules! rcstr {
384399
($s:expr) => {{
385400
const INLINE: core::option::Option<$crate::RcStr> = $crate::inline_atom($s);
386-
// this condition should be able to be compile time evaluated and inlined.
401+
// This condition can be compile time evaluated and inlined.
387402
if INLINE.is_some() {
388403
INLINE.unwrap()
389404
} else {
390405
#[inline(never)]
391406
fn get_rcstr() -> $crate::RcStr {
392-
static CACHE: std::sync::LazyLock<$crate::RcStr> =
393-
std::sync::LazyLock::new(|| $crate::from_static($s));
394-
395-
(*CACHE).clone()
407+
// Allocate static storage for the PrehashedString
408+
static INNER: ::std::sync::LazyLock<$crate::PrehashedString> =
409+
::std::sync::LazyLock::new(|| $crate::PrehashedString {
410+
value: $s.into(),
411+
// compute the hash at compile time
412+
hash: const { $crate::hash_bytes($s.as_bytes()) },
413+
});
414+
static CACHE: ::std::sync::LazyLock<$crate::RcStr> =
415+
::std::sync::LazyLock::new(|| $crate::from_static(&*INNER));
416+
417+
(*CACHE).unsafe_copy_for_macro()
396418
}
397419
get_rcstr()
398420
}

0 commit comments

Comments
 (0)