Skip to content

Commit 5f0adad

Browse files
authored
chore(turbopack): Update dashmap from 5.x to 6.x (#72433)
Dashmap 6.x claims fairly large performance improvements: > This release contains performance optimizations, most notably 10-40% gains on Apple Silicon but also 5-10% gains when measured in Intel Sapphire Rapids. -- https://github.com/xacrimon/dashmap/releases/tag/v6.0.0 I don't think this means too much for us because while `turbo-tasks` uses dashmap pretty heavily, we spend more time actually doing computations than in `turbo-tasks`, so it's a fraction of a fraction. Still, here's a fairly unscientific (noisy environment, inside a VM) benchmark on my macbook using a microbenchmark that's designed to hit `turbo-tasks` extremely hard (much more so than would happen in realistic scenarios): ``` $ TURBOPACK_BENCH_STRESS=yes cargo bench -p turbo-tasks-memory -- fibonacci/200 Compiling turbo-tasks-memory v0.1.0 (/home/bgw.linux/next.js/turbopack/crates/turbo-tasks-memory) Finished `bench` profile [optimized] target(s) in 2.80s Running benches/mod.rs (target/release/deps/mod-7b9a423ac1e10dce) Gnuplot not found, using plotters backend Benchmarking turbo_tasks_memory_stress/fibonacci/200: Warming up for 3.0000 s Warning: Unable to complete 2000 samples in 5.0s. You may wish to increase target time to 91.9s, or reduce sample count to 100. turbo_tasks_memory_stress/fibonacci/200 time: [33.008 ms 33.108 ms 33.208 ms] thrpt: [605.31 Kelem/s 607.13 Kelem/s 608.97 Kelem/s] change: time: [-2.0573% -1.6309% -1.2221%] (p = 0.00 < 0.05) thrpt: [+1.2373% +1.6580% +2.1005%] Performance has improved. Found 44 outliers among 2000 measurements (2.20%) 2 (0.10%) low mild 38 (1.90%) high mild 4 (0.20%) high severe ``` Any potential improvement to realistic topline numbers are going to be <1% and thus too small to measure (especially since I don't have a low-noise way of measuring Apple Silicon where most of the benefits supposedly are, only x86-64). Closes PACK-3411
1 parent f668af2 commit 5f0adad

File tree

5 files changed

+107
-81
lines changed

5 files changed

+107
-81
lines changed

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ console = "0.15.5"
136136
console-subscriber = "0.1.8"
137137
criterion = "0.5.1"
138138
crossbeam-channel = "0.5.8"
139-
dashmap = "5.4.0"
139+
dashmap = "6.1.0"
140140
dhat = { version = "0.3.2" }
141141
dialoguer = "0.10.3"
142142
dunce = "1.0.3"

turbopack/crates/turbo-tasks-backend/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ auto-hash-map = { workspace = true }
2525
byteorder = "1.5.0"
2626
dashmap = { workspace = true, features = ["raw-api"]}
2727
either = { workspace = true }
28-
hashbrown = { workspace = true }
28+
hashbrown = { workspace = true, features = ["raw"] }
2929
indexmap = { workspace = true }
3030
lmdb-rkv = "0.14.0"
3131
once_cell = { workspace = true }

turbopack/crates/turbo-tasks-backend/src/backend/storage.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ where
359359
T: KeyValuePair,
360360
T::Key: Indexed,
361361
{
362-
inner: RefMut<'a, K, InnerStorage<T>, BuildHasherDefault<FxHasher>>,
362+
inner: RefMut<'a, K, InnerStorage<T>>,
363363
}
364364

365365
impl<K, T> Deref for StorageWriteGuard<'_, K, T>

turbopack/crates/turbo-tasks-backend/src/utils/dash_map_multi.rs

Lines changed: 100 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,26 @@ use std::{
55
};
66

77
use dashmap::{DashMap, RwLockWriteGuard, SharedValue};
8-
use hashbrown::{hash_map, HashMap};
8+
use hashbrown::raw::{Bucket, RawTable};
99

10-
pub enum RefMut<'a, K, V, S>
11-
where
12-
S: BuildHasher,
13-
{
14-
Base(dashmap::mapref::one::RefMut<'a, K, V, S>),
10+
type RwLockWriteTableGuard<'a, K, V> = RwLockWriteGuard<'a, RawTable<(K, SharedValue<V>)>>;
11+
12+
pub enum RefMut<'a, K, V> {
13+
Base(dashmap::mapref::one::RefMut<'a, K, V>),
1514
Simple {
16-
_guard: RwLockWriteGuard<'a, HashMap<K, SharedValue<V>, S>>,
17-
key: *const K,
18-
value: *mut V,
15+
_guard: RwLockWriteTableGuard<'a, K, V>,
16+
bucket: Bucket<(K, SharedValue<V>)>,
1917
},
2018
Shared {
21-
_guard: Arc<RwLockWriteGuard<'a, HashMap<K, SharedValue<V>, S>>>,
22-
key: *const K,
23-
value: *mut V,
19+
_guard: Arc<RwLockWriteTableGuard<'a, K, V>>,
20+
bucket: Bucket<(K, SharedValue<V>)>,
2421
},
2522
}
2623

27-
unsafe impl<K: Eq + Hash + Sync, V: Sync, S: BuildHasher> Send for RefMut<'_, K, V, S> {}
28-
unsafe impl<K: Eq + Hash + Sync, V: Sync, S: BuildHasher> Sync for RefMut<'_, K, V, S> {}
24+
unsafe impl<K: Eq + Hash + Sync, V: Sync> Send for RefMut<'_, K, V> {}
25+
unsafe impl<K: Eq + Hash + Sync, V: Sync> Sync for RefMut<'_, K, V> {}
2926

30-
impl<K: Eq + Hash, V, S: BuildHasher> RefMut<'_, K, V, S> {
27+
impl<K: Eq + Hash, V> RefMut<'_, K, V> {
3128
pub fn value(&self) -> &V {
3229
self.pair().1
3330
}
@@ -39,94 +36,123 @@ impl<K: Eq + Hash, V, S: BuildHasher> RefMut<'_, K, V, S> {
3936
pub fn pair(&self) -> (&K, &V) {
4037
match self {
4138
RefMut::Base(r) => r.pair(),
42-
&RefMut::Simple { key, value, .. } => unsafe { (&*key, &*value) },
43-
&RefMut::Shared { key, value, .. } => unsafe { (&*key, &*value) },
39+
RefMut::Simple { bucket, .. } | RefMut::Shared { bucket, .. } => {
40+
// SAFETY:
41+
// - The bucket is still valid, as we're holding a write guard on the shard
42+
// - These bucket pointers are convertable to references
43+
//
44+
// https://doc.rust-lang.org/std/ptr/index.html#pointer-to-reference-conversion
45+
let entry = unsafe { bucket.as_ref() };
46+
(&entry.0, entry.1.get())
47+
}
4448
}
4549
}
4650

4751
pub fn pair_mut(&mut self) -> (&K, &mut V) {
4852
match self {
4953
RefMut::Base(r) => r.pair_mut(),
50-
&mut RefMut::Simple { key, value, .. } => unsafe { (&*key, &mut *value) },
51-
&mut RefMut::Shared { key, value, .. } => unsafe { (&*key, &mut *value) },
54+
RefMut::Simple { bucket, .. } | RefMut::Shared { bucket, .. } => {
55+
// SAFETY: Same as above in `pair`, plus aliasing is prevented via:
56+
// 1. The lifetime of `&mut self`.
57+
// 2. `Simple` values come from separate shards (no aliasing possible)
58+
// 3. `Shared` values are asserted in `get_multiple_mut` to have unique pointers
59+
let entry = unsafe { bucket.as_mut() };
60+
(&entry.0, entry.1.get_mut())
61+
}
5262
}
5363
}
5464
}
5565

56-
impl<K: Eq + Hash, V, S: BuildHasher> Deref for RefMut<'_, K, V, S> {
66+
impl<K: Eq + Hash, V> Deref for RefMut<'_, K, V> {
5767
type Target = V;
5868

5969
fn deref(&self) -> &V {
6070
self.value()
6171
}
6272
}
6373

64-
impl<K: Eq + Hash, V, S: BuildHasher> DerefMut for RefMut<'_, K, V, S> {
74+
impl<K: Eq + Hash, V> DerefMut for RefMut<'_, K, V> {
6575
fn deref_mut(&mut self) -> &mut V {
6676
self.value_mut()
6777
}
6878
}
6979

70-
impl<'a, K, V, S> From<dashmap::mapref::one::RefMut<'a, K, V, S>> for RefMut<'a, K, V, S>
80+
impl<'a, K, V> From<dashmap::mapref::one::RefMut<'a, K, V>> for RefMut<'a, K, V>
7181
where
7282
K: Hash + Eq,
73-
S: BuildHasher,
7483
{
75-
fn from(r: dashmap::mapref::one::RefMut<'a, K, V, S>) -> Self {
84+
fn from(r: dashmap::mapref::one::RefMut<'a, K, V>) -> Self {
7685
RefMut::Base(r)
7786
}
7887
}
7988

80-
pub fn get_multiple_mut<K, V, S>(
81-
map: &DashMap<K, V, S>,
89+
pub fn get_multiple_mut<K, V>(
90+
map: &DashMap<K, V, impl BuildHasher + Clone>,
8291
key1: K,
8392
key2: K,
8493
insert_with: impl Fn() -> V,
85-
) -> (RefMut<'_, K, V, S>, RefMut<'_, K, V, S>)
94+
) -> (RefMut<'_, K, V>, RefMut<'_, K, V>)
8695
where
8796
K: Hash + Eq + Clone,
88-
S: BuildHasher + Clone,
8997
{
90-
let s1 = map.determine_map(&key1);
91-
let s2 = map.determine_map(&key2);
98+
let hasher = map.hasher();
99+
let hash_entry = |entry: &(K, _)| hasher.hash_one(&entry.0);
100+
let h1 = hasher.hash_one(&key1);
101+
let h2 = hasher.hash_one(&key2);
102+
103+
// Use `determine_shard` instead of `determine_map` to avoid extra rehashing.
104+
// This u64 -> usize conversion also happens internally within DashMap using `as usize`.
105+
// See: `DashMap::hash_usize`
106+
let s1 = map.determine_shard(h1 as usize);
107+
let s2 = map.determine_shard(h2 as usize);
108+
109+
let eq1 = |other: &(K, _)| key1.eq(&other.0);
110+
let eq2 = |other: &(K, _)| key2.eq(&other.0);
111+
92112
let shards = map.shards();
93113
if s1 == s2 {
94114
let mut guard = shards[s1].write();
95-
let e1 = guard
96-
.raw_entry_mut()
97-
.from_key(&key1)
98-
.or_insert_with(|| (key1.clone(), SharedValue::new(insert_with())));
99-
let mut key1_ptr = e1.0 as *const K;
100-
let mut value1_ptr = e1.1.get_mut() as *mut V;
101-
let key2_ptr;
102-
let value2_ptr;
103-
match guard.raw_entry_mut().from_key(&key2) {
104-
hash_map::RawEntryMut::Occupied(e) => {
105-
let e2 = e.into_key_value();
106-
key2_ptr = e2.0 as *const K;
107-
value2_ptr = e2.1.get_mut() as *mut V;
108-
}
109-
hash_map::RawEntryMut::Vacant(e) => {
110-
let e2 = e.insert(key2.clone(), SharedValue::new(insert_with()));
111-
key2_ptr = e2.0 as *const K;
112-
value2_ptr = e2.1.get_mut() as *mut V;
113-
// inserting a new entry might invalidate the pointers of the first entry
114-
let e1 = guard.get_key_value_mut(&key1).unwrap();
115-
key1_ptr = e1.0 as *const K;
116-
value1_ptr = e1.1.get_mut() as *mut V;
117-
}
118-
}
115+
116+
// we need to call `find_or_find_insert_slot` to avoid overwriting existing entries, but we
117+
// can't use the returned bucket until after we get `bucket2` (below)
118+
let _ = guard
119+
.find_or_find_insert_slot(h1, eq1, hash_entry)
120+
.unwrap_or_else(|slot| unsafe {
121+
// SAFETY: This slot was previously returned by `find_or_find_insert_slot`, and no
122+
// mutation of the table has occured since that call.
123+
guard.insert_in_slot(h1, slot, (key1.clone(), SharedValue::new(insert_with())))
124+
});
125+
126+
let bucket2 = guard
127+
.find_or_find_insert_slot(h2, eq2, hash_entry)
128+
.unwrap_or_else(|slot| unsafe {
129+
// SAFETY: See previous call above
130+
guard.insert_in_slot(h2, slot, (key2.clone(), SharedValue::new(insert_with())))
131+
});
132+
133+
// Getting `bucket2` might invalidate the bucket pointer of the first entry, *even if no
134+
// insert happens* as `RawTable::find_or_find_insert_slot` will *sometimes* resize the
135+
// table, as it unconditionally reserves space for a potential insertion.
136+
let bucket1 = guard.find(h1, eq1).expect(
137+
"failed to find bucket of previously inserted item, is the hash or eq implementation \
138+
incorrect?",
139+
);
140+
141+
// this assertion is needed for memory safety reasons
142+
assert!(
143+
!std::ptr::eq(bucket1.as_ptr(), bucket2.as_ptr()),
144+
"`get_multiple_mut` was called with equal keys, which breaks mutable referencing rules"
145+
);
146+
119147
let guard = Arc::new(guard);
120148
(
121149
RefMut::Shared {
122150
_guard: guard.clone(),
123-
key: key1_ptr,
124-
value: value1_ptr,
151+
bucket: bucket1,
125152
},
126153
RefMut::Shared {
127154
_guard: guard,
128-
key: key2_ptr,
129-
value: value2_ptr,
155+
bucket: bucket2,
130156
},
131157
)
132158
} else {
@@ -144,28 +170,28 @@ where
144170
}
145171
}
146172
};
147-
let e1 = guard1
148-
.raw_entry_mut()
149-
.from_key(&key1)
150-
.or_insert_with(|| (key1, SharedValue::new(insert_with())));
151-
let key1 = e1.0 as *const K;
152-
let value1 = e1.1.get_mut() as *mut V;
153-
let e2 = guard2
154-
.raw_entry_mut()
155-
.from_key(&key2)
156-
.or_insert_with(|| (key2, SharedValue::new(insert_with())));
157-
let key2 = e2.0 as *const K;
158-
let value2 = e2.1.get_mut() as *mut V;
173+
174+
let bucket1 = guard1
175+
.find_or_find_insert_slot(h1, eq1, hash_entry)
176+
.unwrap_or_else(|slot| unsafe {
177+
// SAFETY: See first insert_in_slot call
178+
guard1.insert_in_slot(h1, slot, (key1.clone(), SharedValue::new(insert_with())))
179+
});
180+
let bucket2 = guard2
181+
.find_or_find_insert_slot(h2, eq2, hash_entry)
182+
.unwrap_or_else(|slot| unsafe {
183+
// SAFETY: See first insert_in_slot call
184+
guard2.insert_in_slot(h2, slot, (key2.clone(), SharedValue::new(insert_with())))
185+
});
186+
159187
(
160188
RefMut::Simple {
161189
_guard: guard1,
162-
key: key1,
163-
value: value1,
190+
bucket: bucket1,
164191
},
165192
RefMut::Simple {
166193
_guard: guard2,
167-
key: key2,
168-
value: value2,
194+
bucket: bucket2,
169195
},
170196
)
171197
}

0 commit comments

Comments
 (0)