Repository: vercel/next.js
Pull request: perf(turbopack/rcstr): Precompute hash for equality bailout

### What?

Precompute hash for `RcStr` and use it for equality bailout.

### Why?


We have lots of `RcStr` in key positions of the hashmap, so adding a fast bailout path to `==` improved the performance.

x-ref: https://vercel.slack.com/archives/C06PPGZ0FD3/p1746715835436899?thread_ts=1746658944.635819&cid=C06PPGZ0FD3

### How?



Closes PACK-4755

Changed files:
filename: Cargo.lock
patch: @@ -9562,6 +9562,7 @@ dependencies = [
  "codspeed-criterion-compat",
  "napi",
  "new_debug_unreachable",
+ "rustc-hash 2.1.1",
  "serde",
  "shrink-to-fit",
  "triomphe 0.1.12",

filename: turbopack/crates/turbo-rcstr/Cargo.toml
patch: @@ -16,6 +16,7 @@ serde = { workspace = true }
 new_debug_unreachable = "1.0.6"
 shrink-to-fit = { workspace = true }
 napi = { workspace = true, optional = true }
+rustc-hash = { workspace = true }
 
 [dev-dependencies]
 criterion = { workspace = true }

filename: turbopack/crates/turbo-rcstr/src/dynamic.rs
patch: @@ -1,23 +1,33 @@
-use std::{num::NonZeroU8, ptr::NonNull};
+use std::{
+    hash::{Hash, Hasher},
+    num::NonZeroU8,
+    ptr::NonNull,
+};
 
+use rustc_hash::FxHasher;
 use triomphe::Arc;
 
 use crate::{
     INLINE_TAG, INLINE_TAG_INIT, LEN_OFFSET, RcStr, TAG_MASK,
     tagged_value::{MAX_INLINE_LEN, TaggedValue},
 };
 
-pub unsafe fn cast(ptr: TaggedValue) -> *const String {
+pub(crate) struct PrehashedString {
+    pub value: String,
+    pub hash: u64,
+}
+
+pub unsafe fn cast(ptr: TaggedValue) -> *const PrehashedString {
     ptr.get_ptr().cast()
 }
 
-pub unsafe fn deref_from<'i>(ptr: TaggedValue) -> &'i String {
+pub(crate) unsafe fn deref_from<'i>(ptr: TaggedValue) -> &'i PrehashedString {
     unsafe { &*cast(ptr) }
 }
 
 /// Caller should call `forget` (or `clone`) on the returned `Arc`
-pub unsafe fn restore_arc(v: TaggedValue) -> Arc<String> {
-    let ptr = v.get_ptr() as *const String;
+pub unsafe fn restore_arc(v: TaggedValue) -> Arc<PrehashedString> {
+    let ptr = v.get_ptr() as *const PrehashedString;
     unsafe { Arc::from_raw(ptr) }
 }
 
@@ -36,12 +46,17 @@ pub(crate) fn new_atom<T: AsRef<str> + Into<String>>(text: T) -> RcStr {
         return RcStr { unsafe_data };
     }
 
-    let entry = Arc::new(text.into());
+    let hash = compute_fxhash(text.as_ref());
+
+    let entry: Arc<PrehashedString> = Arc::new(PrehashedString {
+        value: text.into(),
+        hash,
+    });
     let entry = Arc::into_raw(entry);
 
-    let ptr: NonNull<String> = unsafe {
+    let ptr: NonNull<PrehashedString> = unsafe {
         // Safety: Arc::into_raw returns a non-null pointer
-        NonNull::new_unchecked(entry as *mut String)
+        NonNull::new_unchecked(entry as *mut _)
     };
     debug_assert!(0 == ptr.as_ptr() as u8 & TAG_MASK);
     RcStr {
@@ -71,3 +86,9 @@ pub(crate) const fn inline_atom(text: &str) -> Option<RcStr> {
     }
     None
 }
+
+fn compute_fxhash(s: &str) -> u64 {
+    let mut hasher = FxHasher::default();
+    s.hash(&mut hasher);
+    hasher.finish()
+}

filename: turbopack/crates/turbo-rcstr/src/lib.rs
patch: @@ -15,7 +15,10 @@ use shrink_to_fit::ShrinkToFit;
 use triomphe::Arc;
 use turbo_tasks_hash::{DeterministicHash, DeterministicHasher};
 
-use crate::{dynamic::new_atom, tagged_value::TaggedValue};
+use crate::{
+    dynamic::{deref_from, new_atom},
+    tagged_value::TaggedValue,
+};
 
 mod dynamic;
 mod tagged_value;
@@ -80,7 +83,7 @@ impl RcStr {
     #[inline(never)]
     pub fn as_str(&self) -> &str {
         match self.tag() {
-            DYNAMIC_TAG => unsafe { dynamic::deref_from(self.unsafe_data) },
+            DYNAMIC_TAG => unsafe { dynamic::deref_from(self.unsafe_data).value.as_str() },
             INLINE_TAG => {
                 let len = (self.unsafe_data.tag() & LEN_MASK) >> LEN_OFFSET;
                 let src = self.unsafe_data.data();
@@ -103,8 +106,8 @@ impl RcStr {
                 // convert `self` into `arc`
                 let arc = unsafe { dynamic::restore_arc(ManuallyDrop::new(self).unsafe_data) };
                 match Arc::try_unwrap(arc) {
-                    Ok(v) => v,
-                    Err(arc) => arc.to_string(),
+                    Ok(v) => v.value,
+                    Err(arc) => arc.value.to_string(),
                 }
             }
             INLINE_TAG => self.as_str().to_string(),
@@ -256,7 +259,15 @@ impl Default for RcStr {
 
 impl PartialEq for RcStr {
     fn eq(&self, other: &Self) -> bool {
-        self.as_str() == other.as_str()
+        match (self.tag(), other.tag()) {
+            (DYNAMIC_TAG, DYNAMIC_TAG) => {
+                let l = unsafe { deref_from(self.unsafe_data) };
+                let r = unsafe { deref_from(other.unsafe_data) };
+                l.hash == r.hash && l.value == r.value
+            }
+            (INLINE_TAG, INLINE_TAG) => self.unsafe_data == other.unsafe_data,
+            _ => false,
+        }
     }
 }
 
@@ -276,7 +287,16 @@ impl Ord for RcStr {
 
 impl Hash for RcStr {
     fn hash<H: Hasher>(&self, state: &mut H) {
-        self.as_str().hash(state);
+        match self.tag() {
+            DYNAMIC_TAG => {
+                let l = unsafe { deref_from(self.unsafe_data) };
+                l.value.as_str().hash(state);
+            }
+            INLINE_TAG => {
+                self.as_str().hash(state);
+            }
+            _ => unsafe { debug_unreachable!() },
+        }
     }
 }