├── .gitignore ├── fuzz ├── .gitignore ├── Cargo.toml ├── fuzz_targets │ └── std.rs └── Cargo.lock ├── .github ├── DOCS.md └── workflows │ ├── test.yml │ ├── safety.yml │ └── check.yml ├── assets ├── papaya-hist.png ├── dashmap-hist.png ├── Exchange.ahash.throughput.svg ├── RapidGrow.ahash.throughput.svg ├── ReadHeavy.ahash.throughput.svg ├── RapidGrow.ahash.latency.svg ├── Exchange.ahash.latency.svg └── ReadHeavy.ahash.latency.svg ├── LICENSE.md ├── Cargo.toml ├── src ├── raw │ ├── probe.rs │ ├── utils │ │ ├── counter.rs │ │ ├── stack.rs │ │ ├── mod.rs │ │ ├── tagged.rs │ │ └── parker.rs │ └── alloc.rs ├── serde_impls.rs └── lib.rs ├── README.md ├── benches ├── single_thread.rs └── latency.rs ├── BENCHMARKS.md └── tests ├── common.rs ├── cuckoo.rs └── basic_set.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /.github/DOCS.md: -------------------------------------------------------------------------------- 1 | Workflows adapted from https://github.com/jonhoo/rust-ci-conf. 2 | -------------------------------------------------------------------------------- /assets/papaya-hist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibraheemdev/papaya/HEAD/assets/papaya-hist.png -------------------------------------------------------------------------------- /assets/dashmap-hist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibraheemdev/papaya/HEAD/assets/dashmap-hist.png -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "papaya-fuzz" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | libfuzzer-sys = "0.4" 12 | arbitrary = { features = ["derive"], version = "1.0" } 13 | 14 | [dependencies.papaya] 15 | path = ".." 16 | 17 | [[bin]] 18 | name = "std" 19 | path = "fuzz_targets/std.rs" 20 | test = false 21 | doc = false 22 | bench = false 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "papaya" 3 | version = "0.2.3" 4 | authors = ["Ibraheem Ahmed "] 5 | description = "A fast and ergonomic concurrent hash-table for read-heavy workloads." 6 | edition = "2021" 7 | rust-version = "1.72.0" 8 | license = "MIT" 9 | readme = "README.md" 10 | repository = "https://github.com/ibraheemdev/papaya" 11 | categories = ["algorithms", "concurrency", "data-structures"] 12 | keywords = ["concurrent", "hashmap", "atomic", "lock-free"] 13 | exclude = ["assets/*"] 14 | 15 | [dependencies] 16 | equivalent = "1" 17 | seize = "0.5" 18 | serde = { version = "1", optional = true } 19 | 20 | [dev-dependencies] 21 | rand = "0.8" 22 | base64 = "0.22" 23 | hdrhistogram = "7" 24 | dashmap = "5" 25 | criterion = "0.5" 26 | tokio = { version = "1", features = ["fs", "rt"] } 27 | num_cpus = "1" 28 | serde_json = "1" 29 | 30 | [features] 31 | default = [] 32 | serde = ["dep:serde"] 33 | 34 | [profile.test] 35 | inherits = "release" 36 | debug-assertions = true 37 | 38 | [lints.rust] 39 | unexpected_cfgs = { level = "warn", check-cfg = [ 40 | 'cfg(papaya_stress)', 41 | 'cfg(papaya_asan)', 42 | ] } 43 | 44 | [[bench]] 45 | name = "single_thread" 46 | harness = false 47 | 48 | [[bench]] 49 | name = "latency" 50 | harness = false 51 | -------------------------------------------------------------------------------- /src/raw/probe.rs: -------------------------------------------------------------------------------- 1 | // A quadratic probe sequence. 2 | #[derive(Default)] 3 | pub struct Probe { 4 | // The current index in the probe sequence. 5 | pub i: usize, 6 | // The current length of the probe sequence. 7 | pub len: usize, 8 | } 9 | 10 | impl Probe { 11 | // Initialize the probe sequence. 12 | #[inline] 13 | pub fn start(hash: usize, mask: usize) -> Probe { 14 | Probe { 15 | i: hash & mask, 16 | len: 0, 17 | } 18 | } 19 | 20 | // Increment the probe sequence. 21 | #[inline] 22 | pub fn next(&mut self, mask: usize) { 23 | self.len += 1; 24 | self.i = (self.i + self.len) & mask; 25 | } 26 | } 27 | 28 | // The maximum probe length for table operations. 29 | // 30 | // Estimating a load factor for the hash-table based on probe lengths allows 31 | // the hash-table to avoid loading the length every insert, which is a source 32 | // of contention. 33 | pub fn limit(capacity: usize) -> usize { 34 | // 5 * log2(capacity): Testing shows this gives us a ~85% load factor. 35 | 5 * ((usize::BITS as usize) - (capacity.leading_zeros() as usize) - 1) 36 | } 37 | 38 | // Returns an estimate of the number of entries needed to hold `capacity` elements. 39 | pub fn entries_for(capacity: usize) -> usize { 40 | // We should rarely resize before 75%. 41 | let capacity = capacity.checked_mul(8).expect("capacity overflow") / 6; 42 | capacity.next_power_of_two() 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `papaya` 2 | 3 | [crates.io](https://crates.io/crates/papaya) 4 | [github](https://github.com/ibraheemdev/papaya) 5 | [docs.rs](https://docs.rs/papaya) 6 | 7 | A fast and ergonomic concurrent hash-table for read-heavy workloads. 8 | 9 | See [the documentation](https://docs.rs/papaya/latest) to get started. 10 | 11 | ## Features 12 | 13 | - An ergonomic lock-free API — no more deadlocks! 14 | - Powerful atomic operations. 15 | - Seamless usage in async contexts. 16 | - Extremely scalable, low-latency reads (see [performance](#performance)). 17 | - Predictable latency across all operations. 18 | - Efficient memory usage, with garbage collection powered by [`seize`]. 19 | 20 | ## Performance 21 | 22 | `papaya` is built with read-heavy workloads in mind. As such, read operations are extremely high throughput and provide consistent performance that scales with concurrency, meaning `papaya` will excel in workloads where reads are more common than writes. In write heavy workloads, `papaya` will still provide competitive performance despite not being it's primary use case. See the [benchmarks] for details. 23 | 24 | `papaya` aims to provide predictable and consistent latency across all operations. Most operations are lock-free, and those that aren't only block under rare and constrained conditions. `papaya` also features [incremental resizing]. Predictable latency is an important part of performance that doesn't often show up in benchmarks, but has significant implications for real-world usage. 25 | 26 | [benchmarks]: ./BENCHMARKS.md 27 | [`seize`]: https://github.com/ibraheemdev/seize 28 | [incremental resizing]: https://docs.rs/papaya/latest/papaya/enum.ResizeMode.html 29 | -------------------------------------------------------------------------------- /src/raw/utils/counter.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicIsize, Ordering}, 3 | OnceLock, 4 | }; 5 | 6 | use super::CachePadded; 7 | 8 | // A sharded atomic counter. 9 | // 10 | // Sharding the length counter of `HashMap` is extremely important, 11 | // as a single point of contention for insertions/deletions significantly 12 | // degrades concurrent performance. 13 | // 14 | // We can take advantage of the fact that `seize::Guard` 15 | pub struct Counter(Box<[CachePadded]>); 16 | 17 | impl Default for Counter { 18 | /// Create a new `Counter`. 19 | fn default() -> Counter { 20 | // available_parallelism is quite slow (microseconds). 21 | static CPUS: OnceLock = OnceLock::new(); 22 | let num_cpus = *CPUS.get_or_init(|| { 23 | std::thread::available_parallelism() 24 | .map(Into::into) 25 | .unwrap_or(1) 26 | }); 27 | 28 | // Round up to the next power-of-two for fast modulo. 29 | let shards = (0..num_cpus.next_power_of_two()) 30 | .map(|_| Default::default()) 31 | .collect(); 32 | 33 | Counter(shards) 34 | } 35 | } 36 | 37 | impl Counter { 38 | // Return the shard for the given thread ID. 39 | #[inline] 40 | pub fn get(&self, guard: &impl seize::Guard) -> &AtomicIsize { 41 | // Guard thread IDs are essentially perfectly sharded due to 42 | // the internal thread ID allocator, which makes contention 43 | // very unlikely even with the exact number of shards as CPUs. 44 | let shard = guard.thread_id() & (self.0.len() - 1); 45 | 46 | &self.0[shard].value 47 | } 48 | 49 | // Returns the sum of all counter shards. 50 | #[inline] 51 | pub fn sum(&self) -> usize { 52 | self.0 53 | .iter() 54 | .map(|x| x.value.load(Ordering::Relaxed)) 55 | .sum::() 56 | .try_into() 57 | // Depending on the order of deletion/insertions this might be negative, 58 | // in which case we assume the map is empty. 59 | .unwrap_or(0) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /benches/single_thread.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 4 | 5 | const SIZE: usize = 10_000; 6 | 7 | fn compare(c: &mut Criterion) { 8 | let mut group = c.benchmark_group("read"); 9 | 10 | #[derive(Clone, Copy)] 11 | struct RandomKeys { 12 | state: usize, 13 | } 14 | 15 | impl RandomKeys { 16 | fn new() -> Self { 17 | RandomKeys { state: 0 } 18 | } 19 | } 20 | 21 | impl Iterator for RandomKeys { 22 | type Item = usize; 23 | fn next(&mut self) -> Option { 24 | // Add 1 then multiply by some 32 bit prime. 25 | self.state = self.state.wrapping_add(1).wrapping_mul(3_787_392_781); 26 | Some(self.state) 27 | } 28 | } 29 | 30 | group.bench_function("papaya", |b| { 31 | let m = papaya::HashMap::::builder() 32 | .collector(seize::Collector::new()) 33 | .build(); 34 | 35 | for i in RandomKeys::new().take(SIZE) { 36 | m.pin().insert(i, i); 37 | } 38 | 39 | b.iter(|| { 40 | for i in RandomKeys::new().take(SIZE) { 41 | black_box(assert_eq!(m.pin().get(&i), Some(&i))); 42 | } 43 | }); 44 | }); 45 | 46 | group.bench_function("std", |b| { 47 | let mut m = HashMap::::default(); 48 | for i in RandomKeys::new().take(SIZE) { 49 | m.insert(i, i); 50 | } 51 | 52 | b.iter(|| { 53 | for i in RandomKeys::new().take(SIZE) { 54 | black_box(assert_eq!(m.get(&i), Some(&i))); 55 | } 56 | }); 57 | }); 58 | 59 | group.bench_function("dashmap", |b| { 60 | let m = dashmap::DashMap::::default(); 61 | for i in RandomKeys::new().take(SIZE) { 62 | m.insert(i, i); 63 | } 64 | 65 | b.iter(|| { 66 | for i in RandomKeys::new().take(SIZE) { 67 | black_box(assert_eq!(*m.get(&i).unwrap(), i)); 68 | } 69 | }); 70 | }); 71 | 72 | group.finish(); 73 | } 74 | 75 | criterion_group!(benches, compare); 76 | criterion_main!(benches); 77 | -------------------------------------------------------------------------------- /BENCHMARKS.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | *As always, benchmarks should be taken with a grain of salt. Always measure for your workload.* 4 | 5 | Below are the benchmark results from the [`conc-map-bench`](https://github.com/xacrimon/conc-map-bench) benchmarking harness under varying workloads. All benchmarks were run on a AMD Ryzen 9 9950X processor, using [`ahash`](https://github.com/tkaitchuck/aHash) and the [`mimalloc`](https://github.com/microsoft/mimalloc) allocator. 6 | 7 | ### Read Heavy 8 | 9 | | | | 10 | :-------------------------:|:-------------------------: 11 | ![](assets/ReadHeavy.ahash.throughput.svg) | ![](assets/ReadHeavy.ahash.latency.svg) 12 | 13 | ### Exchange 14 | 15 | | | | 16 | :-------------------------:|:-------------------------: 17 | ![](assets/Exchange.ahash.throughput.svg) | ![](assets/Exchange.ahash.latency.svg) 18 | 19 | ### Rapid Grow 20 | 21 | | | | 22 | :-------------------------:|:-------------------------: 23 | ![](assets/RapidGrow.ahash.throughput.svg) | ![](assets/RapidGrow.ahash.latency.svg) 24 | 25 | # Discussion 26 | 27 | As mentioned in the [performance](../README#performance) section of the guide, `papaya` is optimized read-heavy workloads. As expected, it outperforms all competitors in the read-heavy benchmark. An important guarantee of `papaya` is that reads *never* block under any circumstances. This is crucial for providing consistent read latency regardless of write concurrency. However, it falls short in update-heavy workloads due to allocator pressure and the overhead of memory reclamation, which is necessary for lock-free reads. If your workload is write-heavy and you do not benefit from any of `papaya`'s features, you may wish to consider an alternate hash-table implementation. 28 | 29 | Additionally, `papaya` does a lot better in terms of latency distribution due to incremental resizing and the lack of bucket locks. Comparing histograms of `insert` latency between `papaya` and `dashmap`, we see that `papaya` manages to keep tail latency lower by a few orders of magnitude. Some latency spikes are unavoidable due to the allocations necessary to maintain a large hash-table, but the distribution is much more consistent (notice the scale of the y-axis). 30 | 31 | ![](assets/papaya-hist.png) 32 | ![](assets/dashmap-hist.png) 33 | -------------------------------------------------------------------------------- /src/raw/utils/stack.rs: -------------------------------------------------------------------------------- 1 | use std::ptr; 2 | use std::sync::atomic::{AtomicPtr, Ordering}; 3 | 4 | /// A simple lock-free, append-only, stack of pointers. 5 | /// 6 | /// This stack is used to defer the reclamation of borrowed entries during 7 | /// an incremental resize, which is relatively rare. 8 | /// 9 | /// Alternative, the deletion algorithm could traverse and delete the entry 10 | /// from previous tables to ensure it is unreachable from the root. However, 11 | /// it's not clear whether this is better than an allocation or even lock. 12 | pub struct Stack { 13 | head: AtomicPtr>, 14 | } 15 | 16 | /// A node in the stack. 17 | struct Node { 18 | value: T, 19 | next: *mut Node, 20 | } 21 | 22 | impl Stack { 23 | /// Create a new `Stack`. 24 | pub fn new() -> Self { 25 | Self { 26 | head: AtomicPtr::new(ptr::null_mut()), 27 | } 28 | } 29 | 30 | /// Add an entry to the stack. 31 | pub fn push(&self, value: T) { 32 | let node = Box::into_raw(Box::new(Node { 33 | value, 34 | next: ptr::null_mut(), 35 | })); 36 | 37 | loop { 38 | // Load the head node. 39 | // 40 | // `Relaxed` is sufficient here as all reads are through `&mut self`. 41 | let head = self.head.load(Ordering::Relaxed); 42 | 43 | // Link the node to the stack. 44 | unsafe { (*node).next = head } 45 | 46 | // Attempt to push the node. 47 | // 48 | // `Relaxed` is similarly sufficient here. 49 | if self 50 | .head 51 | .compare_exchange(head, node, Ordering::Relaxed, Ordering::Relaxed) 52 | .is_ok() 53 | { 54 | break; 55 | } 56 | } 57 | } 58 | 59 | /// Drain all elements from the stack. 60 | pub fn drain(&mut self, mut f: impl FnMut(T)) { 61 | let mut head = *self.head.get_mut(); 62 | 63 | while !head.is_null() { 64 | // Safety: We have `&mut self` and the node is non-null. 65 | let owned_head = unsafe { Box::from_raw(head) }; 66 | 67 | // Drain the element. 68 | f(owned_head.value); 69 | 70 | // Continue iterating over the stack. 71 | head = owned_head.next; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use papaya::{HashMap, HashSet, ResizeMode}; 4 | use seize::Collector; 5 | 6 | // Run the test on different configurations of a `HashMap`. 7 | pub fn with_map(mut test: impl FnMut(&dyn Fn() -> HashMap)) { 8 | let collector = || Collector::new().batch_size(128); 9 | 10 | // Blocking resize mode. 11 | if !cfg!(papaya_stress) { 12 | test( 13 | &(|| { 14 | HashMap::builder() 15 | .collector(collector()) 16 | .resize_mode(ResizeMode::Blocking) 17 | .build() 18 | }), 19 | ); 20 | } 21 | 22 | // Incremental resize mode with a small chunk to stress operations on nested tables. 23 | test( 24 | &(|| { 25 | HashMap::builder() 26 | .collector(collector()) 27 | .resize_mode(ResizeMode::Incremental(1)) 28 | .build() 29 | }), 30 | ); 31 | 32 | // Incremental resize mode with a medium-sized chunk to promote interference with incremental 33 | // resizing. 34 | test( 35 | &(|| { 36 | HashMap::builder() 37 | .collector(collector()) 38 | .resize_mode(ResizeMode::Incremental(128)) 39 | .build() 40 | }), 41 | ); 42 | } 43 | 44 | // Run the test on different configurations of a `HashSet`. 45 | pub fn with_set(mut test: impl FnMut(&dyn Fn() -> HashSet)) { 46 | // Blocking resize mode. 47 | if !cfg!(papaya_stress) { 48 | test(&(|| HashSet::builder().resize_mode(ResizeMode::Blocking).build())); 49 | } 50 | 51 | // Incremental resize mode with a small chunk to stress operations on nested tables. 52 | test( 53 | &(|| { 54 | HashSet::builder() 55 | .resize_mode(ResizeMode::Incremental(1)) 56 | .build() 57 | }), 58 | ); 59 | 60 | // Incremental resize mode with a medium-sized chunk to promote interference with incremental 61 | // resizing. 62 | test( 63 | &(|| { 64 | HashSet::builder() 65 | .resize_mode(ResizeMode::Incremental(128)) 66 | .build() 67 | }), 68 | ); 69 | } 70 | 71 | // Prints a log message if `RUST_LOG=debug` is set. 72 | #[macro_export] 73 | macro_rules! debug { 74 | ($($x:tt)*) => { 75 | if std::env::var("RUST_LOG").as_deref() == Ok("debug") { 76 | println!($($x)*); 77 | } 78 | }; 79 | } 80 | 81 | // Returns the number of threads to use for stress testing. 82 | pub fn threads() -> usize { 83 | if cfg!(miri) { 84 | 2 85 | } else { 86 | num_cpus::get_physical().next_power_of_two() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /benches/latency.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::sync::Barrier; 3 | use std::thread; 4 | 5 | use base64::engine::general_purpose::STANDARD; 6 | use base64::write::EncoderWriter; 7 | use hdrhistogram::serialization::{Serializer, V2DeflateSerializer}; 8 | use hdrhistogram::{Histogram, SyncHistogram}; 9 | 10 | fn main() { 11 | println!("=== papaya (incremental) ==="); 12 | p99_insert(papaya::HashMap::new(), |map, i| { 13 | map.pin().insert(i, ()); 14 | }); 15 | p99_concurrent_insert("papaya", papaya::HashMap::new(), |map, i| { 16 | map.pin().insert(i, ()); 17 | }); 18 | 19 | println!("=== papaya (blocking) ==="); 20 | let map = papaya::HashMap::builder() 21 | .resize_mode(papaya::ResizeMode::Blocking) 22 | .build(); 23 | 24 | p99_insert(map.clone(), |map, i| { 25 | map.pin().insert(i, ()); 26 | }); 27 | p99_concurrent_insert("papaya-blocking", map, |map, i| { 28 | map.pin().insert(i, ()); 29 | }); 30 | 31 | println!("=== dashmap ==="); 32 | p99_insert(dashmap::DashMap::new(), |map, i| { 33 | map.insert(i, ()); 34 | }); 35 | p99_concurrent_insert("dashmap", dashmap::DashMap::new(), |map, i| { 36 | map.insert(i, ()); 37 | }); 38 | } 39 | 40 | fn p99_insert(map: T, insert: impl Fn(&T, usize)) { 41 | const ITEMS: usize = 10_000_000; 42 | 43 | let mut max = None; 44 | 45 | for i in 0..ITEMS { 46 | let now = std::time::Instant::now(); 47 | insert(&map, i); 48 | let elapsed = now.elapsed(); 49 | 50 | if max.map(|max| elapsed > max).unwrap_or(true) { 51 | max = Some(elapsed); 52 | } 53 | } 54 | 55 | println!("p99 insert: {}ms", max.unwrap().as_millis()); 56 | } 57 | 58 | fn p99_concurrent_insert(name: &str, map: T, insert: impl Fn(&T, usize) + Send + Copy) { 59 | const ITEMS: usize = 1_000_000; 60 | 61 | let barrier = Barrier::new(8); 62 | let mut hist = SyncHistogram::::from(Histogram::new(1).unwrap()); 63 | 64 | thread::scope(|s| { 65 | for t in 0..8 { 66 | let (barrier, map) = (&barrier, &map); 67 | let mut hist = hist.recorder(); 68 | s.spawn(move || { 69 | barrier.wait(); 70 | 71 | let mut max = None; 72 | for i in 0..ITEMS { 73 | let i = (t + 1) * i; 74 | 75 | let now = std::time::Instant::now(); 76 | insert(&map, i); 77 | let elapsed = now.elapsed(); 78 | 79 | if max.map(|max| elapsed > max).unwrap_or(true) { 80 | max = Some(elapsed); 81 | } 82 | 83 | hist.record(elapsed.as_micros().try_into().unwrap()) 84 | .unwrap(); 85 | } 86 | 87 | println!("p99 concurrent insert: {}ms", max.unwrap().as_millis()); 88 | }); 89 | } 90 | }); 91 | 92 | hist.refresh(); 93 | 94 | let mut f = File::create(format!("{name}.hist")).unwrap(); 95 | let mut s = V2DeflateSerializer::new(); 96 | s.serialize(&hist, &mut EncoderWriter::new(&mut f, &STANDARD)) 97 | .unwrap(); 98 | } 99 | -------------------------------------------------------------------------------- /src/raw/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod counter; 2 | mod parker; 3 | mod stack; 4 | mod tagged; 5 | 6 | pub use counter::Counter; 7 | pub use parker::Parker; 8 | pub use stack::Stack; 9 | pub use tagged::{untagged, AtomicPtrFetchOps, StrictProvenance, Tagged, Unpack}; 10 | 11 | /// A `seize::Guard` that has been verified to belong to a given map. 12 | pub trait VerifiedGuard: seize::Guard {} 13 | 14 | #[repr(transparent)] 15 | pub struct MapGuard(G); 16 | 17 | impl MapGuard { 18 | /// Create a new `MapGuard`. 19 | /// 20 | /// # Safety 21 | /// 22 | /// The guard must be valid to use with the given map. 23 | pub unsafe fn new(guard: G) -> MapGuard { 24 | MapGuard(guard) 25 | } 26 | 27 | /// Create a new `MapGuard` from a reference. 28 | /// 29 | /// # Safety 30 | /// 31 | /// The guard must be valid to use with the given map. 32 | pub unsafe fn from_ref(guard: &G) -> &MapGuard { 33 | // Safety: `VerifiedGuard` is `repr(transparent)` over `G`. 34 | unsafe { &*(guard as *const G as *const MapGuard) } 35 | } 36 | } 37 | 38 | impl VerifiedGuard for MapGuard where G: seize::Guard {} 39 | 40 | impl seize::Guard for MapGuard 41 | where 42 | G: seize::Guard, 43 | { 44 | #[inline] 45 | fn refresh(&mut self) { 46 | self.0.refresh(); 47 | } 48 | 49 | #[inline] 50 | fn flush(&self) { 51 | self.0.flush(); 52 | } 53 | 54 | #[inline] 55 | fn collector(&self) -> &seize::Collector { 56 | self.0.collector() 57 | } 58 | 59 | #[inline] 60 | fn thread_id(&self) -> usize { 61 | self.0.thread_id() 62 | } 63 | 64 | #[inline] 65 | unsafe fn defer_retire(&self, ptr: *mut T, reclaim: unsafe fn(*mut T, &seize::Collector)) { 66 | unsafe { self.0.defer_retire(ptr, reclaim) }; 67 | } 68 | } 69 | 70 | /// Pads and aligns a value to the length of a cache line. 71 | /// 72 | // Source: https://github.com/crossbeam-rs/crossbeam/blob/0f81a6957588ddca9973e32e92e7e94abdad801e/crossbeam-utils/src/cache_padded.rs#L63. 73 | #[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] 74 | #[cfg_attr( 75 | any( 76 | target_arch = "x86_64", 77 | target_arch = "aarch64", 78 | target_arch = "arm64ec", 79 | target_arch = "powerpc64", 80 | ), 81 | repr(align(128)) 82 | )] 83 | #[cfg_attr( 84 | any( 85 | target_arch = "arm", 86 | target_arch = "mips", 87 | target_arch = "mips32r6", 88 | target_arch = "mips64", 89 | target_arch = "mips64r6", 90 | target_arch = "sparc", 91 | target_arch = "hexagon", 92 | ), 93 | repr(align(32)) 94 | )] 95 | #[cfg_attr(target_arch = "m68k", repr(align(16)))] 96 | #[cfg_attr(target_arch = "s390x", repr(align(256)))] 97 | #[cfg_attr( 98 | not(any( 99 | target_arch = "x86_64", 100 | target_arch = "aarch64", 101 | target_arch = "arm64ec", 102 | target_arch = "powerpc64", 103 | target_arch = "arm", 104 | target_arch = "mips", 105 | target_arch = "mips32r6", 106 | target_arch = "mips64", 107 | target_arch = "mips64r6", 108 | target_arch = "sparc", 109 | target_arch = "hexagon", 110 | target_arch = "m68k", 111 | target_arch = "s390x", 112 | )), 113 | repr(align(64)) 114 | )] 115 | pub struct CachePadded { 116 | value: T, 117 | } 118 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is the main CI workflow that runs the test suite on all pushes to main and all pull requests. 2 | # It runs the following jobs: 3 | # - required: runs the test suite on ubuntu with stable and beta rust toolchains 4 | # requirements of this crate, and its dependencies 5 | # - os-check: runs the test suite on mac and windows 6 | # See check.yml for information about how the concurrency cancellation and workflow triggering works 7 | permissions: 8 | contents: read 9 | on: 10 | push: 11 | branches: [master] 12 | pull_request: 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | name: test 17 | jobs: 18 | required: 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 15 21 | name: ubuntu / ${{ matrix.toolchain }} 22 | strategy: 23 | matrix: 24 | # run on stable and beta to ensure that tests won't break on the next version of the rust 25 | # toolchain 26 | toolchain: [stable, beta] 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | - name: Install ${{ matrix.toolchain }} 32 | uses: dtolnay/rust-toolchain@master 33 | with: 34 | toolchain: ${{ matrix.toolchain }} 35 | - name: cargo generate-lockfile 36 | # enable this ci template to run regardless of whether the lockfile is checked in or not 37 | if: hashFiles('Cargo.lock') == '' 38 | run: cargo generate-lockfile 39 | # https://twitter.com/jonhoo/status/1571290371124260865 40 | - name: cargo test --locked 41 | run: cargo test --locked --all-features --all-targets 42 | # https://github.com/rust-lang/cargo/issues/6669 43 | - name: cargo test --doc 44 | run: cargo test --locked --all-features --doc 45 | # run stress tests serially, as they individually spawn many threads to provoke contention 46 | - name: stress tests 47 | run: cargo test -- --ignored --test-threads 1 48 | os-check: 49 | # run cargo test on mac and windows 50 | runs-on: ${{ matrix.os }} 51 | timeout-minutes: 15 52 | name: ${{ matrix.os }} / stable 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | os: [macos-latest, windows-latest] 57 | steps: 58 | - uses: actions/checkout@v4 59 | with: 60 | submodules: true 61 | - name: Install stable 62 | uses: dtolnay/rust-toolchain@stable 63 | - name: cargo generate-lockfile 64 | if: hashFiles('Cargo.lock') == '' 65 | run: cargo generate-lockfile 66 | - name: cargo test 67 | run: cargo test --locked --all-features --all-targets 68 | - name: stress tests 69 | run: cargo test -- --ignored --test-threads 1 70 | stress: 71 | # run resize stress tests on linux and mac 72 | runs-on: ${{ matrix.os }} 73 | timeout-minutes: 15 74 | name: ${{ matrix.os }} / stress 75 | strategy: 76 | fail-fast: false 77 | matrix: 78 | os: [ubuntu-latest, macos-latest] 79 | steps: 80 | - uses: actions/checkout@v4 81 | with: 82 | submodules: true 83 | - name: Install stable 84 | uses: dtolnay/rust-toolchain@stable 85 | - name: cargo generate-lockfile 86 | if: hashFiles('Cargo.lock') == '' 87 | run: cargo generate-lockfile 88 | - name: stress tests 89 | run: cargo test --locked -- --ignored --test-threads 1 90 | env: 91 | RUSTFLAGS: "--cfg papaya_stress" 92 | RUST_MIN_STACK: 8192000 # avoid stack overflows on macOS 93 | -------------------------------------------------------------------------------- /.github/workflows/safety.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs checks for unsafe code. In crates that don't have any unsafe code, this can be 2 | # removed. Runs: 3 | # - miri - detects undefined behavior and memory leaks 4 | # - address sanitizer - detects memory errors 5 | # - leak sanitizer - detects memory leaks 6 | # See check.yml for information about how the concurrency cancellation and workflow triggering works 7 | permissions: 8 | contents: read 9 | on: 10 | push: 11 | branches: [master] 12 | pull_request: 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | name: safety 17 | jobs: 18 | sanitizers: 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 15 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | submodules: true 25 | - name: Install nightly 26 | uses: dtolnay/rust-toolchain@nightly 27 | - run: | 28 | # to get the symbolizer for debug symbol resolution 29 | sudo apt install llvm 30 | # to fix buggy leak analyzer: 31 | # https://github.com/japaric/rust-san#unrealiable-leaksanitizer 32 | # ensure there's a profile.dev section 33 | if ! grep -qE '^[ \t]*[profile.dev]' Cargo.toml; then 34 | echo >> Cargo.toml 35 | echo '[profile.dev]' >> Cargo.toml 36 | fi 37 | # remove pre-existing opt-levels in profile.dev 38 | sed -i '/^\s*\[profile.dev\]/,/^\s*\[/ {/^\s*opt-level/d}' Cargo.toml 39 | # now set opt-level to 1 40 | sed -i '/^\s*\[profile.dev\]/a opt-level = 1' Cargo.toml 41 | cat Cargo.toml 42 | name: Enable debug symbols 43 | - name: cargo test -Zsanitizer=address 44 | # only --lib --tests b/c of https://github.com/rust-lang/rust/issues/53945 45 | run: cargo test --lib --tests --all-features --target x86_64-unknown-linux-gnu 46 | env: 47 | ASAN_OPTIONS: "detect_odr_violation=0:detect_leaks=0" 48 | RUSTFLAGS: "-Z sanitizer=address --cfg papaya_asan" 49 | - name: stress tests -Zsanitizer=address 50 | run: cargo test --lib --tests --all-features --target x86_64-unknown-linux-gnu -- --ignored --test-threads 1 51 | env: 52 | ASAN_OPTIONS: "detect_odr_violation=0:detect_leaks=0" 53 | RUSTFLAGS: "-Z sanitizer=address --cfg papaya_asan" 54 | - name: cargo test -Zsanitizer=leak 55 | if: always() 56 | run: cargo test --all-features --target x86_64-unknown-linux-gnu 57 | env: 58 | LSAN_OPTIONS: "suppressions=lsan-suppressions.txt" 59 | RUSTFLAGS: "-Z sanitizer=leak" 60 | - name: stress tests -Zsanitizer=leak 61 | if: always() 62 | run: cargo test --all-features --target x86_64-unknown-linux-gnu -- --ignored --test-threads 1 63 | env: 64 | LSAN_OPTIONS: "suppressions=lsan-suppressions.txt" 65 | RUSTFLAGS: "-Z sanitizer=leak" 66 | miri: 67 | runs-on: ubuntu-latest 68 | timeout-minutes: 15 69 | steps: 70 | - uses: actions/checkout@v4 71 | with: 72 | submodules: true 73 | - run: | 74 | echo "NIGHTLY=nightly-$(curl -s https://rust-lang.github.io/rustup-components-history/x86_64-unknown-linux-gnu/miri)" >> $GITHUB_ENV 75 | - name: Install ${{ env.NIGHTLY }} 76 | uses: dtolnay/rust-toolchain@master 77 | with: 78 | toolchain: ${{ env.NIGHTLY }} 79 | components: miri 80 | - name: cargo miri test 81 | run: cargo miri test -- --ignored 82 | env: 83 | # need this until feature(strict_provenance_atomic_ptr) is stabilized 84 | MIRIFLAGS: "-Zmiri-permissive-provenance" 85 | -------------------------------------------------------------------------------- /src/raw/utils/tagged.rs: -------------------------------------------------------------------------------- 1 | use std::mem::align_of; 2 | use std::sync::atomic::{AtomicPtr, Ordering}; 3 | 4 | // Polyfill for the unstable strict-provenance APIs. 5 | #[allow(clippy::missing_safety_doc)] 6 | #[allow(dead_code)] // `strict_provenance` has stabilized on nightly. 7 | pub unsafe trait StrictProvenance: Sized { 8 | fn addr(self) -> usize; 9 | fn map_addr(self, f: impl FnOnce(usize) -> usize) -> Self; 10 | fn unpack(self) -> Tagged 11 | where 12 | T: Unpack; 13 | } 14 | 15 | // Unpack a tagged pointer. 16 | pub trait Unpack: Sized { 17 | // A mask for the pointer tag bits. 18 | const MASK: usize; 19 | 20 | // This constant, if used, will fail to compile if T doesn't have an alignment 21 | // that guarantees all valid pointers have zero in the bits excluded by T::MASK. 22 | const ASSERT_ALIGNMENT: () = assert!(align_of::() > !Self::MASK); 23 | } 24 | 25 | unsafe impl StrictProvenance for *mut T { 26 | #[inline(always)] 27 | fn addr(self) -> usize { 28 | self as usize 29 | } 30 | 31 | #[inline(always)] 32 | fn map_addr(self, f: impl FnOnce(usize) -> usize) -> Self { 33 | f(self.addr()) as Self 34 | } 35 | 36 | #[inline(always)] 37 | fn unpack(self) -> Tagged 38 | where 39 | T: Unpack, 40 | { 41 | let () = T::ASSERT_ALIGNMENT; 42 | Tagged { 43 | raw: self, 44 | ptr: self.map_addr(|addr| addr & T::MASK), 45 | } 46 | } 47 | } 48 | 49 | // An unpacked tagged pointer. 50 | pub struct Tagged { 51 | // The raw tagged pointer. 52 | pub raw: *mut T, 53 | 54 | // The untagged pointer. 55 | pub ptr: *mut T, 56 | } 57 | 58 | // Creates a `Tagged` from an untagged pointer. 59 | #[inline] 60 | pub fn untagged(value: *mut T) -> Tagged { 61 | Tagged { 62 | raw: value, 63 | ptr: value, 64 | } 65 | } 66 | 67 | impl Tagged 68 | where 69 | T: Unpack, 70 | { 71 | // Returns the tag portion of this pointer. 72 | #[inline] 73 | pub fn tag(self) -> usize { 74 | self.raw.addr() & !T::MASK 75 | } 76 | 77 | // Maps the tag of this pointer. 78 | #[inline] 79 | pub fn map_tag(self, f: impl FnOnce(usize) -> usize) -> Self { 80 | Tagged { 81 | raw: self.raw.map_addr(f), 82 | ptr: self.ptr, 83 | } 84 | } 85 | } 86 | 87 | impl Copy for Tagged {} 88 | 89 | impl Clone for Tagged { 90 | fn clone(&self) -> Self { 91 | *self 92 | } 93 | } 94 | 95 | // Polyfill for the unstable `atomic_ptr_strict_provenance` APIs. 96 | pub trait AtomicPtrFetchOps { 97 | fn fetch_or(&self, value: usize, ordering: Ordering) -> *mut T; 98 | } 99 | 100 | impl AtomicPtrFetchOps for AtomicPtr { 101 | #[inline] 102 | fn fetch_or(&self, value: usize, ordering: Ordering) -> *mut T { 103 | #[cfg(not(miri))] 104 | { 105 | use std::sync::atomic::AtomicUsize; 106 | 107 | // Safety: `AtomicPtr` and `AtomicUsize` are identical in terms 108 | // of memory layout. This operation is technically invalid in that 109 | // it loses provenance, but there is no stable alternative. 110 | unsafe { &*(self as *const AtomicPtr as *const AtomicUsize) } 111 | .fetch_or(value, ordering) as *mut T 112 | } 113 | 114 | // Avoid ptr2int under Miri. 115 | #[cfg(miri)] 116 | { 117 | // Returns the ordering for the read in an RMW operation. 118 | const fn read_ordering(ordering: Ordering) -> Ordering { 119 | match ordering { 120 | Ordering::SeqCst => Ordering::SeqCst, 121 | Ordering::AcqRel => Ordering::Acquire, 122 | _ => Ordering::Relaxed, 123 | } 124 | } 125 | 126 | self.fetch_update(ordering, read_ordering(ordering), |ptr| { 127 | Some(ptr.map_addr(|addr| addr | value)) 128 | }) 129 | .unwrap() 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs whenever a PR is opened or updated, or a commit is pushed to main. It runs 2 | # several checks: 3 | # - fmt: checks that the code is formatted according to rustfmt 4 | # - clippy: checks that the code does not contain any clippy warnings 5 | # - doc: checks that the code can be documented without errors 6 | # - hack: check combinations of feature flags 7 | # - msrv: check that the msrv specified in the crate is correct 8 | permissions: 9 | contents: read 10 | # This configuration allows maintainers of this repo to create a branch and pull request based on 11 | # the new branch. Restricting the push trigger to the main branch ensures that the PR only gets 12 | # built once. 13 | on: 14 | push: 15 | branches: [master] 16 | pull_request: 17 | # If new code is pushed to a PR branch, then cancel in progress workflows for that PR. Ensures that 18 | # we don't waste CI time, and returns results quicker https://github.com/jonhoo/rust-ci-conf/pull/5 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 21 | cancel-in-progress: true 22 | name: check 23 | jobs: 24 | fmt: 25 | runs-on: ubuntu-latest 26 | name: stable / fmt 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | - name: Install stable 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | components: rustfmt 35 | - name: cargo fmt --check 36 | run: cargo fmt --check 37 | clippy: 38 | runs-on: ubuntu-latest 39 | name: ${{ matrix.toolchain }} / clippy 40 | permissions: 41 | contents: read 42 | checks: write 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | # Get early warning of new lints which are regularly introduced in beta channels. 47 | toolchain: [stable, beta] 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: true 52 | - name: Install ${{ matrix.toolchain }} 53 | uses: dtolnay/rust-toolchain@master 54 | with: 55 | toolchain: ${{ matrix.toolchain }} 56 | components: clippy 57 | - name: cargo clippy 58 | uses: giraffate/clippy-action@v1 59 | with: 60 | reporter: 'github-pr-check' 61 | github_token: ${{ secrets.GITHUB_TOKEN }} 62 | semver: 63 | runs-on: ubuntu-latest 64 | name: semver 65 | steps: 66 | - uses: actions/checkout@v4 67 | with: 68 | submodules: true 69 | - name: Install stable 70 | uses: dtolnay/rust-toolchain@stable 71 | with: 72 | components: rustfmt 73 | - name: cargo-semver-checks 74 | uses: obi1kenobi/cargo-semver-checks-action@v2 75 | doc: 76 | # run docs generation on nightly rather than stable. This enables features like 77 | # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an 78 | # API be documented as only available in some specific platforms. 79 | runs-on: ubuntu-latest 80 | name: nightly / doc 81 | steps: 82 | - uses: actions/checkout@v4 83 | with: 84 | submodules: true 85 | - name: Install nightly 86 | uses: dtolnay/rust-toolchain@nightly 87 | - name: cargo doc 88 | run: cargo doc --no-deps --all-features 89 | env: 90 | RUSTDOCFLAGS: --cfg docsrs 91 | hack: 92 | # cargo-hack checks combinations of feature flags to ensure that features are all additive 93 | # which is required for feature unification 94 | runs-on: ubuntu-latest 95 | name: ubuntu / stable / features 96 | steps: 97 | - uses: actions/checkout@v4 98 | with: 99 | submodules: true 100 | - name: Install stable 101 | uses: dtolnay/rust-toolchain@stable 102 | - name: cargo install cargo-hack 103 | uses: taiki-e/install-action@cargo-hack 104 | # intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4 105 | # --feature-powerset runs for every combination of features 106 | - name: cargo hack 107 | run: cargo hack --feature-powerset check 108 | msrv: 109 | # check that we can build using the minimal rust version that is specified by this crate 110 | runs-on: ubuntu-latest 111 | # we use a matrix here just because env can't be used in job names 112 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability 113 | strategy: 114 | matrix: 115 | msrv: ["1.72.0"] 116 | name: ubuntu / ${{ matrix.msrv }} 117 | steps: 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: true 121 | - name: Install ${{ matrix.msrv }} 122 | uses: dtolnay/rust-toolchain@master 123 | with: 124 | toolchain: ${{ matrix.msrv }} 125 | - name: cargo +${{ matrix.msrv }} check 126 | run: cargo check 127 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/std.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | use std::collections::hash_map::Entry; 5 | 6 | use arbitrary::unstructured::Int; 7 | use arbitrary::{Arbitrary, Unstructured}; 8 | use papaya::{Guard, HashMap as PapayaHashMap, HashMapRef}; 9 | use std::collections::HashMap as StdHashMap; 10 | use std::hash::{BuildHasher, Hash}; 11 | use std::ops::Add; 12 | 13 | #[derive(Debug, Arbitrary)] 14 | enum Operation { 15 | Insert(K, V), 16 | Remove(K), 17 | Get(K), 18 | Contains(K), 19 | Clear, 20 | Len, 21 | IsEmpty, 22 | Update(K, V), 23 | UpdateOrInsert(K, V, V), 24 | GetOrInsert(K, V), 25 | Compute(K), 26 | } 27 | 28 | #[derive(Debug, Arbitrary)] 29 | struct FuzzInput { 30 | operations: Vec>, 31 | } 32 | 33 | fn fuzz_hashmap(input: FuzzInput) { 34 | let mut std_map = StdHashMap::new(); 35 | let papaya_raw = PapayaHashMap::new(); 36 | let papaya_map = papaya_raw.pin(); 37 | 38 | for op in input.operations { 39 | match op { 40 | Operation::Insert(k, v) => { 41 | let std_result = std_map.insert(k.clone(), v.clone()); 42 | let papaya_result = papaya_map.insert(k, v); 43 | assert_eq!(std_result.as_ref(), papaya_result); 44 | } 45 | Operation::Remove(k) => { 46 | let std_result = std_map.remove(&k); 47 | let papaya_result = papaya_map.remove(&k); 48 | assert_eq!(std_result.as_ref(), papaya_result); 49 | } 50 | Operation::Get(k) => { 51 | let std_result = std_map.get(&k); 52 | let papaya_result = papaya_map.get(&k); 53 | assert_eq!(std_result, papaya_result); 54 | } 55 | Operation::Contains(k) => { 56 | let std_result = std_map.contains_key(&k); 57 | let papaya_result = papaya_map.contains_key(&k); 58 | assert_eq!(std_result, papaya_result); 59 | } 60 | Operation::Clear => { 61 | std_map.clear(); 62 | papaya_map.clear(); 63 | } 64 | Operation::Len => { 65 | assert_eq!(std_map.len(), papaya_map.len()); 66 | } 67 | Operation::IsEmpty => { 68 | assert_eq!(std_map.is_empty(), papaya_map.is_empty()); 69 | } 70 | Operation::Update(k, v) => { 71 | let std_result = std_map.get_mut(&k).map(|e| { 72 | *e = e.wrapping_add(v); 73 | e as &u32 74 | }); 75 | let papaya_result = papaya_map.update(k, |e| e.wrapping_add(v)); 76 | assert_eq!(std_result, papaya_result); 77 | } 78 | Operation::UpdateOrInsert(k, v, default) => { 79 | let std_result = std_map 80 | .entry(k.clone()) 81 | .and_modify(|e| *e = e.wrapping_add(v)) 82 | .or_insert(default.clone()); 83 | let papaya_result = papaya_map.update_or_insert(k, |e| e.wrapping_add(v), default); 84 | assert_eq!(std_result, papaya_result); 85 | } 86 | Operation::GetOrInsert(k, v) => { 87 | let std_result = std_map.entry(k.clone()).or_insert(v.clone()); 88 | let papaya_result = papaya_map.get_or_insert(k, v); 89 | assert_eq!(std_result, papaya_result); 90 | } 91 | Operation::Compute(k) => compute(&mut std_map, &papaya_map, k), 92 | } 93 | } 94 | 95 | // Final consistency checks 96 | for (k, v) in std_map.iter() { 97 | let papaya_result = papaya_map.get(k); 98 | assert_eq!(Some(v), papaya_result); 99 | } 100 | assert_eq!(std_map.len(), papaya_map.len()); 101 | assert_eq!(std_map.is_empty(), papaya_map.is_empty()); 102 | } 103 | 104 | fn compute(std: &mut StdHashMap, papaya: &HashMapRef, k: u32) 105 | where 106 | S: BuildHasher, 107 | G: Guard, 108 | { 109 | match std.entry(k) { 110 | Entry::Occupied(mut entry) => { 111 | let value = entry.get(); 112 | if value % 2 == 0 { 113 | entry.remove(); 114 | } else { 115 | *entry.get_mut() = value.wrapping_add(1); 116 | } 117 | } 118 | Entry::Vacant(_) => { 119 | // Do nothing for non-existent keys 120 | } 121 | } 122 | 123 | let compute = |entry: Option<(&u32, &u32)>| match entry { 124 | // Remove the value if it is even. 125 | Some((_key, value)) if value % 2 == 0 => papaya::Operation::Remove, 126 | 127 | // Increment the value if it is odd. 128 | Some((_key, value)) => papaya::Operation::Insert(value.wrapping_add(1)), 129 | 130 | // Do nothing if the key does not exist 131 | None => papaya::Operation::Abort(()), 132 | }; 133 | papaya.compute(k, compute); 134 | } 135 | 136 | fuzz_target!(|data: FuzzInput| { 137 | fuzz_hashmap(data); 138 | }); 139 | -------------------------------------------------------------------------------- /src/raw/utils/parker.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::atomic::{AtomicPtr, AtomicU8, AtomicUsize, Ordering}; 3 | use std::sync::Mutex; 4 | use std::thread::{self, Thread}; 5 | 6 | // A simple thread parker. 7 | // 8 | // This parker is rarely used and relatively naive. Ideally this would just use a futex 9 | // but the hashmap needs to park on tagged pointer state so we would either need mixed-sized 10 | // atomic accesses (https://github.com/rust-lang/unsafe-code-guidelines/issues/345) which are 11 | // questionable, or 64-bit futexes, which are not available on most platforms. 12 | // 13 | // The parker implementation may be sharded and use intrusive lists if it is found to be 14 | // a bottleneck. 15 | #[derive(Default)] 16 | pub struct Parker { 17 | pending: AtomicUsize, 18 | state: Mutex, 19 | } 20 | 21 | #[derive(Default)] 22 | struct State { 23 | count: u64, 24 | threads: HashMap>, 25 | } 26 | 27 | impl Parker { 28 | // Block the current thread until the park condition is false. 29 | // 30 | // This method is guaranteed to establish happens-before with the unpark condition 31 | // before it returns. 32 | pub fn park(&self, atomic: &impl Atomic, should_park: impl Fn(T) -> bool) { 33 | let key = atomic as *const _ as usize; 34 | 35 | loop { 36 | // Announce our thread. 37 | // 38 | // This must be done before inserting our thread to prevent 39 | // incorrect decrements if we are unparked in-between inserting 40 | // the thread and incrementing the counter. 41 | // 42 | // Note that the `SeqCst` store here establishes a total order 43 | // with the `SeqCst` store that establishes the unpark condition. 44 | self.pending.fetch_add(1, Ordering::SeqCst); 45 | 46 | // Insert our thread into the parker. 47 | let id = { 48 | let state = &mut *self.state.lock().unwrap(); 49 | state.count += 1; 50 | 51 | let threads = state.threads.entry(key).or_default(); 52 | threads.insert(state.count, thread::current()); 53 | 54 | state.count 55 | }; 56 | 57 | // Check the park condition. 58 | // 59 | // Note that `SeqCst` is necessary here to participate in the 60 | // total order established above. 61 | if !should_park(atomic.load(Ordering::SeqCst)) { 62 | // Don't need to park, remove our thread if it wasn't already unparked. 63 | let thread = { 64 | let mut state = self.state.lock().unwrap(); 65 | state 66 | .threads 67 | .get_mut(&key) 68 | .and_then(|threads| threads.remove(&id)) 69 | }; 70 | 71 | if thread.is_some() { 72 | self.pending.fetch_sub(1, Ordering::Relaxed); 73 | } 74 | 75 | return; 76 | } 77 | 78 | // Park until we are unparked. 79 | loop { 80 | thread::park(); 81 | 82 | let mut state = self.state.lock().unwrap(); 83 | if !state 84 | .threads 85 | .get_mut(&key) 86 | .is_some_and(|threads| threads.contains_key(&id)) 87 | { 88 | break; 89 | } 90 | } 91 | 92 | // Ensure we were unparked for the correct reason. 93 | // 94 | // Establish happens-before with the unpark condition. 95 | if !should_park(atomic.load(Ordering::Acquire)) { 96 | return; 97 | } 98 | } 99 | } 100 | 101 | // Unpark all threads waiting on the given atomic. 102 | // 103 | // Note that any modifications must be `SeqCst` to be visible to unparked threads. 104 | pub fn unpark(&self, atomic: &impl Atomic) { 105 | let key = atomic as *const _ as usize; 106 | 107 | // Fast-path, no one waiting to be unparked. 108 | // 109 | // Note that `SeqCst` is necessary here to participate in the 110 | // total order established between the increment of `self.pending` 111 | // in `park` and the `SeqCst` store of the unpark condition by 112 | // the caller. 113 | if self.pending.load(Ordering::SeqCst) == 0 { 114 | return; 115 | } 116 | 117 | // Remove and unpark any threads waiting on the atomic. 118 | let threads = { 119 | let mut state = self.state.lock().unwrap(); 120 | state.threads.remove(&key) 121 | }; 122 | 123 | if let Some(threads) = threads { 124 | self.pending.fetch_sub(threads.len(), Ordering::Relaxed); 125 | 126 | for (_, thread) in threads { 127 | thread.unpark(); 128 | } 129 | } 130 | } 131 | } 132 | 133 | /// A generic atomic variable. 134 | pub trait Atomic { 135 | /// Load the value using the given ordering. 136 | fn load(&self, ordering: Ordering) -> T; 137 | } 138 | 139 | impl Atomic<*mut T> for AtomicPtr { 140 | fn load(&self, ordering: Ordering) -> *mut T { 141 | self.load(ordering) 142 | } 143 | } 144 | 145 | impl Atomic for AtomicU8 { 146 | fn load(&self, ordering: Ordering) -> u8 { 147 | self.load(ordering) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /fuzz/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arbitrary" 7 | version = "1.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" 10 | dependencies = [ 11 | "derive_arbitrary", 12 | ] 13 | 14 | [[package]] 15 | name = "atomic-wait" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "a55b94919229f2c42292fd71ffa4b75e83193bffdd77b1e858cd55fd2d0b0ea8" 19 | dependencies = [ 20 | "libc", 21 | "windows-sys", 22 | ] 23 | 24 | [[package]] 25 | name = "cc" 26 | version = "1.1.0" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" 29 | dependencies = [ 30 | "jobserver", 31 | "libc", 32 | "once_cell", 33 | ] 34 | 35 | [[package]] 36 | name = "derive_arbitrary" 37 | version = "1.3.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" 40 | dependencies = [ 41 | "proc-macro2", 42 | "quote", 43 | "syn", 44 | ] 45 | 46 | [[package]] 47 | name = "jobserver" 48 | version = "0.1.31" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" 51 | dependencies = [ 52 | "libc", 53 | ] 54 | 55 | [[package]] 56 | name = "libc" 57 | version = "0.2.155" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 60 | 61 | [[package]] 62 | name = "libfuzzer-sys" 63 | version = "0.4.7" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" 66 | dependencies = [ 67 | "arbitrary", 68 | "cc", 69 | "once_cell", 70 | ] 71 | 72 | [[package]] 73 | name = "once_cell" 74 | version = "1.19.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 77 | 78 | [[package]] 79 | name = "papaya" 80 | version = "0.1.1" 81 | dependencies = [ 82 | "atomic-wait", 83 | "seize", 84 | ] 85 | 86 | [[package]] 87 | name = "papaya-fuzz" 88 | version = "0.0.0" 89 | dependencies = [ 90 | "arbitrary", 91 | "libfuzzer-sys", 92 | "papaya", 93 | ] 94 | 95 | [[package]] 96 | name = "proc-macro2" 97 | version = "1.0.86" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 100 | dependencies = [ 101 | "unicode-ident", 102 | ] 103 | 104 | [[package]] 105 | name = "quote" 106 | version = "1.0.36" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 109 | dependencies = [ 110 | "proc-macro2", 111 | ] 112 | 113 | [[package]] 114 | name = "seize" 115 | version = "0.4.4" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "88c33a118e1f9cf4b4b4c0a1dd4f7f703a64a0973697de977e307cd09daffff0" 118 | 119 | [[package]] 120 | name = "syn" 121 | version = "2.0.70" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" 124 | dependencies = [ 125 | "proc-macro2", 126 | "quote", 127 | "unicode-ident", 128 | ] 129 | 130 | [[package]] 131 | name = "unicode-ident" 132 | version = "1.0.12" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 135 | 136 | [[package]] 137 | name = "windows-sys" 138 | version = "0.42.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 141 | dependencies = [ 142 | "windows_aarch64_gnullvm", 143 | "windows_aarch64_msvc", 144 | "windows_i686_gnu", 145 | "windows_i686_msvc", 146 | "windows_x86_64_gnu", 147 | "windows_x86_64_gnullvm", 148 | "windows_x86_64_msvc", 149 | ] 150 | 151 | [[package]] 152 | name = "windows_aarch64_gnullvm" 153 | version = "0.42.2" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 156 | 157 | [[package]] 158 | name = "windows_aarch64_msvc" 159 | version = "0.42.2" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 162 | 163 | [[package]] 164 | name = "windows_i686_gnu" 165 | version = "0.42.2" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 168 | 169 | [[package]] 170 | name = "windows_i686_msvc" 171 | version = "0.42.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 174 | 175 | [[package]] 176 | name = "windows_x86_64_gnu" 177 | version = "0.42.2" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 180 | 181 | [[package]] 182 | name = "windows_x86_64_gnullvm" 183 | version = "0.42.2" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 186 | 187 | [[package]] 188 | name = "windows_x86_64_msvc" 189 | version = "0.42.2" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 192 | -------------------------------------------------------------------------------- /src/serde_impls.rs: -------------------------------------------------------------------------------- 1 | use serde::de::{MapAccess, SeqAccess, Visitor}; 2 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 3 | 4 | use std::fmt::{self, Formatter}; 5 | use std::hash::{BuildHasher, Hash}; 6 | use std::marker::PhantomData; 7 | 8 | use crate::{Guard, HashMap, HashMapRef, HashSet, HashSetRef}; 9 | 10 | struct MapVisitor { 11 | _marker: PhantomData>, 12 | } 13 | 14 | impl Serialize for HashMapRef<'_, K, V, S, G> 15 | where 16 | K: Serialize + Hash + Eq, 17 | V: Serialize, 18 | G: Guard, 19 | S: BuildHasher, 20 | { 21 | fn serialize(&self, serializer: Sr) -> Result 22 | where 23 | Sr: Serializer, 24 | { 25 | serializer.collect_map(self) 26 | } 27 | } 28 | 29 | impl Serialize for HashMap 30 | where 31 | K: Serialize + Hash + Eq, 32 | V: Serialize, 33 | S: BuildHasher, 34 | { 35 | fn serialize(&self, serializer: Sr) -> Result 36 | where 37 | Sr: Serializer, 38 | { 39 | self.pin().serialize(serializer) 40 | } 41 | } 42 | 43 | impl<'de, K, V, S> Deserialize<'de> for HashMap 44 | where 45 | K: Deserialize<'de> + Hash + Eq, 46 | V: Deserialize<'de>, 47 | S: Default + BuildHasher, 48 | { 49 | fn deserialize(deserializer: D) -> Result 50 | where 51 | D: Deserializer<'de>, 52 | { 53 | deserializer.deserialize_map(MapVisitor::new()) 54 | } 55 | } 56 | 57 | impl MapVisitor { 58 | pub(crate) fn new() -> Self { 59 | Self { 60 | _marker: PhantomData, 61 | } 62 | } 63 | } 64 | 65 | impl<'de, K, V, S> Visitor<'de> for MapVisitor 66 | where 67 | K: Deserialize<'de> + Hash + Eq, 68 | V: Deserialize<'de>, 69 | S: Default + BuildHasher, 70 | { 71 | type Value = HashMap; 72 | 73 | fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { 74 | write!(f, "a map") 75 | } 76 | 77 | fn visit_map(self, mut access: M) -> Result 78 | where 79 | M: MapAccess<'de>, 80 | { 81 | let values = match access.size_hint() { 82 | Some(size) => HashMap::with_capacity_and_hasher(size, S::default()), 83 | None => HashMap::default(), 84 | }; 85 | 86 | { 87 | let values = values.pin(); 88 | while let Some((key, value)) = access.next_entry()? { 89 | values.insert(key, value); 90 | } 91 | } 92 | 93 | Ok(values) 94 | } 95 | } 96 | 97 | struct SetVisitor { 98 | _marker: PhantomData>, 99 | } 100 | 101 | impl Serialize for HashSetRef<'_, K, S, G> 102 | where 103 | K: Serialize + Hash + Eq, 104 | G: Guard, 105 | S: BuildHasher, 106 | { 107 | fn serialize(&self, serializer: Sr) -> Result 108 | where 109 | Sr: Serializer, 110 | { 111 | serializer.collect_seq(self) 112 | } 113 | } 114 | 115 | impl Serialize for HashSet 116 | where 117 | K: Serialize + Hash + Eq, 118 | S: BuildHasher, 119 | { 120 | fn serialize(&self, serializer: Sr) -> Result 121 | where 122 | Sr: Serializer, 123 | { 124 | self.pin().serialize(serializer) 125 | } 126 | } 127 | 128 | impl<'de, K, S> Deserialize<'de> for HashSet 129 | where 130 | K: Deserialize<'de> + Hash + Eq, 131 | S: Default + BuildHasher, 132 | { 133 | fn deserialize(deserializer: D) -> Result 134 | where 135 | D: Deserializer<'de>, 136 | { 137 | deserializer.deserialize_seq(SetVisitor::new()) 138 | } 139 | } 140 | 141 | impl SetVisitor { 142 | pub(crate) fn new() -> Self { 143 | Self { 144 | _marker: PhantomData, 145 | } 146 | } 147 | } 148 | 149 | impl<'de, K, S> Visitor<'de> for SetVisitor 150 | where 151 | K: Deserialize<'de> + Hash + Eq, 152 | S: Default + BuildHasher, 153 | { 154 | type Value = HashSet; 155 | 156 | fn expecting(&self, f: &mut Formatter<'_>) -> fmt::Result { 157 | write!(f, "a set") 158 | } 159 | 160 | fn visit_seq(self, mut access: M) -> Result 161 | where 162 | M: SeqAccess<'de>, 163 | { 164 | let values = match access.size_hint() { 165 | Some(size) => HashSet::with_capacity_and_hasher(size, S::default()), 166 | None => HashSet::default(), 167 | }; 168 | 169 | { 170 | let values = values.pin(); 171 | while let Some(key) = access.next_element()? { 172 | values.insert(key); 173 | } 174 | } 175 | 176 | Ok(values) 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod test { 182 | use crate::HashMap; 183 | use crate::HashSet; 184 | 185 | #[test] 186 | fn test_map() { 187 | let map: HashMap = HashMap::new(); 188 | let guard = map.guard(); 189 | 190 | map.insert(0, 4, &guard); 191 | map.insert(1, 3, &guard); 192 | map.insert(2, 2, &guard); 193 | map.insert(3, 1, &guard); 194 | map.insert(4, 0, &guard); 195 | 196 | let serialized = serde_json::to_string(&map).unwrap(); 197 | let deserialized = serde_json::from_str(&serialized).unwrap(); 198 | 199 | assert_eq!(map, deserialized); 200 | } 201 | 202 | #[test] 203 | fn test_set() { 204 | let map: HashSet = HashSet::new(); 205 | let guard = map.guard(); 206 | 207 | map.insert(0, &guard); 208 | map.insert(1, &guard); 209 | map.insert(2, &guard); 210 | map.insert(3, &guard); 211 | map.insert(4, &guard); 212 | 213 | let serialized = serde_json::to_string(&map).unwrap(); 214 | let deserialized = serde_json::from_str(&serialized).unwrap(); 215 | 216 | assert_eq!(map, deserialized); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/raw/alloc.rs: -------------------------------------------------------------------------------- 1 | use std::alloc::Layout; 2 | use std::marker::PhantomData; 3 | use std::sync::atomic::{AtomicPtr, AtomicU8, Ordering}; 4 | use std::{alloc, mem, ptr}; 5 | 6 | use super::{probe, State}; 7 | 8 | // A hash-table laid out in a single allocation. 9 | // 10 | // Note that the `PhantomData` ensures that the hash-table is invariant 11 | // with respect to `T`, as this struct is stored behind an `AtomicPtr`. 12 | #[repr(transparent)] 13 | pub struct RawTable(u8, PhantomData); 14 | 15 | // The layout of the table allocation. 16 | #[repr(C)] 17 | struct TableLayout { 18 | /// A mask to get an index into the table from a hash. 19 | mask: usize, 20 | 21 | /// The maximum probe limit for this table. 22 | limit: usize, 23 | 24 | /// State for the table resize. 25 | state: State, 26 | 27 | /// An array of metadata for each entry. 28 | meta: [AtomicU8; 0], 29 | 30 | /// An array of entries. 31 | entries: [AtomicPtr; 0], 32 | } 33 | 34 | // Manages a table allocation. 35 | #[repr(C)] 36 | pub struct Table { 37 | /// A mask to get an index into the table from a hash. 38 | pub mask: usize, 39 | 40 | /// The maximum probe limit for this table. 41 | pub limit: usize, 42 | 43 | // The raw table allocation. 44 | // 45 | // Invariant: This pointer is initialized and valid for reads and writes. 46 | pub raw: *mut RawTable, 47 | } 48 | 49 | impl Copy for Table {} 50 | 51 | impl Clone for Table { 52 | fn clone(&self) -> Self { 53 | *self 54 | } 55 | } 56 | 57 | impl Table { 58 | // Allocate a table with the provided length and collector. 59 | pub fn alloc(len: usize) -> Table { 60 | assert!(len.is_power_of_two()); 61 | 62 | // Pad the meta table to fulfill the alignment requirement of an entry. 63 | let len = len.max(mem::align_of::>()); 64 | let mask = len - 1; 65 | let limit = probe::limit(len); 66 | 67 | let layout = Table::::layout(len); 68 | 69 | // Allocate the table, zeroing the entries. 70 | // 71 | // Safety: The layout for is guaranteed to be non-zero. 72 | let ptr = unsafe { alloc::alloc_zeroed(layout) }; 73 | if ptr.is_null() { 74 | alloc::handle_alloc_error(layout); 75 | } 76 | 77 | // Safety: We just allocated the pointer and ensured it is non-null above. 78 | unsafe { 79 | // Write the table state. 80 | ptr.cast::>().write(TableLayout { 81 | mask, 82 | limit, 83 | meta: [], 84 | entries: [], 85 | state: State::default(), 86 | }); 87 | 88 | // Initialize the meta table. 89 | ptr.add(mem::size_of::>()) 90 | .cast::() 91 | .write_bytes(super::meta::EMPTY, len); 92 | } 93 | 94 | Table { 95 | mask, 96 | limit, 97 | // Invariant: We allocated and initialized the allocation above. 98 | raw: ptr.cast::>(), 99 | } 100 | } 101 | 102 | // Creates a `Table` from a raw pointer. 103 | // 104 | // # Safety 105 | // 106 | // The pointer must either be null, or a valid pointer created with `Table::alloc`. 107 | #[inline] 108 | pub unsafe fn from_raw(raw: *mut RawTable) -> Table { 109 | if raw.is_null() { 110 | return Table { 111 | raw, 112 | mask: 0, 113 | limit: 0, 114 | }; 115 | } 116 | 117 | // Safety: The caller guarantees that the pointer is valid. 118 | let layout = unsafe { &*raw.cast::>() }; 119 | 120 | Table { 121 | raw, 122 | mask: layout.mask, 123 | limit: layout.limit, 124 | } 125 | } 126 | 127 | // Returns the metadata entry at the given index. 128 | // 129 | // # Safety 130 | // 131 | // The index must be in-bounds for the length of the table. 132 | #[inline] 133 | pub unsafe fn meta(&self, i: usize) -> &AtomicU8 { 134 | debug_assert!(i < self.len()); 135 | 136 | // Safety: The caller guarantees the index is in-bounds. 137 | unsafe { 138 | let meta = self.raw.add(mem::size_of::>()); 139 | &*meta.cast::().add(i) 140 | } 141 | } 142 | 143 | // Returns the entry at the given index. 144 | // 145 | // # Safety 146 | // 147 | // The index must be in-bounds for the length of the table. 148 | #[inline] 149 | pub unsafe fn entry(&self, i: usize) -> &AtomicPtr { 150 | debug_assert!(i < self.len()); 151 | 152 | // Safety: The caller guarantees the index is in-bounds. 153 | unsafe { 154 | let meta = self.raw.add(mem::size_of::>()); 155 | let entries = meta.add(self.len()).cast::>(); 156 | &*entries.add(i) 157 | } 158 | } 159 | 160 | /// Returns the length of the table. 161 | #[inline] 162 | pub fn len(&self) -> usize { 163 | self.mask + 1 164 | } 165 | 166 | // Returns a reference to the table state. 167 | #[inline] 168 | pub fn state(&self) -> &State { 169 | // Safety: The raw table pointer is always valid for reads and writes. 170 | unsafe { &(*self.raw.cast::>()).state } 171 | } 172 | 173 | // Returns a mutable reference to the table state. 174 | #[inline] 175 | pub fn state_mut(&mut self) -> &mut State { 176 | // Safety: The raw table pointer is always valid for reads and writes. 177 | unsafe { &mut (*self.raw.cast::>()).state } 178 | } 179 | 180 | // Returns a pointer to the next table, if it has already been created. 181 | #[inline] 182 | pub fn next_table(&self) -> Option { 183 | let next = self.state().next.load(Ordering::Acquire); 184 | 185 | if !next.is_null() { 186 | // Safety: We verified that the pointer is non-null, and the 187 | // next pointer is otherwise a valid pointer to a table allocation. 188 | return unsafe { Some(Table::from_raw(next)) }; 189 | } 190 | 191 | None 192 | } 193 | 194 | // Deallocate the table. 195 | // 196 | // # Safety 197 | // 198 | // The table may not be accessed in any way after this method is 199 | // called. 200 | pub unsafe fn dealloc(table: Table) { 201 | let layout = Self::layout(table.len()); 202 | 203 | // Safety: The raw table pointer is valid and allocated with `alloc::alloc_zeroed`. 204 | // Additionally, the caller guarantees that the allocation will not be accessed after 205 | // this point. 206 | unsafe { 207 | ptr::drop_in_place(table.raw.cast::>()); 208 | alloc::dealloc(table.raw.cast::(), layout); 209 | }; 210 | } 211 | 212 | // Returns the non-zero layout for a table allocation. 213 | fn layout(len: usize) -> Layout { 214 | let size = mem::size_of::>() 215 | + (mem::size_of::() * len) // Metadata table. 216 | + (mem::size_of::>() * len); // Entry pointers. 217 | // 218 | Layout::from_size_align(size, mem::align_of::>()).unwrap() 219 | } 220 | } 221 | 222 | #[test] 223 | fn layout() { 224 | unsafe { 225 | let table: Table = Table::alloc(4); 226 | let table: Table = Table::from_raw(table.raw); 227 | 228 | // The capacity is padded for pointer alignment. 229 | assert_eq!(table.mask, 7); 230 | assert_eq!(table.len(), 8); 231 | Table::dealloc(table); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /tests/cuckoo.rs: -------------------------------------------------------------------------------- 1 | // Adapted from: https://github.com/jonhoo/flurry/blob/main/tests/cuckoo/stress.rs 2 | 3 | use papaya::{HashMap, ResizeMode}; 4 | 5 | use rand::distributions::{Distribution, Uniform}; 6 | 7 | use std::sync::atomic::Ordering; 8 | use std::sync::Mutex; 9 | use std::sync::{atomic::AtomicBool, Arc}; 10 | use std::thread; 11 | 12 | #[cfg(not(miri))] 13 | mod cfg { 14 | /// Number of keys and values to work with. 15 | pub const NUM_KEYS: usize = 1 << 14; 16 | /// Number of threads that should be started. 17 | pub const NUM_THREADS: usize = 4; 18 | /// How long the stress test will run (in milliseconds). 19 | pub const TEST_LEN: u64 = 10_000; 20 | } 21 | 22 | #[cfg(miri)] 23 | mod cfg { 24 | /// Number of keys and values to work with. 25 | pub const NUM_KEYS: usize = 1 << 10; 26 | /// Number of threads that should be started. 27 | pub const NUM_THREADS: usize = 4; 28 | /// How long the stress test will run (in milliseconds). 29 | pub const TEST_LEN: u64 = 5000; 30 | } 31 | 32 | type Key = usize; 33 | type Value = usize; 34 | 35 | struct Environment { 36 | table1: HashMap, 37 | table2: HashMap, 38 | keys: Vec, 39 | vals1: Mutex>, 40 | vals2: Mutex>, 41 | ind_dist: Uniform, 42 | val_dist1: Uniform, 43 | val_dist2: Uniform, 44 | in_table: Mutex>, 45 | in_use: Mutex>, 46 | finished: AtomicBool, 47 | } 48 | 49 | impl Environment { 50 | pub fn new() -> Self { 51 | let mut keys = Vec::with_capacity(cfg::NUM_KEYS); 52 | let mut in_use = Vec::with_capacity(cfg::NUM_KEYS); 53 | 54 | for i in 0..cfg::NUM_KEYS { 55 | keys.push(i); 56 | in_use.push(AtomicBool::new(false)); 57 | } 58 | 59 | Self { 60 | table1: HashMap::new(), 61 | table2: HashMap::new(), 62 | keys, 63 | vals1: Mutex::new(vec![0usize; cfg::NUM_KEYS]), 64 | vals2: Mutex::new(vec![0usize; cfg::NUM_KEYS]), 65 | ind_dist: Uniform::from(0..cfg::NUM_KEYS - 1), 66 | val_dist1: Uniform::from(Value::min_value()..Value::max_value()), 67 | val_dist2: Uniform::from(Value::min_value()..Value::max_value()), 68 | in_table: Mutex::new(vec![false; cfg::NUM_KEYS]), 69 | in_use: Mutex::new(in_use), 70 | finished: AtomicBool::new(false), 71 | } 72 | } 73 | } 74 | 75 | fn stress_insert_thread(env: Arc) { 76 | let mut rng = rand::thread_rng(); 77 | let guard1 = env.table1.guard(); 78 | let guard2 = env.table2.guard(); 79 | 80 | while !env.finished.load(Ordering::SeqCst) { 81 | let idx = env.ind_dist.sample(&mut rng); 82 | let in_use = env.in_use.lock().unwrap(); 83 | if (*in_use)[idx] 84 | .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed) 85 | .is_ok() 86 | { 87 | let key = env.keys[idx]; 88 | let val1 = env.val_dist1.sample(&mut rng); 89 | let val2 = env.val_dist2.sample(&mut rng); 90 | let res1 = if !env.table1.contains_key(&key, &guard1) { 91 | env.table1 92 | .insert(key, val1, &guard1) 93 | .map_or(true, |_| false) 94 | } else { 95 | false 96 | }; 97 | let res2 = if !env.table2.contains_key(&key, &guard2) { 98 | env.table2 99 | .insert(key, val2, &guard2) 100 | .map_or(true, |_| false) 101 | } else { 102 | false 103 | }; 104 | let mut in_table = env.in_table.lock().unwrap(); 105 | assert_ne!(res1, (*in_table)[idx]); 106 | assert_ne!(res2, (*in_table)[idx]); 107 | if res1 { 108 | assert_eq!(Some(&val1), env.table1.get(&key, &guard1)); 109 | assert_eq!(Some(&val2), env.table2.get(&key, &guard2)); 110 | let mut vals1 = env.vals1.lock().unwrap(); 111 | let mut vals2 = env.vals2.lock().unwrap(); 112 | (*vals1)[idx] = val1; 113 | (*vals2)[idx] = val2; 114 | (*in_table)[idx] = true; 115 | } 116 | (*in_use)[idx].swap(false, Ordering::SeqCst); 117 | } 118 | } 119 | } 120 | 121 | fn stress_delete_thread(env: Arc) { 122 | let mut rng = rand::thread_rng(); 123 | let guard1 = env.table1.guard(); 124 | let guard2 = env.table2.guard(); 125 | 126 | while !env.finished.load(Ordering::SeqCst) { 127 | let idx = env.ind_dist.sample(&mut rng); 128 | let in_use = env.in_use.lock().unwrap(); 129 | if (*in_use)[idx] 130 | .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed) 131 | .is_ok() 132 | { 133 | let key = env.keys[idx]; 134 | let res1 = env.table1.remove(&key, &guard1).map_or(false, |_| true); 135 | let res2 = env.table2.remove(&key, &guard2).map_or(false, |_| true); 136 | let mut in_table = env.in_table.lock().unwrap(); 137 | assert_eq!(res1, (*in_table)[idx]); 138 | assert_eq!(res2, (*in_table)[idx]); 139 | if res1 { 140 | assert!(env.table1.get(&key, &guard1).is_none()); 141 | assert!(env.table2.get(&key, &guard2).is_none()); 142 | (*in_table)[idx] = false; 143 | } 144 | (*in_use)[idx].swap(false, Ordering::SeqCst); 145 | } 146 | } 147 | } 148 | 149 | fn stress_find_thread(env: Arc) { 150 | let mut rng = rand::thread_rng(); 151 | let guard1 = env.table1.guard(); 152 | let guard2 = env.table2.guard(); 153 | 154 | while !env.finished.load(Ordering::SeqCst) { 155 | let idx = env.ind_dist.sample(&mut rng); 156 | let in_use = env.in_use.lock().unwrap(); 157 | if (*in_use)[idx] 158 | .compare_exchange(false, true, Ordering::SeqCst, Ordering::Relaxed) 159 | .is_ok() 160 | { 161 | let key = env.keys[idx]; 162 | let in_table = env.in_table.lock().unwrap(); 163 | let val1 = (*env.vals1.lock().unwrap())[idx]; 164 | let val2 = (*env.vals2.lock().unwrap())[idx]; 165 | 166 | let value = env.table1.get(&key, &guard1); 167 | if value.is_some() { 168 | assert_eq!(&val1, value.unwrap()); 169 | assert!((*in_table)[idx]); 170 | } 171 | let value = env.table2.get(&key, &guard2); 172 | if value.is_some() { 173 | assert_eq!(&val2, value.unwrap()); 174 | assert!((*in_table)[idx]); 175 | } 176 | (*in_use)[idx].swap(false, Ordering::SeqCst); 177 | } 178 | } 179 | } 180 | 181 | #[test] 182 | #[ignore] 183 | #[cfg(not(papaya_stress))] 184 | fn stress_test_blocking() { 185 | let mut root = Environment::new(); 186 | root.table1 = HashMap::builder().resize_mode(ResizeMode::Blocking).build(); 187 | root.table2 = HashMap::builder().resize_mode(ResizeMode::Blocking).build(); 188 | run(Arc::new(root)); 189 | } 190 | 191 | #[test] 192 | #[ignore] 193 | fn stress_test_incremental() { 194 | let mut root = Environment::new(); 195 | root.table1 = HashMap::builder() 196 | .resize_mode(ResizeMode::Incremental(1024)) 197 | .build(); 198 | root.table2 = HashMap::builder() 199 | .resize_mode(ResizeMode::Incremental(1024)) 200 | .build(); 201 | run(Arc::new(root)); 202 | } 203 | 204 | #[test] 205 | #[ignore] 206 | fn stress_test_incremental_slow() { 207 | let mut root = Environment::new(); 208 | root.table1 = HashMap::builder() 209 | .resize_mode(ResizeMode::Incremental(1)) 210 | .build(); 211 | root.table2 = HashMap::builder() 212 | .resize_mode(ResizeMode::Incremental(1)) 213 | .build(); 214 | run(Arc::new(root)); 215 | } 216 | 217 | fn run(root: Arc) { 218 | let mut threads = Vec::new(); 219 | for _ in 0..cfg::NUM_THREADS { 220 | let env = Arc::clone(&root); 221 | threads.push(thread::spawn(move || stress_insert_thread(env))); 222 | let env = Arc::clone(&root); 223 | threads.push(thread::spawn(move || stress_delete_thread(env))); 224 | let env = Arc::clone(&root); 225 | threads.push(thread::spawn(move || stress_find_thread(env))); 226 | } 227 | thread::sleep(std::time::Duration::from_millis(cfg::TEST_LEN)); 228 | root.finished.swap(true, Ordering::SeqCst); 229 | for t in threads { 230 | t.join().expect("failed to join thread"); 231 | } 232 | 233 | if !cfg!(papaya_stress) { 234 | let in_table = &*root.in_table.lock().unwrap(); 235 | let num_filled = in_table.iter().filter(|b| **b).count(); 236 | assert_eq!(num_filled, root.table1.pin().len()); 237 | assert_eq!(num_filled, root.table2.pin().len()); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A fast and ergonomic concurrent hash-table for read-heavy workloads. 2 | //! 3 | //! # Features 4 | //! 5 | //! - An ergonomic lock-free API — no more deadlocks! 6 | //! - Powerful atomic operations. 7 | //! - Seamless usage in async contexts. 8 | //! - Extremely scalable, low-latency reads (see [performance](#performance)). 9 | //! - Predictable latency across all operations. 10 | //! - Efficient memory usage, with garbage collection powered by [`seize`]. 11 | //! 12 | //! # Overview 13 | //! 14 | //! The top-level crate documentation is organized as follows: 15 | //! 16 | //! - [Usage](#usage) shows how to interact with the concurrent `HashMap`. 17 | //! - [Consistency](#consistency) describes the guarantees of concurrent operations. 18 | //! - [Atomic Operations](#atomic-operations) shows how to perform dynamic operations atomically. 19 | //! - [Async Support](#async-support) shows how to use the map in an async context. 20 | //! - [Advanced Lifetimes](#advanced-lifetimes) explains how to use guards when working with nested types. 21 | //! - [Performance](#performance) provides details of expected performance characteristics. 22 | //! 23 | //! # Usage 24 | //! 25 | //! `papaya` aims to provide an ergonomic API without sacrificing performance. [`HashMap`] exposes a lock-free API, enabling it to hand out direct references to objects in the map without the need for wrapper types that are clunky and prone to deadlocks. However, you can't hold on to references forever due to concurrent removals. Because of this, the `HashMap` API is structured around *pinning*. Through a pin you can access the map just like a standard `HashMap`. A pin is similar to a lock guard, so any references that are returned will be tied to the lifetime of the guard. Unlike a lock however, pinning is cheap and can never cause deadlocks. 26 | //! 27 | //! ```rust 28 | //! use papaya::HashMap; 29 | //! 30 | //! // Create a map. 31 | //! let map = HashMap::new(); 32 | //! 33 | //! // Pin the map. 34 | //! let map = map.pin(); 35 | //! 36 | //! // Use the map as normal. 37 | //! map.insert('A', 1); 38 | //! assert_eq!(map.get(&'A'), Some(&1)); 39 | //! assert_eq!(map.len(), 1); 40 | //! ``` 41 | //! 42 | //! As expected of a concurrent `HashMap`, all operations take a shared reference. This allows the map to be freely pinned and accessed from multiple threads: 43 | //! 44 | //! ```rust 45 | //! use papaya::HashMap; 46 | //! 47 | //! // Use a map from multiple threads. 48 | //! let map = HashMap::new(); 49 | //! std::thread::scope(|s| { 50 | //! // Insert some values. 51 | //! s.spawn(|| { 52 | //! let map = map.pin(); 53 | //! for i in 'A'..='Z' { 54 | //! map.insert(i, 1); 55 | //! } 56 | //! }); 57 | //! 58 | //! // Remove the values. 59 | //! s.spawn(|| { 60 | //! let map = map.pin(); 61 | //! for i in 'A'..='Z' { 62 | //! map.remove(&i); 63 | //! } 64 | //! }); 65 | //! 66 | //! // Read the values. 67 | //! s.spawn(|| { 68 | //! for (key, value) in map.pin().iter() { 69 | //! println!("{key}: {value}"); 70 | //! } 71 | //! }); 72 | //! }); 73 | //! ``` 74 | //! 75 | //! It is important to note that as long as you are holding on to a guard, you are preventing the map from performing garbage collection. Pinning and unpinning the table is relatively cheap but not free, similar to the cost of locking and unlocking an uncontended or lightly contended `Mutex`. Thus guard reuse is encouraged, within reason. See the [`seize`] crate for advanced usage and specifics of the garbage collection algorithm. 76 | //! 77 | //! # Consistency 78 | //! 79 | //! Due to the concurrent nature of the map, read and write operations may overlap in time. There is no support for locking the entire table nor individual keys to prevent concurrent access, except through external fine-grained locking. As such, read operations (such as `get`) reflect the results of the *most-recent* write. More formally, a read establishes a *happens-before* relationship with the corresponding write. 80 | //! 81 | //! Aggregate operations, such as iterators, rely on a weak snapshot of the table and return results reflecting the state of the table at or some point after the creation of the iterator. This means that they may, but are not guaranteed to, reflect concurrent modifications to the table that occur during iteration. Similarly, operations such as `clear` and `clone` rely on iteration and may not produce "perfect" results if the map is being concurrently modified. 82 | //! 83 | //! Note that to obtain a stable snapshot of the table, aggregate table operations require completing any in-progress resizes. If you rely heavily on iteration or similar operations you should consider configuring [`ResizeMode::Blocking`]. 84 | //! 85 | //! # Atomic Operations 86 | //! 87 | //! As mentioned above, `papaya` does not support locking keys to prevent access, which makes performing complex operations more challenging. Instead, `papaya` exposes a number of atomic operations. The most basic of these is [`HashMap::update`], which can be used to update an existing value in the map using a closure: 88 | //! 89 | //! ```rust 90 | //! let map = papaya::HashMap::new(); 91 | //! map.pin().insert("poneyland", 42); 92 | //! assert_eq!(map.pin().update("poneyland", |e| e + 1), Some(&43)); 93 | //! ``` 94 | //! 95 | //! Note that in the event that the entry is concurrently modified during an `update`, the closure may be called multiple times to retry the operation. For this reason, update operations are intended to be quick and *pure*, as they may be retried or internally memoized. 96 | //! 97 | //! `papaya` also exposes more powerful atomic operations that serve as a replacement for the [standard entry API](std::collections::hash_map::Entry). These include: 98 | //! 99 | //! - [`HashMap::update`] 100 | //! - [`HashMap::update_or_insert`] 101 | //! - [`HashMap::update_or_insert_with`] 102 | //! - [`HashMap::get_or_insert`] 103 | //! - [`HashMap::get_or_insert_with`] 104 | //! - [`HashMap::compute`] 105 | //! 106 | //! For example, with a standard `HashMap`, `Entry::and_modify` is often paired with `Entry::or_insert`: 107 | //! 108 | //! ```rust 109 | //! use std::collections::HashMap; 110 | //! 111 | //! let mut map = HashMap::new(); 112 | //! // Insert `poneyland` with the value `42` if it doesn't exist, 113 | //! // otherwise increment it's value. 114 | //! map.entry("poneyland") 115 | //! .and_modify(|e| { *e += 1 }) 116 | //! .or_insert(42); 117 | //! ``` 118 | //! 119 | //! However, implementing this with a concurrent `HashMap` is tricky as the entry may be modified in-between operations. Instead, you can write the above operation using [`HashMap::update_or_insert`]: 120 | //! 121 | //! ```rust 122 | //! use papaya::HashMap; 123 | //! 124 | //! let map = HashMap::new(); 125 | //! // Insert `poneyland` with the value `42` if it doesn't exist, 126 | //! // otherwise increment it's value. 127 | //! map.pin().update_or_insert("poneyland", |e| e + 1, 42); 128 | //! ``` 129 | //! 130 | //! Atomic operations are extremely powerful but also easy to misuse. They may be less efficient than update mechanisms tailored for the specific type of data in the map. For example, concurrent counters should avoid using `update` and instead use `AtomicUsize`. Entries that are frequently modified may also benefit from fine-grained locking. 131 | //! 132 | //! # Async Support 133 | //! 134 | //! By default, a pinned map guard does not implement `Send` as it is tied to the current thread, similar to a lock guard. This leads to an issue in work-stealing schedulers as guards are not valid across `.await` points. 135 | //! 136 | //! To overcome this, you can use an *owned* guard. 137 | //! 138 | //! ```rust 139 | //! # use std::sync::Arc; 140 | //! use papaya::HashMap; 141 | //! 142 | //! async fn run(map: Arc>) { 143 | //! tokio::spawn(async move { 144 | //! // Pin the map with an owned guard. 145 | //! let map = map.pin_owned(); 146 | //! 147 | //! // Hold references across await points. 148 | //! let value = map.get(&37); 149 | //! tokio::fs::write("db.txt", format!("{value:?}")).await; 150 | //! println!("{value:?}"); 151 | //! }); 152 | //! } 153 | //! ``` 154 | //! 155 | //! Note that owned guards are more expensive to create than regular guards, so they should only be used if necessary. In the above example, you could instead drop the reference and call `get` a second time after the asynchronous call. A more fitting example involves asynchronous iteration: 156 | //! 157 | //! ```rust 158 | //! # use std::sync::Arc; 159 | //! use papaya::HashMap; 160 | //! 161 | //! async fn run(map: Arc>) { 162 | //! tokio::spawn(async move { 163 | //! for (key, value) in map.pin_owned().iter() { 164 | //! tokio::fs::write("db.txt", format!("{key}: {value}\n")).await; 165 | //! } 166 | //! }); 167 | //! } 168 | //! ``` 169 | //! 170 | //! # Advanced Lifetimes 171 | //! 172 | //! You may run into issues when you try to return a reference to a map contained within an outer type. For example: 173 | //! 174 | //! ```rust,compile_fail 175 | //! pub struct Metrics { 176 | //! map: papaya::HashMap> 177 | //! } 178 | //! 179 | //! impl Metrics { 180 | //! pub fn get(&self, name: &str) -> Option<&[u64]> { 181 | //! // error[E0515]: cannot return value referencing temporary value 182 | //! Some(self.map.pin().get(name)?.as_slice()) 183 | //! } 184 | //! } 185 | //! ``` 186 | //! 187 | //! The solution is to accept a guard in the method directly, tying the lifetime to the caller's stack frame: 188 | //! 189 | //! ```rust 190 | //! use papaya::Guard; 191 | //! 192 | //! pub struct Metrics { 193 | //! map: papaya::HashMap> 194 | //! } 195 | //! 196 | //! impl Metrics { 197 | //! pub fn guard(&self) -> impl Guard + '_ { 198 | //! self.map.guard() 199 | //! } 200 | //! 201 | //! pub fn get<'guard>(&self, name: &str, guard: &'guard impl Guard) -> Option<&'guard [u64]> { 202 | //! Some(self.map.get(name, guard)?.as_slice()) 203 | //! } 204 | //! } 205 | //! ``` 206 | //! 207 | //! The `Guard` trait supports both local and owned guards. Note the `'guard` lifetime that ties the guard to the returned reference. No wrapper types or guard mapping is necessary. 208 | //! 209 | //! # Performance 210 | //! 211 | //! `papaya` is built with read-heavy workloads in mind. As such, read operations are extremely high throughput and provide consistent performance that scales with concurrency, meaning `papaya` will excel in workloads where reads are more common than writes. In write heavy workloads, `papaya` will still provide competitive performance despite not being it's primary use case. See the [benchmarks] for details. 212 | //! 213 | //! `papaya` aims to provide predictable and consistent latency across all operations. Most operations are lock-free, and those that aren't only block under rare and constrained conditions. `papaya` also features [incremental resizing](ResizeMode). Predictable latency is an important part of performance that doesn't often show up in benchmarks, but has significant implications for real-world usage. 214 | //! 215 | //! [benchmarks]: https://github.com/ibraheemdev/papaya/blob/master/BENCHMARKS.md 216 | 217 | #![deny( 218 | missing_debug_implementations, 219 | missing_docs, 220 | dead_code, 221 | unsafe_op_in_unsafe_fn 222 | )] 223 | // Polyfills for unstable APIs related to strict-provenance. 224 | #![allow(unstable_name_collisions)] 225 | // Stylistic preferences. 226 | #![allow(clippy::multiple_bound_locations, clippy::single_match)] 227 | // Clippy trips up with pollyfills. 228 | #![allow(clippy::incompatible_msrv)] 229 | 230 | mod map; 231 | mod raw; 232 | mod set; 233 | 234 | #[cfg(feature = "serde")] 235 | mod serde_impls; 236 | 237 | pub use equivalent::Equivalent; 238 | pub use map::{ 239 | Compute, HashMap, HashMapBuilder, HashMapRef, Iter, Keys, OccupiedError, Operation, ResizeMode, 240 | Values, 241 | }; 242 | pub use seize::{Guard, LocalGuard, OwnedGuard}; 243 | pub use set::{HashSet, HashSetBuilder, HashSetRef}; 244 | -------------------------------------------------------------------------------- /assets/Exchange.ahash.throughput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exchange.ahash: Throughput 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Throughput 40 | 41 | 42 | Threads 43 | 44 | 45 | Throughput 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 Mop/s 66 | 67 | 68 | 69 | 20 Mop/s 70 | 71 | 72 | 73 | 40 Mop/s 74 | 75 | 76 | 77 | 60 Mop/s 78 | 79 | 80 | 81 | 80 Mop/s 82 | 83 | 84 | 85 | 100 Mop/s 86 | 87 | 88 | 89 | 120 Mop/s 90 | 91 | 92 | 93 | 140 Mop/s 94 | 95 | 96 | 97 | 98 | 2 99 | 100 | 101 | 102 | 4 103 | 104 | 105 | 106 | 6 107 | 108 | 109 | 110 | 8 111 | 112 | 113 | 114 | 10 115 | 116 | 117 | 118 | 12 119 | 120 | 121 | 122 | 14 123 | 124 | 125 | 126 | 16 127 | 128 | 129 | 130 | 18 131 | 132 | 133 | 134 | 20 135 | 136 | 137 | 138 | 22 139 | 140 | 141 | 142 | 24 143 | 144 | 145 | 146 | 26 147 | 148 | 149 | 150 | 28 151 | 152 | 153 | 154 | 30 155 | 156 | 157 | 158 | 32 159 | 160 | 161 | 162 | 163 | 0 Mop/s 164 | 165 | 166 | 167 | 20 Mop/s 168 | 169 | 170 | 171 | 40 Mop/s 172 | 173 | 174 | 175 | 60 Mop/s 176 | 177 | 178 | 179 | 80 Mop/s 180 | 181 | 182 | 183 | 100 Mop/s 184 | 185 | 186 | 187 | 120 Mop/s 188 | 189 | 190 | 191 | 140 Mop/s 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | DashMap 202 | 203 | 204 | Flurry 205 | 206 | 207 | Papaya 208 | 209 | 210 | Scc 211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /assets/RapidGrow.ahash.throughput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RapidGrow.ahash: Throughput 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Throughput 40 | 41 | 42 | Threads 43 | 44 | 45 | Throughput 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 Mop/s 66 | 67 | 68 | 69 | 20 Mop/s 70 | 71 | 72 | 73 | 40 Mop/s 74 | 75 | 76 | 77 | 60 Mop/s 78 | 79 | 80 | 81 | 80 Mop/s 82 | 83 | 84 | 85 | 100 Mop/s 86 | 87 | 88 | 89 | 120 Mop/s 90 | 91 | 92 | 93 | 140 Mop/s 94 | 95 | 96 | 97 | 98 | 2 99 | 100 | 101 | 102 | 4 103 | 104 | 105 | 106 | 6 107 | 108 | 109 | 110 | 8 111 | 112 | 113 | 114 | 10 115 | 116 | 117 | 118 | 12 119 | 120 | 121 | 122 | 14 123 | 124 | 125 | 126 | 16 127 | 128 | 129 | 130 | 18 131 | 132 | 133 | 134 | 20 135 | 136 | 137 | 138 | 22 139 | 140 | 141 | 142 | 24 143 | 144 | 145 | 146 | 26 147 | 148 | 149 | 150 | 28 151 | 152 | 153 | 154 | 30 155 | 156 | 157 | 158 | 32 159 | 160 | 161 | 162 | 163 | 0 Mop/s 164 | 165 | 166 | 167 | 20 Mop/s 168 | 169 | 170 | 171 | 40 Mop/s 172 | 173 | 174 | 175 | 60 Mop/s 176 | 177 | 178 | 179 | 80 Mop/s 180 | 181 | 182 | 183 | 100 Mop/s 184 | 185 | 186 | 187 | 120 Mop/s 188 | 189 | 190 | 191 | 140 Mop/s 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | DashMap 202 | 203 | 204 | Flurry 205 | 206 | 207 | Papaya 208 | 209 | 210 | Scc 211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /assets/ReadHeavy.ahash.throughput.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReadHeavy.ahash: Throughput 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Throughput 40 | 41 | 42 | Threads 43 | 44 | 45 | Throughput 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 Mop/s 66 | 67 | 68 | 69 | 50 Mop/s 70 | 71 | 72 | 73 | 100 Mop/s 74 | 75 | 76 | 77 | 150 Mop/s 78 | 79 | 80 | 81 | 200 Mop/s 82 | 83 | 84 | 85 | 250 Mop/s 86 | 87 | 88 | 89 | 300 Mop/s 90 | 91 | 92 | 93 | 350 Mop/s 94 | 95 | 96 | 97 | 98 | 2 99 | 100 | 101 | 102 | 4 103 | 104 | 105 | 106 | 6 107 | 108 | 109 | 110 | 8 111 | 112 | 113 | 114 | 10 115 | 116 | 117 | 118 | 12 119 | 120 | 121 | 122 | 14 123 | 124 | 125 | 126 | 16 127 | 128 | 129 | 130 | 18 131 | 132 | 133 | 134 | 20 135 | 136 | 137 | 138 | 22 139 | 140 | 141 | 142 | 24 143 | 144 | 145 | 146 | 26 147 | 148 | 149 | 150 | 28 151 | 152 | 153 | 154 | 30 155 | 156 | 157 | 158 | 32 159 | 160 | 161 | 162 | 163 | 0 Mop/s 164 | 165 | 166 | 167 | 50 Mop/s 168 | 169 | 170 | 171 | 100 Mop/s 172 | 173 | 174 | 175 | 150 Mop/s 176 | 177 | 178 | 179 | 200 Mop/s 180 | 181 | 182 | 183 | 250 Mop/s 184 | 185 | 186 | 187 | 300 Mop/s 188 | 189 | 190 | 191 | 350 Mop/s 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | DashMap 202 | 203 | 204 | Flurry 205 | 206 | 207 | Papaya 208 | 209 | 210 | Scc 211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /assets/RapidGrow.ahash.latency.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RapidGrow.ahash: Latency 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Latency 40 | 41 | 42 | Threads 43 | 44 | 45 | Latency 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 ns 66 | 67 | 68 | 69 | 100 ns 70 | 71 | 72 | 73 | 200 ns 74 | 75 | 76 | 77 | 300 ns 78 | 79 | 80 | 81 | 400 ns 82 | 83 | 84 | 85 | 500 ns 86 | 87 | 88 | 89 | 600 ns 90 | 91 | 92 | 93 | 700 ns 94 | 95 | 96 | 97 | 800 ns 98 | 99 | 100 | 101 | 900 ns 102 | 103 | 104 | 105 | 106 | 2 107 | 108 | 109 | 110 | 4 111 | 112 | 113 | 114 | 6 115 | 116 | 117 | 118 | 8 119 | 120 | 121 | 122 | 10 123 | 124 | 125 | 126 | 12 127 | 128 | 129 | 130 | 14 131 | 132 | 133 | 134 | 16 135 | 136 | 137 | 138 | 18 139 | 140 | 141 | 142 | 20 143 | 144 | 145 | 146 | 22 147 | 148 | 149 | 150 | 24 151 | 152 | 153 | 154 | 26 155 | 156 | 157 | 158 | 28 159 | 160 | 161 | 162 | 30 163 | 164 | 165 | 166 | 32 167 | 168 | 169 | 170 | 171 | 0 ns 172 | 173 | 174 | 175 | 100 ns 176 | 177 | 178 | 179 | 200 ns 180 | 181 | 182 | 183 | 300 ns 184 | 185 | 186 | 187 | 400 ns 188 | 189 | 190 | 191 | 500 ns 192 | 193 | 194 | 195 | 600 ns 196 | 197 | 198 | 199 | 700 ns 200 | 201 | 202 | 203 | 800 ns 204 | 205 | 206 | 207 | 900 ns 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | DashMap 218 | 219 | 220 | Flurry 221 | 222 | 223 | Papaya 224 | 225 | 226 | Scc 227 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /tests/basic_set.rs: -------------------------------------------------------------------------------- 1 | // Adapted from: https://github.com/jonhoo/flurry/blob/main/tests/basic.rs 2 | 3 | use papaya::HashSet; 4 | 5 | use std::hash::{BuildHasher, BuildHasherDefault, Hasher}; 6 | use std::sync::Arc; 7 | 8 | mod common; 9 | use common::with_set; 10 | 11 | #[test] 12 | fn new() { 13 | with_set::(|set| drop(set())); 14 | } 15 | 16 | #[test] 17 | fn clear() { 18 | with_set::(|set| { 19 | let set = set(); 20 | let guard = set.guard(); 21 | { 22 | set.insert(0, &guard); 23 | set.insert(1, &guard); 24 | set.insert(2, &guard); 25 | set.insert(3, &guard); 26 | set.insert(4, &guard); 27 | } 28 | set.clear(&guard); 29 | assert!(set.is_empty()); 30 | }); 31 | } 32 | 33 | #[test] 34 | fn insert() { 35 | with_set::(|set| { 36 | let set = set(); 37 | let guard = set.guard(); 38 | assert_eq!(set.insert(42, &guard), true); 39 | assert_eq!(set.insert(42, &guard), false); 40 | assert_eq!(set.len(), 1); 41 | }); 42 | } 43 | 44 | #[test] 45 | fn get_empty() { 46 | with_set::(|set| { 47 | let set = set(); 48 | let guard = set.guard(); 49 | let e = set.get(&42, &guard); 50 | assert!(e.is_none()); 51 | }); 52 | } 53 | 54 | #[test] 55 | fn remove_empty() { 56 | with_set::(|set| { 57 | let set = set(); 58 | let guard = set.guard(); 59 | assert_eq!(set.remove(&42, &guard), false); 60 | }); 61 | } 62 | 63 | #[test] 64 | fn insert_and_remove() { 65 | with_set::(|set| { 66 | let set = set(); 67 | let guard = set.guard(); 68 | assert!(set.insert(42, &guard)); 69 | assert!(set.remove(&42, &guard)); 70 | assert!(set.get(&42, &guard).is_none()); 71 | }); 72 | } 73 | 74 | #[test] 75 | fn insert_and_get() { 76 | with_set::(|set| { 77 | let set = set(); 78 | set.insert(42, &set.guard()); 79 | 80 | { 81 | let guard = set.guard(); 82 | let e = set.get(&42, &guard).unwrap(); 83 | assert_eq!(e, &42); 84 | } 85 | }); 86 | } 87 | 88 | #[test] 89 | fn reinsert() { 90 | with_set::(|set| { 91 | let set = set(); 92 | let guard = set.guard(); 93 | assert!(set.insert(42, &guard)); 94 | assert!(!set.insert(42, &guard)); 95 | { 96 | let guard = set.guard(); 97 | let e = set.get(&42, &guard).unwrap(); 98 | assert_eq!(e, &42); 99 | } 100 | }); 101 | } 102 | 103 | #[test] 104 | fn concurrent_insert() { 105 | with_set::(|set| { 106 | let set = set(); 107 | let set = Arc::new(set); 108 | 109 | let set1 = set.clone(); 110 | let t1 = std::thread::spawn(move || { 111 | for i in 0..64 { 112 | set1.insert(i, &set1.guard()); 113 | } 114 | }); 115 | let set2 = set.clone(); 116 | let t2 = std::thread::spawn(move || { 117 | for i in 0..64 { 118 | set2.insert(i, &set2.guard()); 119 | } 120 | }); 121 | 122 | t1.join().unwrap(); 123 | t2.join().unwrap(); 124 | 125 | let guard = set.guard(); 126 | for i in 0..64 { 127 | let v = set.get(&i, &guard).unwrap(); 128 | assert!(v == &i); 129 | } 130 | }); 131 | } 132 | 133 | #[test] 134 | fn concurrent_remove() { 135 | with_set::(|set| { 136 | let set = set(); 137 | let set = Arc::new(set); 138 | 139 | { 140 | let guard = set.guard(); 141 | for i in 0..64 { 142 | set.insert(i, &guard); 143 | } 144 | } 145 | 146 | let set1 = set.clone(); 147 | let t1 = std::thread::spawn(move || { 148 | let guard = set1.guard(); 149 | for i in 0..64 { 150 | set1.remove(&i, &guard); 151 | } 152 | }); 153 | let set2 = set.clone(); 154 | let t2 = std::thread::spawn(move || { 155 | let guard = set2.guard(); 156 | for i in 0..64 { 157 | set2.remove(&i, &guard); 158 | } 159 | }); 160 | 161 | t1.join().unwrap(); 162 | t2.join().unwrap(); 163 | 164 | // after joining the threads, the set should be empty 165 | let guard = set.guard(); 166 | for i in 0..64 { 167 | assert!(set.get(&i, &guard).is_none()); 168 | } 169 | }); 170 | } 171 | 172 | #[test] 173 | #[cfg(not(miri))] 174 | fn concurrent_resize_and_get() { 175 | if cfg!(papaya_stress) { 176 | return; 177 | } 178 | 179 | with_set::(|set| { 180 | let set = set(); 181 | let set = Arc::new(set); 182 | 183 | { 184 | let guard = set.guard(); 185 | for i in 0..1024 { 186 | set.insert(i, &guard); 187 | } 188 | } 189 | 190 | let set1 = set.clone(); 191 | // t1 is using reserve to trigger a bunch of resizes 192 | let t1 = std::thread::spawn(move || { 193 | let guard = set1.guard(); 194 | // there should be 2 ** 10 capacity already, so trigger additional resizes 195 | for power in 11..16 { 196 | set1.reserve(1 << power, &guard); 197 | } 198 | }); 199 | let set2 = set.clone(); 200 | // t2 is retrieving existing keys a lot, attempting to encounter a BinEntry::Moved 201 | let t2 = std::thread::spawn(move || { 202 | let guard = set2.guard(); 203 | for _ in 0..32 { 204 | for i in 0..1024 { 205 | let v = set2.get(&i, &guard).unwrap(); 206 | assert_eq!(v, &i); 207 | } 208 | } 209 | }); 210 | 211 | t1.join().unwrap(); 212 | t2.join().unwrap(); 213 | 214 | // make sure all the entries still exist after all the resizes 215 | { 216 | let guard = set.guard(); 217 | 218 | for i in 0..1024 { 219 | let v = set.get(&i, &guard).unwrap(); 220 | assert_eq!(v, &i); 221 | } 222 | } 223 | }); 224 | } 225 | 226 | #[test] 227 | fn current_kv_dropped() { 228 | let dropped1 = Arc::new(0); 229 | 230 | with_set::>(|set| { 231 | let set = set(); 232 | set.insert(dropped1.clone(), &set.guard()); 233 | assert_eq!(Arc::strong_count(&dropped1), 2); 234 | 235 | drop(set); 236 | 237 | // dropping the set should immediately drop (not deferred) all keys and values 238 | assert_eq!(Arc::strong_count(&dropped1), 1); 239 | }); 240 | } 241 | 242 | #[test] 243 | fn empty_sets_equal() { 244 | with_set::(|set1| { 245 | let set1 = set1(); 246 | with_set::(|set2| { 247 | let set2 = set2(); 248 | assert_eq!(set1, set2); 249 | assert_eq!(set2, set1); 250 | }); 251 | }); 252 | } 253 | 254 | #[test] 255 | fn different_size_sets_not_equal() { 256 | with_set::(|set1| { 257 | let set1 = set1(); 258 | with_set::(|set2| { 259 | let set2 = set2(); 260 | { 261 | let guard1 = set1.guard(); 262 | let guard2 = set2.guard(); 263 | 264 | set1.insert(1, &guard1); 265 | set1.insert(2, &guard1); 266 | set1.insert(3, &guard1); 267 | 268 | set2.insert(1, &guard2); 269 | set2.insert(2, &guard2); 270 | } 271 | 272 | assert_ne!(set1, set2); 273 | assert_ne!(set2, set1); 274 | }); 275 | }); 276 | } 277 | 278 | #[test] 279 | fn same_values_equal() { 280 | with_set::(|set1| { 281 | let set1 = set1(); 282 | with_set::(|set2| { 283 | let set2 = set2(); 284 | { 285 | set1.pin().insert(1); 286 | set2.pin().insert(1); 287 | } 288 | 289 | assert_eq!(set1, set2); 290 | assert_eq!(set2, set1); 291 | }); 292 | }); 293 | } 294 | 295 | #[test] 296 | fn different_values_not_equal() { 297 | with_set::(|set1| { 298 | let set1 = set1(); 299 | with_set::(|set2| { 300 | let set2 = set2(); 301 | { 302 | set1.pin().insert(1); 303 | set2.pin().insert(2); 304 | } 305 | 306 | assert_ne!(set1, set2); 307 | assert_ne!(set2, set1); 308 | }); 309 | }); 310 | } 311 | 312 | #[test] 313 | fn clone_set_empty() { 314 | with_set::<&'static str>(|set| { 315 | let set = set(); 316 | let cloned_set = set.clone(); 317 | assert_eq!(set.len(), cloned_set.len()); 318 | assert_eq!(&set, &cloned_set); 319 | assert_eq!(cloned_set.len(), 0); 320 | }); 321 | } 322 | 323 | #[test] 324 | // Test that same values exists in both sets (original and cloned) 325 | fn clone_set_filled() { 326 | with_set::<&'static str>(|set| { 327 | let set = set(); 328 | set.insert("FooKey", &set.guard()); 329 | set.insert("BarKey", &set.guard()); 330 | let cloned_set = set.clone(); 331 | assert_eq!(set.len(), cloned_set.len()); 332 | assert_eq!(&set, &cloned_set); 333 | 334 | // test that we are not setting the same tables 335 | set.insert("NewItem", &set.guard()); 336 | assert_ne!(&set, &cloned_set); 337 | }); 338 | } 339 | 340 | #[test] 341 | fn default() { 342 | with_set::(|set| { 343 | let set = set(); 344 | let guard = set.guard(); 345 | set.insert(42, &guard); 346 | 347 | assert_eq!(set.get(&42, &guard), Some(&42)); 348 | }); 349 | } 350 | 351 | #[test] 352 | fn debug() { 353 | with_set::(|set| { 354 | let set = set(); 355 | let guard = set.guard(); 356 | set.insert(42, &guard); 357 | set.insert(16, &guard); 358 | 359 | let formatted = format!("{:?}", set); 360 | 361 | assert!(formatted == "{42, 16}" || formatted == "{16, 42}"); 362 | }); 363 | } 364 | 365 | #[test] 366 | fn extend() { 367 | if cfg!(papaya_stress) { 368 | return; 369 | } 370 | 371 | with_set::(|set| { 372 | let set = set(); 373 | let guard = set.guard(); 374 | 375 | let mut entries: Vec = vec![42, 16, 38]; 376 | entries.sort_unstable(); 377 | 378 | (&set).extend(entries.clone().into_iter()); 379 | 380 | let mut collected: Vec = set.iter(&guard).map(|key| *key).collect(); 381 | collected.sort_unstable(); 382 | 383 | assert_eq!(entries, collected); 384 | }); 385 | } 386 | 387 | #[test] 388 | fn extend_ref() { 389 | if cfg!(papaya_stress) { 390 | return; 391 | } 392 | 393 | with_set::(|set| { 394 | let set = set(); 395 | let mut entries: Vec<&usize> = vec![&42, &36, &18]; 396 | entries.sort(); 397 | 398 | (&set).extend(entries.clone().into_iter()); 399 | 400 | let guard = set.guard(); 401 | let mut collected: Vec<&usize> = set.iter(&guard).collect(); 402 | collected.sort(); 403 | 404 | assert_eq!(entries, collected); 405 | }); 406 | } 407 | 408 | #[test] 409 | fn from_iter_empty() { 410 | use std::iter::FromIterator; 411 | 412 | let entries: Vec = Vec::new(); 413 | let set: HashSet = HashSet::from_iter(entries.into_iter()); 414 | 415 | assert_eq!(set.len(), 0) 416 | } 417 | 418 | #[test] 419 | fn from_iter_repeated() { 420 | use std::iter::FromIterator; 421 | 422 | let entries = vec![0, 0, 0]; 423 | let set: HashSet<_> = HashSet::from_iter(entries.into_iter()); 424 | let set = set.pin(); 425 | assert_eq!(set.len(), 1); 426 | assert_eq!(set.iter().collect::>(), vec![&0]) 427 | } 428 | 429 | #[test] 430 | fn len() { 431 | with_set::(|set| { 432 | let set = set(); 433 | let len = if cfg!(miri) { 100 } else { 10_000 }; 434 | for i in 0..len { 435 | set.pin().insert(i); 436 | } 437 | assert_eq!(set.len(), len); 438 | }); 439 | } 440 | 441 | #[test] 442 | fn iter() { 443 | if cfg!(papaya_stress) { 444 | return; 445 | } 446 | 447 | with_set::(|set| { 448 | let set = set(); 449 | let len = if cfg!(miri) { 100 } else { 10_000 }; 450 | for i in 0..len { 451 | assert_eq!(set.pin().insert(i), true); 452 | } 453 | 454 | let v: Vec<_> = (0..len).collect(); 455 | let mut got: Vec<_> = set.pin().iter().map(|&k| k).collect(); 456 | got.sort(); 457 | assert_eq!(v, got); 458 | }); 459 | } 460 | 461 | #[test] 462 | fn retain_empty() { 463 | with_set::(|set| { 464 | let set = set(); 465 | set.pin().retain(|_| false); 466 | assert_eq!(set.len(), 0); 467 | }); 468 | } 469 | 470 | #[test] 471 | fn retain_all_false() { 472 | with_set::(|set| { 473 | let set = set(); 474 | for i in 0..10 { 475 | set.pin().insert(i); 476 | } 477 | set.pin().retain(|_| false); 478 | assert_eq!(set.len(), 0); 479 | }); 480 | } 481 | 482 | #[test] 483 | fn retain_all_true() { 484 | with_set::(|set| { 485 | let set = set(); 486 | for i in 0..10 { 487 | set.pin().insert(i); 488 | } 489 | set.pin().retain(|_| true); 490 | assert_eq!(set.len(), 10); 491 | }); 492 | } 493 | 494 | #[test] 495 | fn retain_some() { 496 | with_set::(|set| { 497 | let set = set(); 498 | for i in 0..10 { 499 | set.pin().insert(i); 500 | } 501 | set.pin().retain(|&k| k >= 5); 502 | assert_eq!(set.len(), 5); 503 | let mut got: Vec<_> = set.pin().iter().copied().collect(); 504 | got.sort(); 505 | assert_eq!(got, [5, 6, 7, 8, 9]); 506 | }); 507 | } 508 | 509 | #[test] 510 | fn mixed() { 511 | const LEN: usize = if cfg!(miri) { 48 } else { 1024 }; 512 | with_set::(|set| { 513 | let set = set(); 514 | assert!(set.pin().get(&100).is_none()); 515 | set.pin().insert(100); 516 | assert_eq!(set.pin().get(&100), Some(&100)); 517 | 518 | assert!(set.pin().get(&200).is_none()); 519 | set.pin().insert(200); 520 | assert_eq!(set.pin().get(&200), Some(&200)); 521 | 522 | assert!(set.pin().get(&300).is_none()); 523 | 524 | assert_eq!(set.pin().remove(&100), true); 525 | assert_eq!(set.pin().remove(&200), true); 526 | assert_eq!(set.pin().remove(&300), false); 527 | 528 | assert!(set.pin().get(&100).is_none()); 529 | assert!(set.pin().get(&200).is_none()); 530 | assert!(set.pin().get(&300).is_none()); 531 | 532 | for i in 0..LEN { 533 | assert_eq!(set.pin().insert(i), true); 534 | } 535 | 536 | for i in 0..LEN { 537 | assert_eq!(set.pin().get(&i), Some(&i)); 538 | } 539 | 540 | for i in 0..LEN { 541 | assert_eq!(set.pin().remove(&i), true); 542 | } 543 | 544 | for i in 0..LEN { 545 | assert_eq!(set.pin().get(&i), None); 546 | } 547 | 548 | for i in 0..(LEN * 2) { 549 | assert_eq!(set.pin().insert(i), true); 550 | } 551 | 552 | for i in 0..(LEN * 2) { 553 | assert_eq!(set.pin().get(&i), Some(&i)); 554 | } 555 | }); 556 | } 557 | 558 | // run tests with hashers that create unrealistically long probe sequences 559 | mod hasher { 560 | use super::*; 561 | 562 | fn check() { 563 | let range = if cfg!(miri) { 0..16 } else { 0..100 }; 564 | 565 | with_set::(|set| { 566 | let set = set(); 567 | let guard = set.guard(); 568 | for i in range.clone() { 569 | set.insert(i, &guard); 570 | } 571 | 572 | assert!(!set.contains(&i32::min_value(), &guard)); 573 | assert!(!set.contains(&(range.start - 1), &guard)); 574 | for i in range.clone() { 575 | assert!(set.contains(&i, &guard)); 576 | } 577 | assert!(!set.contains(&range.end, &guard)); 578 | assert!(!set.contains(&i32::max_value(), &guard)); 579 | }); 580 | } 581 | 582 | #[test] 583 | fn test_zero_hasher() { 584 | #[derive(Default)] 585 | pub struct ZeroHasher; 586 | 587 | impl Hasher for ZeroHasher { 588 | fn finish(&self) -> u64 { 589 | 0 590 | } 591 | 592 | fn write(&mut self, _: &[u8]) {} 593 | } 594 | 595 | check::>(); 596 | } 597 | 598 | #[test] 599 | fn test_max_hasher() { 600 | #[derive(Default)] 601 | struct MaxHasher; 602 | 603 | impl Hasher for MaxHasher { 604 | fn finish(&self) -> u64 { 605 | u64::max_value() 606 | } 607 | 608 | fn write(&mut self, _: &[u8]) {} 609 | } 610 | 611 | check::>(); 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /assets/Exchange.ahash.latency.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exchange.ahash: Latency 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Latency 40 | 41 | 42 | Threads 43 | 44 | 45 | Latency 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 ns 66 | 67 | 68 | 69 | 50 ns 70 | 71 | 72 | 73 | 100 ns 74 | 75 | 76 | 77 | 150 ns 78 | 79 | 80 | 81 | 200 ns 82 | 83 | 84 | 85 | 250 ns 86 | 87 | 88 | 89 | 300 ns 90 | 91 | 92 | 93 | 350 ns 94 | 95 | 96 | 97 | 400 ns 98 | 99 | 100 | 101 | 450 ns 102 | 103 | 104 | 105 | 500 ns 106 | 107 | 108 | 109 | 550 ns 110 | 111 | 112 | 113 | 600 ns 114 | 115 | 116 | 117 | 650 ns 118 | 119 | 120 | 121 | 700 ns 122 | 123 | 124 | 125 | 750 ns 126 | 127 | 128 | 129 | 800 ns 130 | 131 | 132 | 133 | 850 ns 134 | 135 | 136 | 137 | 138 | 2 139 | 140 | 141 | 142 | 4 143 | 144 | 145 | 146 | 6 147 | 148 | 149 | 150 | 8 151 | 152 | 153 | 154 | 10 155 | 156 | 157 | 158 | 12 159 | 160 | 161 | 162 | 14 163 | 164 | 165 | 166 | 16 167 | 168 | 169 | 170 | 18 171 | 172 | 173 | 174 | 20 175 | 176 | 177 | 178 | 22 179 | 180 | 181 | 182 | 24 183 | 184 | 185 | 186 | 26 187 | 188 | 189 | 190 | 28 191 | 192 | 193 | 194 | 30 195 | 196 | 197 | 198 | 32 199 | 200 | 201 | 202 | 203 | 0 ns 204 | 205 | 206 | 207 | 50 ns 208 | 209 | 210 | 211 | 100 ns 212 | 213 | 214 | 215 | 150 ns 216 | 217 | 218 | 219 | 200 ns 220 | 221 | 222 | 223 | 250 ns 224 | 225 | 226 | 227 | 300 ns 228 | 229 | 230 | 231 | 350 ns 232 | 233 | 234 | 235 | 400 ns 236 | 237 | 238 | 239 | 450 ns 240 | 241 | 242 | 243 | 500 ns 244 | 245 | 246 | 247 | 550 ns 248 | 249 | 250 | 251 | 600 ns 252 | 253 | 254 | 255 | 650 ns 256 | 257 | 258 | 259 | 700 ns 260 | 261 | 262 | 263 | 750 ns 264 | 265 | 266 | 267 | 800 ns 268 | 269 | 270 | 271 | 850 ns 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | DashMap 282 | 283 | 284 | Flurry 285 | 286 | 287 | Papaya 288 | 289 | 290 | Scc 291 | 292 | 293 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /assets/ReadHeavy.ahash.latency.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ReadHeavy.ahash: Latency 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Latency 40 | 41 | 42 | Threads 43 | 44 | 45 | Latency 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 ns 66 | 67 | 68 | 69 | 10 ns 70 | 71 | 72 | 73 | 20 ns 74 | 75 | 76 | 77 | 30 ns 78 | 79 | 80 | 81 | 40 ns 82 | 83 | 84 | 85 | 50 ns 86 | 87 | 88 | 89 | 60 ns 90 | 91 | 92 | 93 | 70 ns 94 | 95 | 96 | 97 | 80 ns 98 | 99 | 100 | 101 | 90 ns 102 | 103 | 104 | 105 | 100 ns 106 | 107 | 108 | 109 | 110 ns 110 | 111 | 112 | 113 | 120 ns 114 | 115 | 116 | 117 | 130 ns 118 | 119 | 120 | 121 | 140 ns 122 | 123 | 124 | 125 | 150 ns 126 | 127 | 128 | 129 | 160 ns 130 | 131 | 132 | 133 | 170 ns 134 | 135 | 136 | 137 | 180 ns 138 | 139 | 140 | 141 | 142 | 2 143 | 144 | 145 | 146 | 4 147 | 148 | 149 | 150 | 6 151 | 152 | 153 | 154 | 8 155 | 156 | 157 | 158 | 10 159 | 160 | 161 | 162 | 12 163 | 164 | 165 | 166 | 14 167 | 168 | 169 | 170 | 16 171 | 172 | 173 | 174 | 18 175 | 176 | 177 | 178 | 20 179 | 180 | 181 | 182 | 22 183 | 184 | 185 | 186 | 24 187 | 188 | 189 | 190 | 26 191 | 192 | 193 | 194 | 28 195 | 196 | 197 | 198 | 30 199 | 200 | 201 | 202 | 32 203 | 204 | 205 | 206 | 207 | 0 ns 208 | 209 | 210 | 211 | 10 ns 212 | 213 | 214 | 215 | 20 ns 216 | 217 | 218 | 219 | 30 ns 220 | 221 | 222 | 223 | 40 ns 224 | 225 | 226 | 227 | 50 ns 228 | 229 | 230 | 231 | 60 ns 232 | 233 | 234 | 235 | 70 ns 236 | 237 | 238 | 239 | 80 ns 240 | 241 | 242 | 243 | 90 ns 244 | 245 | 246 | 247 | 100 ns 248 | 249 | 250 | 251 | 110 ns 252 | 253 | 254 | 255 | 120 ns 256 | 257 | 258 | 259 | 130 ns 260 | 261 | 262 | 263 | 140 ns 264 | 265 | 266 | 267 | 150 ns 268 | 269 | 270 | 271 | 160 ns 272 | 273 | 274 | 275 | 170 ns 276 | 277 | 278 | 279 | 180 ns 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | DashMap 290 | 291 | 292 | Flurry 293 | 294 | 295 | Papaya 296 | 297 | 298 | Scc 299 | 300 | 301 | 302 | 303 | 304 | 305 | --------------------------------------------------------------------------------