├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples └── example.rs ├── src ├── lib.rs ├── map_inner.rs └── pointer.rs └── tests ├── benchmark.rs └── integration_test.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | env: 6 | CI_RUST_TOOLCHAIN: 1.61.0 7 | 8 | jobs: 9 | check: 10 | name: Check 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: ${{ env.CI_RUST_TOOLCHAIN }} 18 | override: true 19 | - uses: actions-rs/cargo@v1 20 | with: 21 | command: check 22 | 23 | test: 24 | name: Test Suite 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: ${{ env.CI_RUST_TOOLCHAIN }} 32 | override: true 33 | - uses: actions-rs/cargo@v1 34 | with: 35 | command: test 36 | 37 | fmt: 38 | name: Rustfmt 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | profile: minimal 45 | toolchain: ${{ env.CI_RUST_TOOLCHAIN }} 46 | override: true 47 | - run: rustup component add rustfmt 48 | - uses: actions-rs/cargo@v1 49 | with: 50 | command: fmt 51 | args: --all -- --check 52 | 53 | clippy: 54 | name: Clippy 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | profile: minimal 61 | toolchain: ${{ env.CI_RUST_TOOLCHAIN }} 62 | override: true 63 | - run: rustup component add clippy 64 | - uses: actions-rs/cargo@v1 65 | with: 66 | command: clippy 67 | args: --all-targets --all-features -- -D warnings 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lockfree-cuckoohash" 3 | version = "0.1.0" 4 | authors = ["Mingcong Han "] 5 | edition = "2018" 6 | description = "A rust implementation of lockfree cuckoo hashmap" 7 | license = "MIT" 8 | repository = "https://github.com/datenlord/lockfree-cuckoohash" 9 | readme = "README.md" 10 | keywords = ["lockfree", "non-blocking", "cuckoo", "hashmap", "hashtable"] 11 | categories = ["data-structures"] 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | crossbeam-epoch = "0.9.7" 16 | clippy-utilities = "0.1.0" 17 | 18 | [dev-dependencies] 19 | rand = "0.8" 20 | num_cpus = "1.13.0" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 datenlord 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lockfree-cuckoohash 2 | 3 | This is a rust implementation of lock-free cuckoo hash table. 4 | 5 | ## Introduction 6 | Cuckoo hashing is an open addressing solution for hash collisions. The basic idea of cuckoo hashing is to resolve collisions by using two or more hash functions instead of only one. In this implementation, we use two hash functions and two arrays (or tables). 7 | 8 | The search operation only looks up two slots, i.e. table[0][hash0(key)] and table[1][hash1(key)]. If these two slots do not contain the key, the hash table do not contain the key. So the search operation only takes a constant time in the worst case. 9 | 10 | The insert operation must pay the price for the quick search. The insert operation can only put the key into one of the two slots. However, when both slots are already full, it will be necessary to move other keys to their second locations (or back to their first locations) to make room for the new key, which is called a `relocation`. If the moved key can't be relocated because the other slot of it is also occupied, another `relocation` is required. If relocation is a very long chain or meets a infinite loop, the table should be resized or rehashed. 11 | 12 | ## Test & Bench 13 | For unit test: 14 | ``` 15 | cargo test 16 | ``` 17 | For simple benchmark: 18 | ``` 19 | cargo test --test benchmark bench_read_write --release -- --ignored --nocapture 20 | ``` 21 | 22 | 23 | ## Reference 24 | * Nguyen, N., & Tsigas, P. (2014). Lock-Free Cuckoo Hashing. 2014 IEEE 34th International Conference on Distributed Computing Systems, 627-636. 25 | 26 | -------------------------------------------------------------------------------- /examples/example.rs: -------------------------------------------------------------------------------- 1 | use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 2 | use std::ops::Not; 3 | use std::sync::Arc; 4 | 5 | fn simple_read_write_example() { 6 | // Create a new empty map. 7 | let map = LockFreeCuckooHash::new(); 8 | // or use LockFreeCuckooHash::with_capacity(capacity) to specify the max capacity. 9 | 10 | // `guard` is used to keep the current thread pinned. 11 | // If a `guard` is pinned, the returned value's reference 12 | // is always valid. In other words, other threads cannot destroy 13 | // `value` until all of the `guard`s are unpinned. 14 | let guard = pin(); 15 | 16 | // Insert the key-value pair into the map. 17 | let key = 1; 18 | let value = "value"; 19 | // The returned value indicates whether the map had the key before this insertion. 20 | assert!(map.insert(key, value).not()); 21 | assert!(map.insert(key, "value2")); 22 | // If you want to get the replaced value, try `insert_with_guard`. 23 | assert_eq!(map.insert_with_guard(key, value, &guard), Some(&"value2")); 24 | 25 | // Search the value corresponding to the key. 26 | assert_eq!(map.get(&key, &guard), Some(&value)); 27 | assert_eq!(map.get(&2, &guard), None); 28 | 29 | // Remove a key-value pair. 30 | // `remove` returns `false` if the map does not have the key. 31 | assert!(map.remove(&2).not()); 32 | assert!(map.remove(&key)); 33 | assert!(map.remove(&key).not()); 34 | 35 | // If you want to get the removed value, use `remove_with_guard` instead. 36 | map.insert(key, value); 37 | assert_eq!(map.remove_with_guard(&key, &guard), Some(&value)); 38 | assert_eq!(map.remove_with_guard(&key, &guard), None); 39 | } 40 | 41 | fn multi_threads_read_write() { 42 | let map = Arc::new(LockFreeCuckooHash::new()); 43 | // Create 4 threads to write the hash table. 44 | let mut handles = Vec::with_capacity(4); 45 | for i in 0..4 { 46 | // Transfer the reference to each thread, no need for a mutex. 47 | let map = map.clone(); 48 | let handle = std::thread::spawn(move || { 49 | for j in 0..100 { 50 | let key = i * 100 + j; 51 | let value = i; 52 | map.insert(key, value); 53 | } 54 | }); 55 | handles.push(handle); 56 | } 57 | 58 | for handle in handles { 59 | handle.join().unwrap(); 60 | } 61 | 62 | let guard = pin(); 63 | assert_eq!(map.size(), 4 * 100); 64 | for i in 0..4 { 65 | for j in 0..100 { 66 | let key = i * 100 + j; 67 | let value = i; 68 | let ret = map.get(&key, &guard); 69 | assert_eq!(ret, Some(&value)); 70 | } 71 | } 72 | } 73 | 74 | fn main() { 75 | simple_read_write_example(); 76 | 77 | multi_threads_read_write(); 78 | } 79 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate implements a lockfree cuckoo hashmap. 2 | #![deny( 3 | // The following are allowed by default lints according to 4 | // https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html 5 | anonymous_parameters, 6 | bare_trait_objects, 7 | // box_pointers, // futures involve boxed pointers 8 | elided_lifetimes_in_paths, // allow anonymous lifetime in generated code 9 | missing_copy_implementations, 10 | missing_debug_implementations, 11 | // missing_docs, // TODO: add documents 12 | single_use_lifetimes, // TODO: fix lifetime names only used once 13 | trivial_casts, // TODO: remove trivial casts in code 14 | trivial_numeric_casts, 15 | // unreachable_pub, use clippy::redundant_pub_crate instead 16 | // unsafe_code, unsafe codes are inevitable here 17 | unstable_features, 18 | unused_extern_crates, 19 | unused_import_braces, 20 | unused_qualifications, 21 | // unused_results, // TODO: fix unused results 22 | variant_size_differences, 23 | 24 | // Treat warnings as errors 25 | warnings, 26 | 27 | clippy::all, 28 | clippy::restriction, 29 | clippy::pedantic, 30 | clippy::nursery, 31 | clippy::cargo 32 | )] 33 | #![allow( 34 | // Some explicitly allowed Clippy lints, must have clear reason to allow 35 | clippy::blanket_clippy_restriction_lints, // allow clippy::restriction 36 | clippy::panic, // allow debug_assert, panic in production code 37 | clippy::implicit_return, // actually omitting the return keyword is idiomatic Rust code 38 | clippy::separated_literal_suffix, // conflicts with clippy::unseparated_literal_suffix 39 | )] 40 | #![allow( 41 | clippy::undocumented_unsafe_blocks, // FIXME 42 | clippy::missing_panics_doc, // FIXME 43 | clippy::single_char_lifetime_names, // FIXME 44 | )] 45 | /// `pointer` defines atomic pointers which will be used for lockfree operations. 46 | mod pointer; 47 | 48 | /// `map_inner` defines the inner implementation of the hashmap. 49 | mod map_inner; 50 | 51 | use pointer::{AtomicPtr, SharedPtr}; 52 | use std::borrow::Borrow; 53 | use std::collections::hash_map::RandomState; 54 | use std::hash::Hash; 55 | use std::sync::atomic::Ordering; 56 | 57 | // Re-export `crossbeam_epoch::pin()` and `crossbeam_epoch::Guard`. 58 | pub use crossbeam_epoch::{pin, Guard}; 59 | 60 | /// `LockFreeCuckooHash` is a lock-free hash table using cuckoo hashing scheme. 61 | /// This implementation is based on the approach discussed in the paper: 62 | /// 63 | /// "Nguyen, N., & Tsigas, P. (2014). Lock-Free Cuckoo Hashing. 2014 IEEE 34th International 64 | /// Conference on Distributed Computing Systems, 627-636." 65 | /// 66 | /// Cuckoo hashing is an open addressing solution for hash collisions. The basic idea of cuckoo 67 | /// hashing is to resolve collisions by using two or more hash functions instead of only one. In this 68 | /// implementation, we use two hash functions and two arrays (or tables). 69 | /// 70 | /// The search operation only looks up two slots, i.e. table[0][hash0(key)] and table[1][hash1(key)]. 71 | /// If these two slots do not contain the key, the hash table does not contain the key. So the search operation 72 | /// only takes a constant time in the worst case. 73 | /// 74 | /// The insert operation must pay the price for the quick search. The insert operation can only put the key 75 | /// into one of the two slots. However, when both slots are already occupied by other entries, it will be 76 | /// necessary to move other keys to their second locations (or back to their first locations) to make room 77 | /// for the new key, which is called a `relocation`. If the moved key can't be relocated because the other 78 | /// slot of it is also occupied, another `relocation` is required and so on. If relocation is a very long chain 79 | /// or meets a infinite loop, the table should be resized or rehashed. 80 | /// 81 | pub struct LockFreeCuckooHash 82 | where 83 | K: Eq + Hash, 84 | { 85 | /// The inner map will be replaced after resize. 86 | map: AtomicPtr>, 87 | } 88 | 89 | impl std::fmt::Debug for LockFreeCuckooHash 90 | where 91 | K: std::fmt::Debug + Eq + Hash, 92 | V: std::fmt::Debug, 93 | { 94 | #[inline] 95 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 96 | let guard = pin(); 97 | self.load_inner(&guard).fmt(f) 98 | } 99 | } 100 | 101 | impl Default for LockFreeCuckooHash 102 | where 103 | K: Eq + Hash, 104 | { 105 | #[inline] 106 | fn default() -> Self { 107 | Self::new() 108 | } 109 | } 110 | 111 | impl Drop for LockFreeCuckooHash 112 | where 113 | K: Eq + Hash, 114 | { 115 | #[inline] 116 | fn drop(&mut self) { 117 | let guard = pin(); 118 | self.load_inner(&guard).drop_entries(&guard); 119 | unsafe { 120 | drop(self.map.load(Ordering::SeqCst, &guard).into_box()); 121 | } 122 | } 123 | } 124 | 125 | impl<'guard, K, V> LockFreeCuckooHash 126 | where 127 | K: 'guard + Eq + Hash, 128 | { 129 | /// The default capacity of a new `LockFreeCuckooHash` when created by `LockFreeHashMap::new()`. 130 | pub const DEFAULT_CAPACITY: usize = 16; 131 | 132 | /// Create an empty `LockFreeCuckooHash` with default capacity. 133 | #[must_use] 134 | #[inline] 135 | pub fn new() -> Self { 136 | Self::with_capacity(Self::DEFAULT_CAPACITY) 137 | } 138 | 139 | /// Creates an empty `LockFreeCuckooHash` with the specified capacity. 140 | #[must_use] 141 | #[inline] 142 | pub fn with_capacity(capacity: usize) -> Self { 143 | Self { 144 | map: AtomicPtr::new(map_inner::MapInner::with_capacity( 145 | capacity, 146 | [RandomState::new(), RandomState::new()], 147 | )), 148 | } 149 | } 150 | 151 | /// Returns the capacity of this hash table. 152 | #[inline] 153 | pub fn capacity(&self) -> usize { 154 | let guard = pin(); 155 | self.load_inner(&guard).capacity() 156 | } 157 | 158 | /// Returns the number of used slots of this hash table. 159 | #[inline] 160 | pub fn size(&self) -> usize { 161 | let guard = pin(); 162 | self.load_inner(&guard).size() 163 | } 164 | 165 | /// # Safety 166 | /// 167 | /// Clear the hashmap with the specified capacity. 168 | /// The caller must make sure the hashmap is not during a resize. 169 | #[inline] 170 | pub unsafe fn clear(&self) { 171 | let cap = self.capacity(); 172 | self.clear_with_capacity(cap); 173 | } 174 | 175 | /// # Safety 176 | /// 177 | /// Clear the hashmap with the specified capacity. 178 | /// The caller must make sure the hashmap is not during a resize. 179 | #[inline] 180 | pub unsafe fn clear_with_capacity(&self, capacity: usize) { 181 | let guard = pin(); 182 | let new_map = SharedPtr::from_box(Box::new(map_inner::MapInner::::with_capacity( 183 | capacity, 184 | [RandomState::new(), RandomState::new()], 185 | ))); 186 | loop { 187 | let current_map = self.map.load(Ordering::SeqCst, &guard); 188 | match self 189 | .map 190 | .compare_and_set(current_map, new_map, Ordering::SeqCst, &guard) 191 | { 192 | Ok(old_map) => { 193 | guard.defer_unchecked(move || { 194 | drop(old_map.into_box()); 195 | }); 196 | break; 197 | } 198 | Err(_) => { 199 | continue; 200 | } 201 | } 202 | } 203 | } 204 | 205 | /// Returns a reference to the value corresponding to the key. 206 | /// 207 | /// # Example: 208 | /// 209 | /// ``` 210 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 211 | /// let map = LockFreeCuckooHash::new(); 212 | /// map.insert(1, "a"); 213 | /// let guard = pin(); 214 | /// let v = map.get(&1, &guard); 215 | /// assert_eq!(v, Some(&"a")); 216 | /// ``` 217 | /// 218 | #[inline] 219 | pub fn get(&self, key: &Q, guard: &'guard Guard) -> Option<&'guard V> 220 | where 221 | K: Borrow, 222 | Q: Hash + Eq, 223 | { 224 | self.load_inner(guard) 225 | .search(key, guard) 226 | .map(|pair| &pair.value) 227 | } 228 | 229 | /// Returns the key-value pair corresponding to the supplied key. 230 | /// 231 | /// # Example 232 | /// 233 | /// ``` 234 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 235 | /// let map = LockFreeCuckooHash::new(); 236 | /// map.insert(1, "a"); 237 | /// let guard = pin(); 238 | /// let v = map.get_key_value(&1, &guard); 239 | /// assert_eq!(v, Some((&1, &"a"))); 240 | /// ``` 241 | /// 242 | #[inline] 243 | pub fn get_key_value( 244 | &self, 245 | key: &Q, 246 | guard: &'guard Guard, 247 | ) -> Option<(&'guard K, &'guard V)> 248 | where 249 | K: Borrow, 250 | Q: Hash + Eq, 251 | { 252 | self.load_inner(guard) 253 | .search(key, guard) 254 | .map(|pair| (&pair.key, &pair.value)) 255 | } 256 | 257 | /// Returns `true` if the map contains a value for the specified key. 258 | /// 259 | /// # Example 260 | /// ``` 261 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 262 | /// let map = LockFreeCuckooHash::new(); 263 | /// map.insert(1, "a"); 264 | /// assert_eq!(map.contains_key(&1), true); 265 | /// assert_eq!(map.contains_key(&2), false); 266 | /// ``` 267 | /// 268 | #[inline] 269 | pub fn contains_key(&self, key: &Q) -> bool 270 | where 271 | K: Borrow, 272 | Q: Hash + Eq, 273 | { 274 | let guard = pin(); 275 | self.get_key_value(key, &guard).is_some() 276 | } 277 | 278 | /// Insert a new key-value pair into the map. 279 | /// If the map did not have this key present, `false` is returned. 280 | /// If the map did have this key present, the value is updated, and `true` is returned. 281 | /// If you want to get the replaced value, try `insert_with_guard` instead. 282 | /// 283 | /// # Example: 284 | /// 285 | /// ``` 286 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 287 | /// let map = LockFreeCuckooHash::new(); 288 | /// assert_eq!(map.insert(1, "a"), false); 289 | /// assert_eq!(map.insert(2, "b"), false); 290 | /// assert_eq!(map.insert(1, "aaa"), true); 291 | /// ``` 292 | /// 293 | #[inline] 294 | pub fn insert(&self, key: K, value: V) -> bool { 295 | let guard = pin(); 296 | self.insert_with_guard(key, value, &guard).is_some() 297 | } 298 | 299 | /// Insert a new key-value pair into the map. 300 | /// If the map did not have this key present, `None` is returned. 301 | /// If the map did have this key present, the value is updated, and the reference to the old value is returned. 302 | /// Different from `insert(k, v)`, this method requires a user provided guard. 303 | /// 304 | /// # Example: 305 | /// 306 | /// ``` 307 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 308 | /// let map = LockFreeCuckooHash::new(); 309 | /// let guard = pin(); 310 | /// assert_eq!(map.insert_with_guard(1, "a", &guard), None); 311 | /// assert_eq!(map.insert_with_guard(2, "b", &guard), None); 312 | /// assert_eq!(map.insert_with_guard(1, "abc", &guard), Some(&"a")); 313 | /// ``` 314 | /// 315 | #[inline] 316 | pub fn insert_with_guard(&self, key: K, value: V, guard: &'guard Guard) -> Option<&'guard V> { 317 | let kvpair = SharedPtr::from_box(Box::new(map_inner::KVPair { key, value })); 318 | loop { 319 | match self.load_inner(guard).insert( 320 | kvpair, 321 | map_inner::InsertType::InsertOrReplace, 322 | &self.map, 323 | guard, 324 | ) { 325 | // If `insert` returns `Retry` it means the hashmap has been 326 | // resized, we need to try to insert the kvpair again. 327 | // `InsertOrReplace` shouldn't fail, just retry for `Fail`. 328 | map_inner::InsertResult::Retry | map_inner::InsertResult::Fail(_) => continue, 329 | map_inner::InsertResult::Succ(result) => return result.map(|pair| &pair.value), 330 | } 331 | } 332 | } 333 | 334 | /// Insert a new key-value pair into the map if the map does not contain the key. 335 | /// If the map contains the key, return the old value. 336 | /// If the map does not contain the key, insert the new key-value, and return the new value. 337 | /// 338 | /// Notice: When two concurrent `get_or_insert` methods are trying to insert the same key, 339 | /// only one will succeed. But if a `get_or_insert` and a `insert` are called simultaneously with 340 | /// the same key, the `get_or_insert` may still can insert the key-value pair even if `insert` has 341 | /// already succeeded. 342 | /// 343 | /// An example for concurrent `get_or_insert`s: 344 | /// 345 | ///# Thread A | Thread B 346 | ///# call `get_or_insert(key, A)` | call `get_or_insert(key, B)` 347 | ///# | 348 | ///# return value = A | 349 | ///# | return value = A 350 | /// 351 | /// We can see, only one thread can insert the key-value, and the other will return the old value. 352 | /// 353 | /// An example for concurrent `get_or_insert` and `insert`: 354 | /// 355 | ///# Thread A | Thread B 356 | ///# call `get_or_insert(key, A)` | call `insert(key, B)` 357 | ///# | return value = B 358 | ///# return value = A | 359 | ///# | call `get(key, A)` 360 | ///# | return value = A 361 | /// 362 | /// We can see here, even if Thread B has already inserted (key, B) into the map, but Thread A can 363 | /// still insert (key, A), which is not consistent with the semantics of `get_or_insert`. 364 | /// 365 | /// # Example: 366 | /// 367 | /// ``` 368 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 369 | /// let map = LockFreeCuckooHash::new(); 370 | /// let guard = pin(); 371 | /// assert_eq!(map.get_or_insert(1, "a", &guard), &"a"); 372 | /// assert_eq!(map.get_or_insert(1, "b", &guard), &"a"); 373 | /// 374 | /// ``` 375 | #[inline] 376 | #[allow(clippy::unwrap_used)] 377 | pub fn get_or_insert(&self, key: K, value: V, guard: &'guard Guard) -> &'guard V { 378 | let kvpair = SharedPtr::from_box(Box::new(map_inner::KVPair { key, value })); 379 | loop { 380 | match self.load_inner(guard).insert( 381 | kvpair, 382 | map_inner::InsertType::GetOrInsert, 383 | &self.map, 384 | guard, 385 | ) { 386 | // If `insert` returns retry it means the hashmap has been 387 | // resized, we need to try to insert the kvpair again. 388 | map_inner::InsertResult::Retry => continue, 389 | map_inner::InsertResult::Fail(result) => return &result.unwrap().value, 390 | map_inner::InsertResult::Succ(_) => { 391 | // SAFETY: kvpair is generated in the function and lifetime is protected by Guard. 392 | // kvpair is guaranteed to be non-null. 393 | return unsafe { &(*kvpair.as_raw()).value }; 394 | } 395 | } 396 | } 397 | } 398 | 399 | /// Insert a new key-value pair into the map if the map does not contain the key. 400 | /// 401 | /// Notice: similar to `get_or_insert`, when two concurent `insert_if_not_exists` are 402 | /// called, only one will succeed. But when concurrent `insert_if_not_exists` and `insert` 403 | /// are called, `insert_if_not_exists` may still succeed even if `insert` has already inserted 404 | /// the pair. 405 | /// 406 | /// # Example: 407 | /// 408 | /// ``` 409 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 410 | /// let map = LockFreeCuckooHash::new(); 411 | /// let guard = pin(); 412 | /// assert_eq!(map.insert_if_not_exists(1, "a"), true); 413 | /// assert_eq!(map.get(&1, &guard), Some(&"a")); 414 | /// assert_eq!(map.insert_if_not_exists(1, "b"), false); 415 | /// assert_eq!(map.get(&1, &guard), Some(&"a")); 416 | /// ``` 417 | #[inline] 418 | pub fn insert_if_not_exists(&self, key: K, value: V) -> bool { 419 | let guard = &pin(); 420 | let kvpair = SharedPtr::from_box(Box::new(map_inner::KVPair { key, value })); 421 | loop { 422 | match self.load_inner(guard).insert( 423 | kvpair, 424 | map_inner::InsertType::GetOrInsert, 425 | &self.map, 426 | guard, 427 | ) { 428 | // If `insert` returns result it means the hashmap has been 429 | // resized, we need to try to insert the kvpair again. 430 | map_inner::InsertResult::Retry => continue, 431 | map_inner::InsertResult::Fail(_) => return false, 432 | map_inner::InsertResult::Succ(_) => return true, 433 | } 434 | } 435 | } 436 | 437 | /// Compare the current value with `old_value`, update the value to `new_value` if 438 | /// they are equal. 439 | /// This method returns true if the update succeeds, otherwise returns false. 440 | /// 441 | /// # Example: 442 | /// 443 | /// ``` 444 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 445 | /// let map = LockFreeCuckooHash::new(); 446 | /// let guard = pin(); 447 | /// assert_eq!(map.insert(1, "a"), false); 448 | /// assert_eq!(map.compare_and_update(1, "c", &"b"), false); 449 | /// assert_eq!(map.get(&1, &guard), Some(&"a")); 450 | /// assert_eq!(map.compare_and_update(1, "c", &"a"), true); 451 | /// assert_eq!(map.get(&1, &guard), Some(&"c")); 452 | /// ``` 453 | #[inline] 454 | pub fn compare_and_update(&self, key: K, new_value: V, old_value: &V) -> bool 455 | where 456 | V: PartialEq, 457 | { 458 | let guard = &pin(); 459 | let kvpair = SharedPtr::from_box(Box::new(map_inner::KVPair { 460 | key, 461 | value: new_value, 462 | })); 463 | let compare_fn: fn(&V, &V) -> bool = V::eq; 464 | loop { 465 | match self.load_inner(guard).insert( 466 | kvpair, 467 | map_inner::InsertType::CompareAndUpdate(old_value, compare_fn), 468 | &self.map, 469 | guard, 470 | ) { 471 | // If `insert` returns false it means the hashmap has been 472 | // resized, we need to try to insert the kvpair again. 473 | map_inner::InsertResult::Retry => continue, 474 | map_inner::InsertResult::Fail(_) => return false, 475 | map_inner::InsertResult::Succ(_) => return true, 476 | } 477 | } 478 | } 479 | 480 | /// Conditional update helper function. 481 | fn update_on_helper( 482 | &self, 483 | key: K, 484 | new_value: V, 485 | compare_fn: fn(&V, &V) -> bool, 486 | force_insert: bool, 487 | guard: &'guard Guard, 488 | ) -> (bool, Option<&'guard V>) { 489 | let kvpair = SharedPtr::from_box(Box::new(map_inner::KVPair { 490 | key, 491 | value: new_value, 492 | })); 493 | loop { 494 | match self.load_inner(guard).insert( 495 | kvpair, 496 | map_inner::InsertType::UpdateOn(compare_fn, force_insert), 497 | &self.map, 498 | guard, 499 | ) { 500 | // If `insert` returns false it means the hashmap has been 501 | // resized, we need to try to insert the kvpair again. 502 | map_inner::InsertResult::Retry => continue, 503 | map_inner::InsertResult::Fail(result) => { 504 | return (false, result.map(|kv| &kv.value)); 505 | } 506 | map_inner::InsertResult::Succ(result) => { 507 | return (true, result.map(|kv| &kv.value)); 508 | } 509 | } 510 | } 511 | } 512 | 513 | /// If the key exist, update the current value to `new_value` if the compare function return true 514 | /// otherwise no change. 515 | /// This method returns a `(bool, Option<&V>)` tuple. 516 | /// `bool` indicates if the value is updated. 517 | /// `Option<&V>` is the old value. 518 | /// 519 | /// # Example: 520 | /// 521 | /// ``` 522 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 523 | /// let map = LockFreeCuckooHash::new(); 524 | /// let guard = &pin(); 525 | /// assert_eq!(map.update_on(1, 5, |v1, v2| { v1 < v2 }, guard), (false, None)); 526 | /// assert_eq!(map.insert(1, 10), false); 527 | /// assert_eq!(map.update_on(1, 5, |v1, v2| { v1 < v2 }, guard), (false, Some(&10))); 528 | /// assert_eq!(map.update_on(1, 20, |v1, v2| { v1 < v2 }, guard), (true, Some(&10))); 529 | /// assert_eq!(map.get(&1, guard), Some(&20)); 530 | /// ``` 531 | #[inline] 532 | pub fn update_on( 533 | &self, 534 | key: K, 535 | new_value: V, 536 | compare_fn: fn(&V, &V) -> bool, 537 | guard: &'guard Guard, 538 | ) -> (bool, Option<&'guard V>) { 539 | self.update_on_helper(key, new_value, compare_fn, false, guard) 540 | } 541 | 542 | /// If the key exist, update the current value to `new_value` if the compare function return true 543 | /// otherwise insert the key. 544 | /// This method returns a `(bool, Option<&V>)` tuple. 545 | /// `bool` indicates if the value is updated or inserted. 546 | /// `Option<&V>` is the old value. 547 | /// 548 | /// # Example: 549 | /// 550 | /// ``` 551 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 552 | /// let map = LockFreeCuckooHash::new(); 553 | /// let guard = &pin(); 554 | /// assert_eq!(map.insert_or_update_on(1, 10, |v1, v2| { v1 < v2 }, guard), (true, None)); 555 | /// assert_eq!(map.insert_or_update_on(1, 5, |v1, v2| { v1 < v2 }, guard), (false, Some(&10))); 556 | /// assert_eq!(map.insert_or_update_on(1, 20, |v1, v2| { v1 < v2 }, guard), (true, Some(&10))); 557 | /// assert_eq!(map.get(&1, guard), Some(&20)); 558 | /// ``` 559 | #[inline] 560 | pub fn insert_or_update_on( 561 | &self, 562 | key: K, 563 | new_value: V, 564 | compare_fn: fn(&V, &V) -> bool, 565 | guard: &'guard Guard, 566 | ) -> (bool, Option<&'guard V>) { 567 | self.update_on_helper(key, new_value, compare_fn, true, guard) 568 | } 569 | 570 | /// Removes a key from the map, returning `true` if the key was previously in the map. 571 | /// If you want to get the old value, try `map.remove_with_guard()` instead. 572 | /// 573 | /// # Example: 574 | /// 575 | /// ``` 576 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 577 | /// let map = LockFreeCuckooHash::new(); 578 | /// map.insert(1, "a"); 579 | /// assert_eq!(map.remove(&2), false); 580 | /// assert_eq!(map.remove(&1), true); 581 | /// assert_eq!(map.remove(&1), false); 582 | /// ``` 583 | /// 584 | #[inline] 585 | pub fn remove(&self, key: &Q) -> bool 586 | where 587 | K: Borrow, 588 | Q: Hash + Eq, 589 | { 590 | let guard = pin(); 591 | self.remove_with_guard(key, &guard).is_some() 592 | } 593 | 594 | /// Remove a key from the map. 595 | /// Different from `remove(k)`, this method requires a user provided guard. 596 | /// 597 | /// # Example: 598 | /// 599 | /// ``` 600 | /// use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 601 | /// let map = LockFreeCuckooHash::new(); 602 | /// let guard = pin(); 603 | /// map.insert(1, "a"); 604 | /// assert_eq!(map.remove_with_guard(&2, &guard), None); 605 | /// assert_eq!(map.remove_with_guard(&1, &guard), Some(&"a")); 606 | /// assert_eq!(map.remove_with_guard(&1, &guard), None); 607 | /// ``` 608 | /// 609 | #[inline] 610 | pub fn remove_with_guard(&self, key: &Q, guard: &'guard Guard) -> Option<&'guard V> 611 | where 612 | K: Borrow, 613 | Q: Hash + Eq, 614 | { 615 | loop { 616 | match self.load_inner(guard).remove(key, &self.map, guard) { 617 | map_inner::RemoveResult::Retry => continue, 618 | map_inner::RemoveResult::Succ(old) => return old.map(|pair| &pair.value), 619 | } 620 | } 621 | } 622 | 623 | /// `load_inner` atomically loads the `MapInner` of hashmap. 624 | fn load_inner(&self, guard: &'guard Guard) -> &'guard map_inner::MapInner { 625 | let raw = self.map.load(Ordering::SeqCst, guard).as_raw(); 626 | // SAFETY: map is always not null, so the unsafe code is safe here. 627 | unsafe { &*raw } 628 | } 629 | } 630 | 631 | #[cfg(test)] 632 | #[allow(clippy::all, clippy::restriction)] 633 | mod tests { 634 | use super::{pin, LockFreeCuckooHash}; 635 | #[test] 636 | fn test_insert() { 637 | let hashtable = LockFreeCuckooHash::new(); 638 | let key: u32 = 1; 639 | let value: u32 = 2; 640 | hashtable.insert(key, value); 641 | let guard = pin(); 642 | let ret = hashtable.get(&key, &guard); 643 | assert!(ret.is_some()); 644 | assert_eq!(*(ret.unwrap()), value); 645 | } 646 | 647 | #[test] 648 | fn test_replace() { 649 | let hashtable = LockFreeCuckooHash::new(); 650 | let key: u32 = 1; 651 | let value0: u32 = 2; 652 | hashtable.insert(key, value0); 653 | let guard = pin(); 654 | let ret0 = hashtable.get(&key, &guard); 655 | assert!(ret0.is_some()); 656 | assert_eq!(*(ret0.unwrap()), value0); 657 | assert_eq!(hashtable.size(), 1); 658 | let value1: u32 = 3; 659 | hashtable.insert(key, value1); 660 | let ret1 = hashtable.get(&key, &guard); 661 | assert!(ret1.is_some()); 662 | assert_eq!(*(ret1.unwrap()), value1); 663 | assert_eq!(hashtable.size(), 1); 664 | } 665 | 666 | #[test] 667 | fn test_get_or_insert() { 668 | let hashtable = LockFreeCuckooHash::new(); 669 | let guard = &pin(); 670 | hashtable.insert(1, 1); 671 | assert_eq!(hashtable.get_or_insert(1, 2, guard), &1); 672 | assert_eq!(hashtable.get_or_insert(2, 3, guard), &3); 673 | assert_eq!(hashtable.get_or_insert(2, 4, guard), &3); 674 | } 675 | 676 | #[test] 677 | fn test_remove() { 678 | let hashtable = LockFreeCuckooHash::new(); 679 | let key = 1; 680 | let value = 2; 681 | let fake_key = 3; 682 | hashtable.insert(key, value); 683 | assert_eq!(hashtable.size(), 1); 684 | assert!(!hashtable.remove(&fake_key)); 685 | assert!(hashtable.remove(&key)); 686 | assert_eq!(hashtable.size(), 0); 687 | } 688 | 689 | #[test] 690 | fn test_clear() { 691 | let hashtable = LockFreeCuckooHash::new(); 692 | let key = 1; 693 | let value = 2; 694 | hashtable.insert(key, value); 695 | let guard = pin(); 696 | let ret = hashtable.get(&key, &guard); 697 | assert!(ret.is_some()); 698 | assert_eq!(*(ret.unwrap()), value); 699 | unsafe { hashtable.clear() }; 700 | let ret = hashtable.get(&key, &guard); 701 | assert!(ret.is_none()); 702 | } 703 | 704 | #[test] 705 | fn test_compare_and_update() { 706 | let hashtable = LockFreeCuckooHash::new(); 707 | let guard = &pin(); 708 | hashtable.insert(1, 10); 709 | assert_eq!(hashtable.compare_and_update(1, 20, &30), false); 710 | assert_eq!(hashtable.get(&1, guard), Some(&10)); 711 | assert_eq!(hashtable.compare_and_update(1, 20, &10), true); 712 | assert_eq!(hashtable.get(&1, guard), Some(&20)); 713 | } 714 | 715 | #[test] 716 | fn test_update_on() { 717 | let hashtable = LockFreeCuckooHash::new(); 718 | let guard = &pin(); 719 | assert_eq!( 720 | hashtable.update_on(1, 5, |v1, v2| { v1 < v2 }, guard), 721 | (false, None) 722 | ); 723 | hashtable.insert(1, 10); 724 | assert_eq!( 725 | hashtable.update_on(1, 5, |v1, v2| { v1 < v2 }, guard), 726 | (false, Some(&10)) 727 | ); 728 | assert_eq!(hashtable.get(&1, guard), Some(&10)); 729 | assert_eq!( 730 | hashtable.update_on(1, 20, |v1, v2| { v1 < v2 }, guard), 731 | (true, Some(&10)) 732 | ); 733 | assert_eq!(hashtable.get(&1, guard), Some(&20)); 734 | } 735 | 736 | #[test] 737 | fn test_insert_or_update_on() { 738 | let hashtable = LockFreeCuckooHash::new(); 739 | let guard = &pin(); 740 | assert_eq!( 741 | hashtable.insert_or_update_on(1, 10, |v1, v2| { v1 < v2 }, guard), 742 | (true, None) 743 | ); 744 | assert_eq!( 745 | hashtable.insert_or_update_on(1, 5, |v1, v2| { v1 < v2 }, guard), 746 | (false, Some(&10)) 747 | ); 748 | assert_eq!( 749 | hashtable.insert_or_update_on(1, 20, |v1, v2| { v1 < v2 }, guard), 750 | (true, Some(&10)) 751 | ); 752 | assert_eq!(hashtable.get(&1, guard), Some(&20)); 753 | } 754 | } 755 | -------------------------------------------------------------------------------- /src/map_inner.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::indexing_slicing)] // TODO: use safe method for indexing and remove this line. 2 | 3 | use super::pointer::{AtomicPtr, SharedPtr}; 4 | use clippy_utilities::{Cast, OverflowArithmetic}; 5 | use crossbeam_epoch::{pin, Guard}; 6 | use std::hash::{BuildHasher, Hash, Hasher}; 7 | use std::sync::atomic::{AtomicUsize, Ordering}; 8 | use std::{borrow::Borrow, collections::hash_map::RandomState}; 9 | 10 | /// `KVPair` contains the key-value pair. 11 | #[derive(Debug)] 12 | pub struct KVPair { 13 | // TODO: maybe cache both hash keys here. 14 | /// `key` is the key of a KV pair. 15 | pub key: K, 16 | /// `value` is the value of a KV pair. 17 | pub value: V, 18 | } 19 | 20 | /// `SlotIndex` represents the index of a slot inside the hashtable. 21 | /// The slot index is composed by `tbl_idx` and `slot_idx`. 22 | #[derive(Clone, Copy, Debug)] 23 | struct SlotIndex { 24 | /// `tbl_idx` is the index of the table. 25 | tbl_idx: usize, 26 | /// `slot_idx` is the index of the slot inside one table. 27 | slot_idx: usize, 28 | } 29 | 30 | /// `SlotState` represents the state of a slot. 31 | /// A slot could be in one of the four states: null, key, reloc and copied. 32 | #[derive(PartialEq)] 33 | enum SlotState { 34 | /// `NullOrKey` means a slot is empty(null) or is occupied by a key-value 35 | /// pair normally without any other flags. 36 | NullOrKey = 0, 37 | /// `Reloc` means a slot is being relocated to the other slot. 38 | Reloc = 1, 39 | /// `Copied` means a slot is being copied to the new map during resize or 40 | /// has been copied to the new map. 41 | Copied = 2, 42 | } 43 | 44 | impl SlotState { 45 | /// `into_u8` converts a `SlotState` to `u8`. 46 | #[allow(clippy::as_conversions)] 47 | const fn into_u8(self) -> u8 { 48 | self as u8 49 | } 50 | 51 | /// `from_u8` converts a `u8` to `SlotState`. 52 | fn from_u8(state: u8) -> Self { 53 | match state { 54 | 0 => Self::NullOrKey, 55 | 1 => Self::Reloc, 56 | 2 => Self::Copied, 57 | _ => panic!("Invalid slot state from u8: {}", state), 58 | } 59 | } 60 | } 61 | 62 | /// `InsertResult` is the returned type of the method `MapInner.insert()` 63 | pub enum InsertResult<'guard, K, V> { 64 | /// The insert operation succeeded, returns `Some(&KVPair)` if the map had the key, 65 | /// otherwise returns `None` if the map does not have that key. 66 | Succ(Option<&'guard KVPair>), 67 | /// The insert operation failed, returns `Some(&KVPair)` if the map had the key, 68 | /// otherwise returns `None` if the map does not have that key. 69 | Fail(Option<&'guard KVPair>), 70 | /// The insert operation might fail because the hashmap has been resized by other writers, 71 | /// so the caller need to retry the insert again. 72 | Retry, 73 | } 74 | 75 | /// `RemoveResult` is the returned type of the method `MapInner.remove()`. 76 | pub enum RemoveResult<'guard, K, V> { 77 | /// The remove operation succeeds, returns `Some(&KVPair)` if the map had the key, 78 | /// otherwise returns `None` if the map does not have that key. 79 | Succ(Option<&'guard KVPair>), 80 | /// The remove operation might fail because the hashmap has been resized by other writers, 81 | /// so the caller need to retry the insert again. 82 | Retry, 83 | } 84 | 85 | /// `InsertType` is the type of a insert operation. 86 | pub enum InsertType<'guard, V> { 87 | /// Insert a new key-value pair if the key does not exist, otherwise replace it. 88 | InsertOrReplace, 89 | /// Get the key-value pair if the key exists, otherwise insert a new one. 90 | GetOrInsert, 91 | /// Compare the current value with the expected one, update it to the new value 92 | /// if they are equal. 93 | /// The parameters for this item are the old_value and compare function 94 | CompareAndUpdate(&'guard V, fn(&V, &V) -> bool), 95 | /// Check the current value with the new value, update it to the new value 96 | /// if the check function return true 97 | /// The parameters for this item is 98 | /// 1. the check function 99 | /// 2. whether need to insert if the key doesn't exist 100 | UpdateOn(fn(&V, &V) -> bool, bool), 101 | } 102 | 103 | /// `FindResult` is the returned type of the method `MapInner.find()`. 104 | /// For more details, see `MapInner.find()`. 105 | struct FindResult<'guard, K, V> { 106 | /// the table index of the slot that has the same key 107 | tbl_idx: Option, 108 | /// the first slot 109 | slot0: SharedPtr<'guard, KVPair>, 110 | /// the second slot 111 | slot1: SharedPtr<'guard, KVPair>, 112 | } 113 | 114 | /// `RelocateResult` is the returned type of the method `MapInner.relocate()`. 115 | enum RelocateResult { 116 | /// The relocation succeeds. 117 | Succ, 118 | /// The relocation fails because the cuckoo path is too long or 119 | /// meets a dead loop. A resize is required for the new insertion. 120 | NeedResize, 121 | /// The map has been resized, should try to insert to the new map. 122 | Resized, 123 | } 124 | 125 | /// `MapInner` is the inner implementation of the `LockFreeCuckooHash`. 126 | pub struct MapInner { 127 | // TODO: support customized hasher. 128 | /// `hash_builders` is used to hash the keys. 129 | hash_builders: [RandomState; 2], 130 | /// `tables` contains the key-value pairs. 131 | tables: Vec>>>, 132 | /// `size` is the number of inserted pairs of the hash map. 133 | size: AtomicUsize, 134 | 135 | // For resize 136 | /// `next_copy_idx` is the next slot idx which need to be copied. 137 | next_copy_idx: AtomicUsize, 138 | /// `copied_num` is the number of copied slots. 139 | copied_num: AtomicUsize, 140 | /// `new_map` is the resized new map. 141 | new_map: AtomicPtr>, 142 | } 143 | 144 | impl std::fmt::Debug for MapInner 145 | where 146 | K: std::fmt::Debug, 147 | V: std::fmt::Debug, 148 | { 149 | // FIXME: This is not thread-safe. 150 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 151 | let capacity = self.tables[0].len(); 152 | let guard = pin(); 153 | let mut debug = f.debug_map(); 154 | for tbl_idx in 0..2 { 155 | for slot_idx in 0..capacity { 156 | let slot = self.tables[tbl_idx][slot_idx].load(Ordering::SeqCst, &guard); 157 | unsafe { 158 | if let Some(kv) = slot.as_raw().as_ref() { 159 | debug.entry(&kv.key, &kv.value); 160 | } 161 | } 162 | } 163 | } 164 | debug.finish() 165 | } 166 | } 167 | 168 | impl<'guard, K, V> MapInner 169 | where 170 | K: 'guard + Eq + Hash, 171 | { 172 | /// `with_capacity` creates a new `MapInner` with specified capacity. 173 | pub fn with_capacity(capacity: usize, hash_builders: [RandomState; 2]) -> Self { 174 | let single_table_capacity = match capacity.checked_add(1) { 175 | Some(cap) => cap.overflow_div(2), 176 | None => capacity.overflow_div(2), 177 | }; 178 | let mut tables = Vec::with_capacity(2); 179 | 180 | for _ in 0_u32..2 { 181 | let mut table = Vec::with_capacity(single_table_capacity); 182 | for _ in 0..single_table_capacity { 183 | table.push(AtomicPtr::null()); 184 | } 185 | tables.push(table); 186 | } 187 | 188 | Self { 189 | hash_builders, 190 | tables, 191 | size: AtomicUsize::new(0), 192 | next_copy_idx: AtomicUsize::new(0), 193 | copied_num: AtomicUsize::new(0), 194 | new_map: AtomicPtr::null(), 195 | } 196 | } 197 | 198 | /// `capacity` returns the current capacity of the hash map. 199 | pub fn capacity(&self) -> usize { 200 | self.tables[0].len().overflow_mul(2) 201 | } 202 | 203 | /// `size` returns the number of inserted pairs of the hash map. 204 | pub fn size(&self) -> usize { 205 | self.size.load(Ordering::SeqCst) 206 | } 207 | 208 | /// `search` searches the value corresponding to the key. 209 | pub fn search(&self, key: &Q, guard: &'guard Guard) -> Option<&'guard KVPair> 210 | where 211 | K: Borrow, 212 | Q: Hash + Eq, 213 | { 214 | // TODO: the second hash value could be lazily evaluated. 215 | let slot_idx = vec![self.get_index(0, key), self.get_index(1, key)]; 216 | // Because other concurrent `insert` operations may relocate the key during 217 | // our `search` here, we may miss the key with one-round query. 218 | // For example, suppose the key is located in `table[1][hash1(key)]` at first: 219 | // 220 | // search thread | relocate thread 221 | // | 222 | // e1 = table[0][hash0(key)] | 223 | // | relocate key from table[1] to table[0] 224 | // e2 = table[1][hash1(key)] | 225 | // | 226 | // both e1 and e2 are empty | 227 | // -> key not exists, return None | 228 | 229 | // So `search` uses a two-round query to deal with the `missing key` problem. 230 | // But it is not enough because a relocation operation might interleave in between. 231 | // The other technique to deal with it is a logic-clock based counter -- `relocation count`. 232 | // Each slot contains a counter that records the number of relocations at the slot. 233 | loop { 234 | let mut counters = Vec::with_capacity(4); 235 | for i in 0_usize..4 { 236 | let (counter, entry, _) = self.get_entry(slot_idx[i.overflowing_rem(2).0], guard); 237 | if let Some(pair) = entry { 238 | if key.eq(pair.key.borrow()) { 239 | return entry; 240 | } 241 | } 242 | counters.push(counter); 243 | } 244 | // Check the counter. 245 | if Self::check_counter(counters[0], counters[1], counters[2], counters[3]) { 246 | continue; 247 | } 248 | break; 249 | } 250 | None 251 | } 252 | 253 | /// Insert a new key-value pair into the hashtable. If the key has already been in the 254 | /// table, the value will be overridden. 255 | /// If the insert operation fails because of the map has been resized, this method returns 256 | /// `WriteResult::Retry`, and the caller need to retry. 257 | #[allow(clippy::needless_pass_by_value, clippy::too_many_lines)] 258 | pub fn insert( 259 | &self, 260 | kvpair: SharedPtr<'guard, KVPair>, 261 | insert_type: InsertType<'guard, V>, 262 | outer_map: &AtomicPtr, 263 | guard: &'guard Guard, 264 | ) -> InsertResult<'guard, K, V> { 265 | let mut new_slot = kvpair; 266 | let (_, new_entry, _) = Self::unwrap_slot(new_slot); 267 | // new_entry is just created from `key`, so the unwrap() is safe here. 268 | let (new_key, new_value) = if let Some(pair) = new_entry { 269 | (&pair.key, &pair.value) 270 | } else { 271 | return InsertResult::Retry; 272 | }; 273 | let slot_idx0 = self.get_index(0, new_key); 274 | let slot_idx1 = self.get_index(1, new_key); 275 | loop { 276 | let find_result = self.find(new_key, slot_idx0, slot_idx1, outer_map, guard); 277 | let (tbl_idx_opt, slot0, slot1) = match find_result { 278 | Some(r) => (r.tbl_idx, r.slot0, r.slot1), 279 | None => return InsertResult::Retry, 280 | }; 281 | let (slot_idx_opt, target_slot, key_exist) = match tbl_idx_opt { 282 | Some(tbl_idx) => { 283 | // The key has already been in the table, we need to replace the value. 284 | if tbl_idx == 0 { 285 | (Some(&slot_idx0), slot0, true) 286 | } else { 287 | (Some(&slot_idx1), slot1, true) 288 | } 289 | } 290 | None => { 291 | // The key is a new one, check if we have an empty slot. 292 | if Self::slot_is_empty(slot0) { 293 | (Some(&slot_idx0), slot0, false) 294 | } else if Self::slot_is_empty(slot1) { 295 | (Some(&slot_idx1), slot1, false) 296 | } else { 297 | // Both slots are occupied, we need a relocation. 298 | (None, slot0, false) 299 | } 300 | } 301 | }; 302 | 303 | let mut need_relocate = false; 304 | 305 | if let Some(slot_idx) = slot_idx_opt { 306 | // We found the key exists or we have an empty slot, 307 | // just replace the slot with the new one. 308 | 309 | match insert_type { 310 | InsertType::GetOrInsert => { 311 | if key_exist { 312 | // The insert type is `GetOrInsert`, but the key exists. 313 | // So we return with a `Get` semantic. 314 | // The new inserted key-value could be dropped immediately 315 | // since no one can read it. 316 | // SAFETY: new_slot is guaranteed to be non-null. 317 | unsafe { 318 | drop(new_slot.into_box()); 319 | } 320 | 321 | return InsertResult::Fail(Self::unwrap_slot(target_slot).1); 322 | } 323 | if slot_idx.tbl_idx != 0 { 324 | // GetOrInsert only inserts key-value pair into the primary slot. 325 | // So if the primary slot is not empty, we force trigger a relocation. 326 | need_relocate = true; 327 | } 328 | } 329 | InsertType::CompareAndUpdate(old_value, compare_fn) => { 330 | // The insert type is `compareAndUpdate`, so we need to check if 331 | // the current value equals to the old_v. 332 | if !key_exist { 333 | // SAFETY: new_slot is guaranteed to be non-null. 334 | unsafe { 335 | drop(new_slot.into_box()); 336 | } 337 | return InsertResult::Fail(None); 338 | } 339 | let (_, entry, _) = Self::unwrap_slot(target_slot); 340 | if let Some(current_pair) = entry { 341 | if !compare_fn(old_value, ¤t_pair.value) { 342 | // SAFETY: new_slot is guaranteed to be non-null. 343 | unsafe { 344 | drop(new_slot.into_box()); 345 | } 346 | return InsertResult::Fail(Some(current_pair)); 347 | } 348 | } 349 | } 350 | InsertType::UpdateOn(compare_fn, force_insert) => { 351 | if !key_exist && !force_insert { 352 | // SAFETY: new_slot is guaranteed to be non-null. 353 | unsafe { 354 | drop(new_slot.into_box()); 355 | } 356 | return InsertResult::Fail(None); 357 | } 358 | let (_, entry, _) = Self::unwrap_slot(target_slot); 359 | if let Some(current_pair) = entry { 360 | if !compare_fn(¤t_pair.value, new_value) { 361 | // SAFETY: new_slot is guaranteed to be non-null. 362 | unsafe { 363 | drop(new_slot.into_box()); 364 | } 365 | return InsertResult::Fail(Some(current_pair)); 366 | } 367 | } 368 | } 369 | InsertType::InsertOrReplace => {} 370 | } 371 | 372 | if !need_relocate { 373 | // update the relocation count. 374 | new_slot = Self::set_rlcount(new_slot, Self::get_rlcount(target_slot), guard); 375 | 376 | match self.tables[slot_idx.tbl_idx][slot_idx.slot_idx].compare_and_set( 377 | target_slot, 378 | new_slot, 379 | Ordering::SeqCst, 380 | guard, 381 | ) { 382 | Ok(old_slot) => { 383 | if !key_exist { 384 | self.size.fetch_add(1, Ordering::SeqCst); 385 | return InsertResult::Succ(None); 386 | } 387 | if old_slot.as_raw() != new_slot.as_raw() { 388 | Self::defer_drop_ifneed(old_slot, guard); 389 | } 390 | return InsertResult::Succ(Self::unwrap_slot(old_slot).1); 391 | } 392 | Err(err) => { 393 | new_slot = err.1; // the snapshot is not valid, try again. 394 | continue; 395 | } 396 | } 397 | } 398 | } else { 399 | need_relocate = true; 400 | } 401 | 402 | if need_relocate { 403 | // We meet a hash collision here, relocate the first slot. 404 | match self.relocate(slot_idx0, outer_map, guard) { 405 | RelocateResult::Succ => continue, 406 | RelocateResult::NeedResize => { 407 | self.resize(outer_map, guard); 408 | return InsertResult::Retry; 409 | } 410 | RelocateResult::Resized => { 411 | return InsertResult::Retry; 412 | } 413 | } 414 | } 415 | } 416 | } 417 | 418 | /// Remove a key from the map. 419 | /// If the remove operation fails because of the map has been resized, this method returns 420 | /// `RemoveResult::Retry`, and the caller need to retry. Otherwise, it will return the removed 421 | /// value if the key exists, or `None` if not. 422 | pub fn remove( 423 | &self, 424 | key: &Q, 425 | outer_map: &AtomicPtr, 426 | guard: &'guard Guard, 427 | ) -> RemoveResult<'guard, K, V> 428 | where 429 | K: Borrow, 430 | Q: Eq + Hash, 431 | { 432 | // TODO: we can return the removed value. 433 | let slot_idx0 = self.get_index(0, key); 434 | let slot_idx1 = self.get_index(1, key); 435 | let new_slot = SharedPtr::null(); 436 | loop { 437 | let find_result = self.find(key, slot_idx0, slot_idx1, outer_map, guard); 438 | let (tbl_idx_opt, slot0, slot1) = match find_result { 439 | Some(r) => (r.tbl_idx, r.slot0, r.slot1), 440 | None => return RemoveResult::Retry, 441 | }; 442 | let tbl_idx = match tbl_idx_opt { 443 | Some(idx) => idx, 444 | None => return RemoveResult::Succ(None), // The key does not exist. 445 | }; 446 | if tbl_idx == 0 { 447 | Self::set_rlcount(new_slot, Self::get_rlcount(slot0), guard); 448 | match self.tables[0][slot_idx0.slot_idx].compare_and_set( 449 | slot0, 450 | new_slot, 451 | Ordering::SeqCst, 452 | guard, 453 | ) { 454 | Ok(old_slot) => { 455 | self.size.fetch_sub(1, Ordering::SeqCst); 456 | Self::defer_drop_ifneed(old_slot, guard); 457 | return RemoveResult::Succ(Self::unwrap_slot(old_slot).1); 458 | } 459 | Err(_) => continue, 460 | } 461 | } 462 | { 463 | if self.tables[0][slot_idx0.slot_idx] 464 | .load(Ordering::SeqCst, guard) 465 | .as_raw() 466 | != slot0.as_raw() 467 | { 468 | continue; 469 | } 470 | Self::set_rlcount(new_slot, Self::get_rlcount(slot1), guard); 471 | match self.tables[1][slot_idx1.slot_idx].compare_and_set( 472 | slot1, 473 | new_slot, 474 | Ordering::SeqCst, 475 | guard, 476 | ) { 477 | Ok(old_slot) => { 478 | self.size.fetch_sub(1, Ordering::SeqCst); 479 | Self::defer_drop_ifneed(old_slot, guard); 480 | return RemoveResult::Succ(Self::unwrap_slot(old_slot).1); 481 | } 482 | Err(_) => continue, 483 | } 484 | } 485 | } 486 | } 487 | 488 | /// `find` is similar to `search`, which searches the value corresponding to the key. 489 | /// The differences are: 490 | /// 1. `find` will help the relocation if the slot is marked. 491 | /// 2. `find` will dedup the duplicated keys. 492 | /// 3. `find` returns an option of `FindResult`. If it return `None`, it means the hashmap 493 | /// has been resized, and the caller need to retry the operation. Otherwise the `FindResult` 494 | /// contains three values: 495 | /// a> the table index of the slot that has the same key. 496 | /// b> the first slot. 497 | /// c> the second slot. 498 | fn find( 499 | &self, 500 | key: &Q, 501 | slot_idx0: SlotIndex, 502 | slot_idx1: SlotIndex, 503 | outer_map: &AtomicPtr, 504 | guard: &'guard Guard, 505 | ) -> Option> 506 | where 507 | K: Borrow, 508 | Q: Eq + Hash, 509 | { 510 | loop { 511 | // Similar to `search`, `find` also uses a two-round search protocol. 512 | // If either of the two rounds finds a slot that contains the key, this method 513 | // returns the table index of that slot. 514 | // If both of the two rounds cannot find the key, we will check the relocation 515 | // count to decide whether we need to retry the two-round search. 516 | 517 | // If `try_find` meets a copied slot (during resize), it will help the resize and 518 | // then returns the `resize0` as true. 519 | // If `try_find` meets a slot which is being relocated, it will help the relocation 520 | // and then returns the `reloc0` as true. Notice that, if the relocation fails because 521 | // `help_relocate` meets a copied slot, `try_find` will return `resize0` as true. 522 | let (fr0, reloc0, resize0) = self.try_find(key, slot_idx0, slot_idx1, outer_map, guard); 523 | 524 | // `try_find` helps finish a resize, so return `None` to let the caller retry 525 | // the opertion with the new resized map. 526 | if resize0 { 527 | return None; 528 | } 529 | 530 | // `try_find` successfully helps a relocation, so we continue the loop to retry the 531 | // two-round search. 532 | if reloc0 { 533 | continue; 534 | } 535 | 536 | // The first round successfully finds a slot that contains the key, so we don't need 537 | // the second round now, just return the result here. 538 | if fr0.tbl_idx.is_some() { 539 | return Some(fr0); 540 | } 541 | 542 | // Otherwise, we need try the second round. 543 | let (fr1, reloc1, resize1) = self.try_find(key, slot_idx0, slot_idx1, outer_map, guard); 544 | if resize1 { 545 | return None; 546 | } 547 | if reloc1 { 548 | continue; 549 | } 550 | if fr1.tbl_idx.is_some() { 551 | return Some(fr1); 552 | } 553 | 554 | // Neither of the tow rounds can find the key, we check the relocation count to determine 555 | // whether we need to retry. 556 | if !Self::check_counter( 557 | Self::get_rlcount(fr0.slot0), 558 | Self::get_rlcount(fr0.slot1), 559 | Self::get_rlcount(fr1.slot0), 560 | Self::get_rlcount(fr1.slot1), 561 | ) { 562 | return Some(FindResult { 563 | tbl_idx: None, 564 | slot0: fr1.slot0, 565 | slot1: fr1.slot1, 566 | }); 567 | } 568 | } 569 | } 570 | 571 | /// `try_find` tries to find the key in the hash map (only once). 572 | /// The returned values are: 573 | /// 1. The find result, including the index of the slot which contains the key, and both slots of the key. 574 | /// 2. Whether we successfully help a relocation. 575 | /// 3. Whether the map has been resized. 576 | fn try_find( 577 | &self, 578 | key: &Q, 579 | slot_idx0: SlotIndex, 580 | slot_idx1: SlotIndex, 581 | outer_map: &AtomicPtr, 582 | guard: &'guard Guard, 583 | ) -> (FindResult<'guard, K, V>, bool, bool) 584 | where 585 | K: Borrow, 586 | Q: Eq + Hash, 587 | { 588 | let mut result = FindResult { 589 | tbl_idx: None, 590 | slot0: SharedPtr::null(), 591 | slot1: SharedPtr::null(), 592 | }; 593 | // Read the first slot at first. 594 | let slot0 = self.get_slot(slot_idx0, guard); 595 | let (_, entry0, state0) = Self::unwrap_slot(slot0); 596 | match state0 { 597 | SlotState::Reloc => { 598 | // The slot is being relocated, we help this relocation. 599 | return if self.help_relocate(slot_idx0, false, true, outer_map, guard) { 600 | // The relocation succeeds, we set the second returned value as `true`. 601 | (result, true, false) 602 | } else { 603 | // The relocation fails, because the table has been resized. 604 | // We set the third returned value as `true`. 605 | (result, false, true) 606 | }; 607 | } 608 | SlotState::Copied => { 609 | // The slot is being copied or has copied to the new map, so we try 610 | // to help the resize, and set the third returned value as `true`. 611 | self.help_resize(outer_map, guard); 612 | return (result, false, true); 613 | } 614 | SlotState::NullOrKey => { 615 | // There is not special flag on the slot, so we compare the keys. 616 | if let Some(pair) = entry0 { 617 | if key.eq(pair.key.borrow()) { 618 | result.tbl_idx = Some(0); 619 | // We successfully match the searched key, but we cannot return here, 620 | // because we may have duplicated keys in both slots. 621 | // We must do the deduplication in this method. 622 | } 623 | } 624 | } 625 | } 626 | // Check the second table. 627 | let slot1 = self.get_slot(slot_idx1, guard); 628 | let (_, entry1, state1) = Self::unwrap_slot(slot1); 629 | match state1 { 630 | SlotState::Reloc => { 631 | return if self.help_relocate(slot_idx1, false, true, outer_map, guard) { 632 | (result, true, false) 633 | } else { 634 | (result, false, true) 635 | }; 636 | } 637 | SlotState::Copied => { 638 | self.help_resize(outer_map, guard); 639 | return (result, false, true); 640 | } 641 | SlotState::NullOrKey => { 642 | if let Some(pair) = entry1 { 643 | if key.eq(pair.key.borrow()) { 644 | if result.tbl_idx.is_some() { 645 | // We have a duplicated key in both slots, 646 | // try to delete the second one. 647 | self.del_dup(slot_idx0, slot_idx1, outer_map, guard); 648 | } else { 649 | // Otherwise, we successfully find the key in the 650 | // second slot, we can return the table index as 1 then. 651 | result.tbl_idx = Some(1); 652 | } 653 | } 654 | } 655 | } 656 | } 657 | 658 | result.slot0 = slot0; 659 | result.slot1 = slot1; 660 | (result, false, false) 661 | } 662 | 663 | /// `help_relocate` helps relocate the slot at `src_idx` to the other corresponding slot. 664 | /// It will first mark the `src_slot`'s state as `Reloc` (when the caller is the initiator), 665 | /// and then try to copy the `src_slot` to the `dst_slot` if `dst_slot` is empty. But if 666 | /// `dst_slot` is not empty, this method will do nothing. 667 | /// This method may fail because one of the slot has been copied to the new map. If so, 668 | /// this method returns false, otherwise returns true. 669 | #[allow(clippy::too_many_lines)] 670 | fn help_relocate( 671 | &self, 672 | src_idx: SlotIndex, 673 | initiator: bool, 674 | need_help_resize: bool, 675 | outer_map: &AtomicPtr, 676 | guard: &'guard Guard, 677 | ) -> bool { 678 | loop { 679 | let mut src_slot = self.get_slot(src_idx, guard); 680 | while initiator && Self::slot_state(src_slot) != SlotState::Reloc { 681 | if Self::slot_state(src_slot) == SlotState::Copied { 682 | if need_help_resize { 683 | self.help_resize(outer_map, guard); 684 | } 685 | return false; 686 | } 687 | if Self::slot_is_empty(src_slot) { 688 | return true; 689 | } 690 | let new_slot_with_reloc = src_slot.with_lower_u2(SlotState::Reloc.into_u8()); 691 | // Only this CAS can set the `Relocation` state! 692 | if self.tables[src_idx.tbl_idx][src_idx.slot_idx] 693 | .compare_and_set(src_slot, new_slot_with_reloc, Ordering::SeqCst, guard) 694 | .is_ok() 695 | { 696 | // do nothing here, the slot state is checked by the while condition. 697 | } 698 | src_slot = self.get_slot(src_idx, guard); 699 | } 700 | 701 | let (src_count, src_entry, src_state) = Self::unwrap_slot(src_slot); 702 | match src_state { 703 | SlotState::NullOrKey => { 704 | return true; 705 | } 706 | SlotState::Copied => { 707 | if need_help_resize { 708 | self.help_resize(outer_map, guard); 709 | } 710 | return false; 711 | } 712 | SlotState::Reloc => {} 713 | } 714 | let src_key = if let Some(pair) = src_entry { 715 | &pair.key 716 | } else { 717 | // The slot is empty, no need for a relocation. 718 | return true; 719 | }; 720 | let dst_idx = self.get_index(1_usize.overflow_sub(src_idx.tbl_idx), src_key); 721 | let dst_slot = self.get_slot(dst_idx, guard); 722 | let (dst_count, dst_entry, dst_state) = Self::unwrap_slot(dst_slot); 723 | if dst_state == SlotState::Copied { 724 | let new_slot_without_mark = src_slot.with_lower_u2(SlotState::NullOrKey.into_u8()); 725 | self.tables[src_idx.tbl_idx][src_idx.slot_idx] 726 | .compare_and_set(src_slot, new_slot_without_mark, Ordering::SeqCst, guard) 727 | .ok(); 728 | if need_help_resize { 729 | self.help_resize(outer_map, guard); 730 | } 731 | return false; 732 | } 733 | if dst_entry.is_none() { 734 | // overflow will be handled by `check_counter`. 735 | let new_count = if src_count > dst_count { 736 | src_count.overflow_add(1) 737 | } else { 738 | dst_count.overflow_add(1) 739 | }; 740 | if self.get_slot(src_idx, guard).as_raw() != src_slot.as_raw() { 741 | continue; 742 | } 743 | let new_slot = Self::set_rlcount(src_slot, new_count, guard) 744 | .with_lower_u2(SlotState::NullOrKey.into_u8()); 745 | 746 | if self.tables[dst_idx.tbl_idx][dst_idx.slot_idx] 747 | .compare_and_set(dst_slot, new_slot, Ordering::SeqCst, guard) 748 | .is_ok() 749 | { 750 | // overflow will be handled by `check_counter`. 751 | let empty_slot = 752 | Self::set_rlcount(SharedPtr::null(), src_count.overflow_add(1), guard); 753 | if self.tables[src_idx.tbl_idx][src_idx.slot_idx] 754 | .compare_and_set(src_slot, empty_slot, Ordering::SeqCst, guard) 755 | .is_ok() 756 | { 757 | // do nothing 758 | } 759 | return true; 760 | } 761 | } 762 | // dst is not null 763 | if src_slot.as_raw() == dst_slot.as_raw() 764 | && Self::slot_state(dst_slot) != SlotState::Reloc 765 | { 766 | // overflow will be handled by `check_counter`. 767 | let empty_slot = 768 | Self::set_rlcount(SharedPtr::null(), src_count.overflow_add(1), guard); 769 | if self.tables[src_idx.tbl_idx][src_idx.slot_idx] 770 | .compare_and_set(src_slot, empty_slot, Ordering::SeqCst, guard) 771 | .is_ok() 772 | { 773 | // do nothing 774 | } 775 | return true; 776 | } 777 | // overflow will be handled by `check_counter`. 778 | let new_slot_without_mark = 779 | Self::set_rlcount(src_slot, src_count.overflow_add(1), guard) 780 | .with_lower_u2(SlotState::NullOrKey.into_u8()); 781 | if self.tables[src_idx.tbl_idx][src_idx.slot_idx] 782 | .compare_and_set(src_slot, new_slot_without_mark, Ordering::SeqCst, guard) 783 | .is_ok() 784 | { 785 | // do nothing 786 | } 787 | return true; 788 | } 789 | } 790 | 791 | /// `resize` resizes the table. 792 | fn resize(&self, outer_map: &AtomicPtr, guard: &'guard Guard) { 793 | if self 794 | .new_map 795 | .load(Ordering::SeqCst, guard) 796 | .as_raw() 797 | .is_null() 798 | { 799 | let new_capacity = self.capacity().saturating_mul(2); 800 | // Allocate the new map. 801 | let new_map = SharedPtr::from_box(Box::new(Self::with_capacity( 802 | new_capacity, 803 | [self.hash_builders[0].clone(), self.hash_builders[1].clone()], 804 | ))); 805 | // Initialize `self.new_map`. 806 | if self 807 | .new_map 808 | .compare_and_set(SharedPtr::null(), new_map, Ordering::SeqCst, guard) 809 | .is_err() 810 | { 811 | // Free the box 812 | // TODO: we can avoid this redundent allocation. 813 | unsafe { 814 | drop(new_map.into_box()); 815 | } 816 | } 817 | } 818 | self.help_resize(outer_map, guard); 819 | } 820 | 821 | /// `help_resize` helps copy the current `MapInner` into `self.new_map`. 822 | fn help_resize(&self, outer_map: &AtomicPtr, guard: &'guard Guard) { 823 | let capacity = self.capacity(); 824 | let capacity_per_table = self.tables[0].len(); 825 | loop { 826 | let next_copy_idx = self.next_copy_idx.fetch_add(1, Ordering::SeqCst); 827 | if next_copy_idx >= capacity { 828 | break; 829 | } 830 | let slot_idx = SlotIndex { 831 | // overflow will never happen here. 832 | tbl_idx: next_copy_idx.overflow_div(capacity_per_table), 833 | slot_idx: next_copy_idx.overflowing_rem(capacity_per_table).0, 834 | }; 835 | self.copy_slot(slot_idx, outer_map, guard); 836 | } 837 | 838 | // waiting for finishing the copy of all the slots. 839 | // Notice: this is not lock-free, because we use a busy-waiting here. 840 | loop { 841 | let copied_num = self.copied_num.load(Ordering::SeqCst); 842 | if copied_num == capacity { 843 | // try to promote the new map 844 | let result = { 845 | let current_map = SharedPtr::from_raw(self); 846 | let new_map = self.new_map.load(Ordering::SeqCst, guard); 847 | outer_map.compare_and_set(current_map, new_map, Ordering::SeqCst, guard) 848 | }; 849 | if let Ok(current_map) = result { 850 | unsafe { 851 | guard.defer_unchecked(move || { 852 | drop(current_map.into_box()); 853 | }); 854 | } 855 | } 856 | break; 857 | } 858 | } 859 | } 860 | 861 | /// `copy_slot` copies a single slot into the new map. 862 | fn copy_slot(&self, slot_idx: SlotIndex, outer_map: &AtomicPtr, guard: &'guard Guard) { 863 | loop { 864 | let slot = self.get_slot(slot_idx, guard); 865 | let (_, _, state) = Self::unwrap_slot(slot); 866 | match state { 867 | SlotState::NullOrKey => { 868 | let new_slot = slot.with_lower_u2(SlotState::Copied.into_u8()); 869 | match self.tables[slot_idx.tbl_idx][slot_idx.slot_idx].compare_and_set( 870 | slot, 871 | new_slot, 872 | Ordering::SeqCst, 873 | guard, 874 | ) { 875 | Ok(_) => { 876 | if !Self::slot_is_empty(new_slot) { 877 | // The insert might fail because the new_map is rezied. 878 | // If so, we need to automically re-load the new_map (which has been resized) 879 | // and try the insert again. 880 | loop { 881 | let new_map = if let Some(new_inner) = unsafe { 882 | self.new_map.load(Ordering::SeqCst, guard).as_raw().as_ref() 883 | } { 884 | new_inner 885 | } else { 886 | // should never be here. 887 | return; 888 | }; 889 | if let InsertResult::Retry = new_map.insert( 890 | SharedPtr::from_raw(slot.as_raw()), 891 | InsertType::InsertOrReplace, 892 | &self.new_map, 893 | guard, 894 | ) { 895 | continue; 896 | } 897 | break; 898 | } 899 | } 900 | self.copied_num.fetch_add(1, Ordering::SeqCst); 901 | break; 902 | } 903 | Err(_) => continue, 904 | } 905 | } 906 | SlotState::Reloc => { 907 | self.help_relocate(slot_idx, false, false, outer_map, guard); 908 | continue; 909 | } 910 | SlotState::Copied => { 911 | // shoule never be here 912 | } 913 | } 914 | } 915 | } 916 | 917 | /// `relocate` tries to make the slot in `origin_idx` empty, in order to insert 918 | /// a new key-value pair into it. 919 | fn relocate( 920 | &self, 921 | origin_idx: SlotIndex, 922 | outer_map: &AtomicPtr, 923 | guard: &'guard Guard, 924 | ) -> RelocateResult { 925 | let threshold = self.relocation_threshold(); 926 | let mut route = Vec::with_capacity(10); // TODO: optimize this. 927 | let mut start_level = 0; 928 | let mut slot_idx = origin_idx; 929 | 930 | // This method consists of two steps: 931 | // 1. Path Discovery 932 | // This step aims to find the cuckoo path which ends with an empty slot, 933 | // so we could swap the empty slot backward to the `origin_idx`. Once the 934 | // slot at `origin_idx` is empty, the new key-value pair can be inserted. 935 | // 2. Swap slot 936 | // When we have discovered a cuckoo path, we can swap the empty slot backward 937 | // to the slot at `origin_idx`. 938 | 939 | 'main_loop: loop { 940 | let mut found = false; 941 | let mut depth = start_level; 942 | loop { 943 | let mut slot = self.get_slot(slot_idx, guard); 944 | while Self::slot_state(slot) == SlotState::Reloc { 945 | if !self.help_relocate(slot_idx, false, true, outer_map, guard) { 946 | return RelocateResult::Resized; 947 | } 948 | slot = self.get_slot(slot_idx, guard); 949 | } 950 | let (_, entry_opt, state) = Self::unwrap_slot(slot); 951 | if state == SlotState::Copied { 952 | self.help_resize(outer_map, guard); 953 | return RelocateResult::Resized; 954 | } 955 | if let Some(entry) = entry_opt { 956 | let key = &entry.key; 957 | 958 | // If there are duplicated keys in both slots, we may 959 | // meet an endless loop. So we must do the dedup here. 960 | let next_slot_idx = self.get_index(1_usize.overflow_sub(slot_idx.tbl_idx), key); 961 | let next_slot = self.get_slot(next_slot_idx, guard); 962 | let (_, next_entry, next_state) = Self::unwrap_slot(next_slot); 963 | if next_state == SlotState::Copied { 964 | self.help_resize(outer_map, guard); 965 | return RelocateResult::Resized; 966 | } 967 | if let Some(pair) = next_entry { 968 | if pair.key.eq(key) { 969 | if slot_idx.tbl_idx == 0 { 970 | self.del_dup(slot_idx, next_slot_idx, outer_map, guard); 971 | } else { 972 | self.del_dup(next_slot_idx, slot_idx, outer_map, guard); 973 | } 974 | } 975 | } 976 | 977 | // push the slot into the cuckoo path. 978 | if route.len() <= depth { 979 | route.push(slot_idx); 980 | } else { 981 | route[depth] = slot_idx; 982 | } 983 | slot_idx = next_slot_idx; 984 | } else { 985 | found = true; 986 | } 987 | depth = depth.overflow_add(1); 988 | if found || depth >= threshold { 989 | break; 990 | } 991 | } 992 | 993 | if found { 994 | depth = depth.overflow_sub(1); 995 | 'slot_swap: for i in (0..depth).rev() { 996 | let src_idx = route[i]; 997 | let mut src_slot = self.get_slot(src_idx, guard); 998 | while Self::slot_state(src_slot) == SlotState::Reloc { 999 | if !self.help_relocate(src_idx, false, true, outer_map, guard) { 1000 | return RelocateResult::Resized; 1001 | } 1002 | src_slot = self.get_slot(src_idx, guard); 1003 | } 1004 | let (_, entry, state) = Self::unwrap_slot(src_slot); 1005 | if state == SlotState::Copied { 1006 | self.help_resize(outer_map, guard); 1007 | return RelocateResult::Resized; 1008 | } 1009 | if let Some(pair) = entry { 1010 | let dst_idx = 1011 | self.get_index(1_usize.overflow_sub(src_idx.tbl_idx), &pair.key); 1012 | let (_, dst_entry, dst_state) = self.get_entry(dst_idx, guard); 1013 | if dst_state == SlotState::Copied { 1014 | self.help_resize(outer_map, guard); 1015 | return RelocateResult::Resized; 1016 | } 1017 | // `dst_entry` should be empty. If it is not, it mains the cuckoo path 1018 | // has been changed by other threads. Go back to complete the path. 1019 | if dst_entry.is_some() { 1020 | start_level = i.overflow_add(1); 1021 | slot_idx = dst_idx; 1022 | continue 'main_loop; 1023 | } 1024 | if !self.help_relocate(src_idx, true, true, outer_map, guard) { 1025 | return RelocateResult::Resized; 1026 | } 1027 | } 1028 | continue 'slot_swap; 1029 | } 1030 | return RelocateResult::Succ; 1031 | } 1032 | return RelocateResult::NeedResize; 1033 | } 1034 | } 1035 | 1036 | /// `del_dup` deletes the duplicated key. It only deletes the key in the second table. 1037 | fn del_dup( 1038 | &self, 1039 | slot_idx0: SlotIndex, 1040 | slot_idx1: SlotIndex, 1041 | outer_map: &AtomicPtr, 1042 | guard: &'guard Guard, 1043 | ) { 1044 | let slot0 = self.get_slot(slot_idx0, guard); 1045 | let slot1 = self.get_slot(slot_idx1, guard); 1046 | 1047 | if Self::slot_state(slot0) == SlotState::Reloc { 1048 | self.help_relocate(slot_idx0, false, false, outer_map, guard); 1049 | return; 1050 | } 1051 | if Self::slot_state(slot0) == SlotState::Copied { 1052 | return; 1053 | } 1054 | if Self::slot_state(slot1) == SlotState::Reloc { 1055 | self.help_relocate(slot_idx1, false, false, outer_map, guard); 1056 | return; 1057 | } 1058 | if Self::slot_state(slot1) == SlotState::Copied { 1059 | return; 1060 | } 1061 | 1062 | if slot1.as_raw() == slot0.as_raw() { 1063 | // FIXME: 1064 | // This is a tricky fix for the duplicated key problem which 1065 | // cannot deduplicate the co-existed key (with the same pointer). 1066 | // This kind of duplicated key is generated by `help_relocate`. 1067 | // We hope another `help_relocate` or `resize` can solve it. 1068 | return; 1069 | } 1070 | 1071 | let (_, entry0, _) = Self::unwrap_slot(slot0); 1072 | let (slot1_count, entry1, _) = Self::unwrap_slot(slot1); 1073 | let mut need_dedup = false; 1074 | if let Some(pair0) = entry0 { 1075 | if let Some(pair1) = entry1 { 1076 | need_dedup = pair0.key.eq(&pair1.key); 1077 | } 1078 | } 1079 | if !need_dedup { 1080 | return; 1081 | } 1082 | let need_free = slot0.as_raw() != slot1.as_raw(); 1083 | let empty_slot = Self::set_rlcount(SharedPtr::null(), slot1_count, guard); 1084 | if let Ok(old_slot) = self.tables[slot_idx1.tbl_idx][slot_idx1.slot_idx].compare_and_set( 1085 | slot1, 1086 | empty_slot, 1087 | Ordering::SeqCst, 1088 | guard, 1089 | ) { 1090 | if need_free { 1091 | self.size.fetch_sub(1, Ordering::SeqCst); 1092 | Self::defer_drop_ifneed(old_slot, guard); 1093 | } 1094 | } 1095 | } 1096 | 1097 | /// `check_counter` checks the relocation count to decide 1098 | /// whether we need to read the slots again. 1099 | fn check_counter(c00_u8: u8, c01_u8: u8, c10_u8: u8, c11_u8: u8) -> bool { 1100 | // Normally, the checked condition should be: 1101 | // c10 >= c00 + 2 && c11 >= c01 + 2 && c11 >= c00 + 3 1102 | // But the relocation count might overflow. If so, we return true. 1103 | 1104 | // FIXME: 1105 | // This method is not really safe for the overflow. 1106 | // For example, 1107 | // c00_u8 = 1 | 1108 | // | slot has been relocated many times, 1109 | // c11_u8 = 257%256 | 1110 | // = 1 | 1111 | // 1112 | // As a result, we cannot detect the overflow. 1113 | // There is a solution for this problem: 1114 | // We use the highest bit of the counter as the overflow flag. 1115 | // The flag will be marked as 1 if the overflow happens. 1116 | // So we can detect the overflow in this method and reset the flag. 1117 | // And no matter how many times the overflow happends, we can detect it. 1118 | 1119 | let (c00, c01, c10, c11) = ( 1120 | c00_u8.cast::(), 1121 | c01_u8.cast::(), 1122 | c10_u8.cast::(), 1123 | c11_u8.cast::(), 1124 | ); 1125 | (c10 < c00) 1126 | || (c11 < c01) 1127 | || (c10 >= c00.overflow_add(2) 1128 | && c11 >= c01.overflow_add(2) 1129 | && c11 >= c00.overflow_add(3)) 1130 | } 1131 | 1132 | /// `drop_entries` drops the entries. 1133 | pub fn drop_entries(&self, guard: &'guard Guard) { 1134 | for i in 0..2 { 1135 | for j in 0..self.tables[0].len() { 1136 | // key might be duplicated, so we only free the one in primary table. 1137 | let slot = self.get_slot( 1138 | SlotIndex { 1139 | tbl_idx: i, 1140 | slot_idx: j, 1141 | }, 1142 | guard, 1143 | ); 1144 | if i == 1 { 1145 | let (_, entry, _) = Self::unwrap_slot(slot); 1146 | if let Some(pair) = entry { 1147 | let primary_slot_idx = self.get_index(0, &pair.key); 1148 | let primary_slot = self.get_slot(primary_slot_idx, guard); 1149 | if primary_slot.as_raw() == slot.as_raw() { 1150 | continue; 1151 | } 1152 | } 1153 | } 1154 | Self::defer_drop_ifneed(slot, guard); 1155 | } 1156 | } 1157 | } 1158 | 1159 | /// `relocation_threshold` returns the threshold of triggering resize. 1160 | fn relocation_threshold(&self) -> usize { 1161 | self.tables[0].len() 1162 | } 1163 | 1164 | /// `slot_state` returns the state of the slot. 1165 | fn slot_state(slot: SharedPtr<'guard, KVPair>) -> SlotState { 1166 | let (_, _, lower_u2) = slot.decompose(); 1167 | SlotState::from_u8(lower_u2) 1168 | } 1169 | 1170 | /// `slot_is_empty` checks if the slot is a null pointer. 1171 | fn slot_is_empty(slot: SharedPtr<'guard, KVPair>) -> bool { 1172 | let raw = slot.as_raw(); 1173 | raw.is_null() 1174 | } 1175 | 1176 | /// `unwrap_slot` unwraps the slot into three parts: 1177 | /// 1. the relocation count 1178 | /// 2. the key value pair 1179 | /// 3. the state of the slot 1180 | fn unwrap_slot( 1181 | slot: SharedPtr<'guard, KVPair>, 1182 | ) -> (u8, Option<&'guard KVPair>, SlotState) { 1183 | let (rlcount, raw, lower_u2) = slot.decompose(); 1184 | let state = SlotState::from_u8(lower_u2); 1185 | unsafe { (rlcount, raw.as_ref(), state) } 1186 | } 1187 | 1188 | /// `set_rlcount` sets the relocation count of a slot. 1189 | fn set_rlcount( 1190 | slot: SharedPtr<'guard, KVPair>, 1191 | rlcount: u8, 1192 | _: &'guard Guard, 1193 | ) -> SharedPtr<'guard, KVPair> { 1194 | slot.with_higher_u8(rlcount) 1195 | } 1196 | 1197 | /// `get_rlcount` returns the relocation count of a slot. 1198 | fn get_rlcount(slot: SharedPtr<'guard, KVPair>) -> u8 { 1199 | let (rlcount, _, _) = slot.decompose(); 1200 | rlcount 1201 | } 1202 | 1203 | /// `get_entry` atomically loads the slot and unwrap it. 1204 | fn get_entry( 1205 | &self, 1206 | slot_idx: SlotIndex, 1207 | guard: &'guard Guard, 1208 | ) -> (u8, Option<&'guard KVPair>, SlotState) { 1209 | // TODO: split this method by different memory ordering. 1210 | Self::unwrap_slot(self.get_slot(slot_idx, guard)) 1211 | } 1212 | 1213 | /// `get_slot` atomically loads the slot. 1214 | fn get_slot( 1215 | &self, 1216 | slot_idx: SlotIndex, 1217 | guard: &'guard Guard, 1218 | ) -> SharedPtr<'guard, KVPair> { 1219 | self.tables[slot_idx.tbl_idx][slot_idx.slot_idx].load(Ordering::SeqCst, guard) 1220 | } 1221 | 1222 | /// `get_index` hashes the key and return the slot index. 1223 | fn get_index(&self, tbl_idx: usize, key: &Q) -> SlotIndex { 1224 | let mut hasher = self.hash_builders[tbl_idx].build_hasher(); 1225 | key.hash(&mut hasher); 1226 | let hash_value = hasher.finish().cast::(); 1227 | // The conversion from u64 to usize will never fail in a 64-bit env. 1228 | // self.tables[0].len() is always non-zero, so the arithmetic is safe here. 1229 | let slot_idx = hash_value.overflowing_rem(self.tables[0].len()).0; 1230 | SlotIndex { tbl_idx, slot_idx } 1231 | } 1232 | 1233 | /// `defer_drop_ifneed` tries to defer to drop the slot if not empty. 1234 | fn defer_drop_ifneed(slot: SharedPtr<'guard, KVPair>, guard: &'guard Guard) { 1235 | if !Self::slot_is_empty(slot) { 1236 | unsafe { 1237 | // We take over the ownership here. 1238 | // Because only one thread can call this method for the same 1239 | // kv-pair, only one thread can take the ownership. 1240 | guard.defer_unchecked(move || { 1241 | drop(slot.into_box()); 1242 | }); 1243 | } 1244 | } 1245 | } 1246 | } 1247 | -------------------------------------------------------------------------------- /src/pointer.rs: -------------------------------------------------------------------------------- 1 | use clippy_utilities::{Cast, OverflowArithmetic}; 2 | use crossbeam_epoch::Guard; 3 | use std::marker::PhantomData; 4 | use std::sync::atomic::{AtomicUsize, Ordering}; 5 | 6 | /// `AtomicPtr` is a pointer which can only be manipulated by 7 | /// atomic operations. 8 | #[derive(Debug)] 9 | pub struct AtomicPtr { 10 | /// `data` is the value of the atomic pointer. 11 | data: AtomicUsize, 12 | /// used for type inference. 13 | _marker: PhantomData<*mut T>, 14 | } 15 | 16 | unsafe impl Send for AtomicPtr {} 17 | unsafe impl Sync for AtomicPtr {} 18 | 19 | impl AtomicPtr { 20 | /// `from_usize` creates an `AtomicPtr` from `usize`. 21 | const fn from_usize(data: usize) -> Self { 22 | Self { 23 | data: AtomicUsize::new(data), 24 | _marker: PhantomData, 25 | } 26 | } 27 | 28 | /// `new` creates the `value` on heap and returns its `AtomicPtr`. 29 | #[allow(clippy::as_conversions)] 30 | pub fn new(value: T) -> Self { 31 | let b = Box::new(value); 32 | let raw_ptr = Box::into_raw(b); 33 | Self::from_usize(raw_ptr as usize) 34 | } 35 | 36 | /// `null` returns a `AtomicPtr` with a null pointer. 37 | pub const fn null() -> Self { 38 | Self::from_usize(0) 39 | } 40 | 41 | /// `load` atomically loads the pointer. 42 | pub fn load<'g>(&self, ord: Ordering, _: &'g Guard) -> SharedPtr<'g, T> { 43 | SharedPtr::from_usize(self.data.load(ord)) 44 | } 45 | 46 | /// `compare_and_set` wraps the `compare_exchange` method of `AtomicUsize`. 47 | pub fn compare_and_set<'g>( 48 | &self, 49 | current_ptr: SharedPtr<'_, T>, 50 | new_ptr: SharedPtr<'_, T>, 51 | ord: Ordering, 52 | _: &'g Guard, 53 | ) -> Result, (SharedPtr<'g, T>, SharedPtr<'g, T>)> { 54 | let new = new_ptr.as_usize(); 55 | // TODO: different ordering. 56 | self.data 57 | .compare_exchange(current_ptr.as_usize(), new, ord, ord) 58 | .map(|_| SharedPtr::from_usize(current_ptr.as_usize())) 59 | .map_err(|current| (SharedPtr::from_usize(current), SharedPtr::from_usize(new))) 60 | } 61 | } 62 | 63 | /// `SharedPtr` is a pointer which can be shared by multi-threads. 64 | /// `SharedPtr` can only be used with 64bit-wide pointer, and the 65 | /// pointer address must be 4-byte aligned. 66 | pub struct SharedPtr<'g, T: 'g> { 67 | /// `data` is the value of the pointers. 68 | /// It will be spilt into three parts: 69 | /// [higher_u8, raw_pointer, lower_u2] 70 | /// 8 bits 54 bits 2 bits 71 | data: usize, 72 | /// used for type inference. 73 | _marker: PhantomData<(&'g (), *const T)>, 74 | } 75 | 76 | impl Clone for SharedPtr<'_, T> { 77 | fn clone(&self) -> Self { 78 | Self { 79 | data: self.data, 80 | _marker: PhantomData, 81 | } 82 | } 83 | } 84 | 85 | impl Copy for SharedPtr<'_, T> {} 86 | 87 | impl SharedPtr<'_, T> { 88 | /// `from_usize` creates a `SharedPtr` from a `usize`. 89 | pub const fn from_usize(data: usize) -> Self { 90 | SharedPtr { 91 | data, 92 | _marker: PhantomData, 93 | } 94 | } 95 | 96 | /// `from_box` creates a `SharedPtr` from a `Box`. 97 | pub fn from_box(b: Box) -> Self { 98 | Self::from_raw(Box::into_raw(b)) 99 | } 100 | 101 | /// `from_raw` creates a `SharedPtr` from a raw pointer. 102 | #[allow(clippy::as_conversions)] 103 | pub fn from_raw(raw: *const T) -> Self { 104 | Self::from_usize(raw as usize) 105 | } 106 | 107 | /// `null` returns a null `SharedPtr`. 108 | pub const fn null() -> Self { 109 | Self::from_usize(0) 110 | } 111 | 112 | /// `into_box` converts the pointer into a Box. 113 | #[must_use] 114 | pub unsafe fn into_box(self) -> Box { 115 | Box::from_raw(self.as_mut_raw()) 116 | } 117 | 118 | /// `as_usize` converts the pointer to `usize`. 119 | pub const fn as_usize(self) -> usize { 120 | self.data 121 | } 122 | 123 | /// `decompose_lower_u2` decomposes the pointer into two parts: 124 | /// 1. the higher 62 bits 125 | /// 2. the lower 2 bits 126 | fn decompose_lower_u2(data: usize) -> (usize, u8) { 127 | let mask: usize = 3; 128 | // The unwrap is safe here, because we have mask the lower 2 bits. 129 | (data & !mask, (data & mask).cast::()) 130 | } 131 | 132 | /// `decompose_higher_u8` decomposes the pointer into two parts: 133 | /// 1. the higher 8 bits 134 | /// 2. the lower 56 bits 135 | fn decompose_higher_u8(data: usize) -> (u8, usize) { 136 | let mask: usize = (1_usize.overflowing_shl(56).0).overflow_sub(1); 137 | // The conversion is safe here, because we have shifted 56 bits. 138 | (data.overflow_shr(56).cast::(), data & mask) 139 | } 140 | 141 | /// `decompose` decomposes the pointer into three parts: 142 | /// 1. the higher 8 bits 143 | /// 2. the raw pointer 144 | /// 3. the lower 2 bits 145 | #[allow(clippy::as_conversions)] 146 | pub fn decompose(self) -> (u8, *const T, u8) { 147 | let data = self.data; 148 | let (higher_u62, lower_u2) = Self::decompose_lower_u2(data); 149 | let (higher_u8, raw_ptr) = Self::decompose_higher_u8(higher_u62); 150 | (higher_u8, raw_ptr as *const T, lower_u2) 151 | } 152 | 153 | /// `as_raw` extracts the raw pointer. 154 | pub fn as_raw(self) -> *const T { 155 | let (_, raw, _) = self.decompose(); 156 | raw 157 | } 158 | 159 | /// `as_mut_raw` extracts the mutable raw pointer. 160 | #[allow(clippy::as_conversions)] 161 | pub fn as_mut_raw(self) -> *mut T { 162 | let const_raw = self.as_raw(); 163 | const_raw as *mut T 164 | } 165 | 166 | /// `with_lower_u2` resets the lower 2 bits of the pointer. 167 | pub fn with_lower_u2(self, lower_u8: u8) -> Self { 168 | let mask: usize = 3; 169 | // Convert a u8 to usize is always safe. 170 | Self::from_usize(self.data & !mask | lower_u8.cast::()) 171 | } 172 | 173 | /// `with_higher_u8` resets the higher 8 bits of pointer. 174 | pub fn with_higher_u8(self, higher_u8: u8) -> Self { 175 | let data = self.data; 176 | let mask: usize = (1_usize.overflowing_shl(56).0).overflow_sub(1); 177 | // Convert a u8 to usize is always safe. 178 | Self::from_usize((data & mask) | ((higher_u8.cast::()).overflowing_shl(56).0)) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/benchmark.rs: -------------------------------------------------------------------------------- 1 | use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 2 | use rand::Rng; 3 | use std::sync::Arc; 4 | use std::time::Instant; 5 | 6 | fn bench( 7 | init_capacity: usize, 8 | capacity: usize, 9 | warmup_factor: f32, 10 | write_factor: f32, 11 | remove_factor: f32, 12 | num_read_per_write: usize, 13 | num_thread: usize, 14 | ) -> (usize, f64) { 15 | assert!(warmup_factor < 1.0); 16 | assert!(write_factor + warmup_factor < 1.0); 17 | assert!(remove_factor < write_factor); 18 | 19 | let warmup_size = (capacity as f32 * warmup_factor) as usize; 20 | 21 | let cuckoo_map = Arc::new(LockFreeCuckooHash::with_capacity(init_capacity)); 22 | 23 | let guard = pin(); 24 | let mut rng = rand::thread_rng(); 25 | 26 | for _ in 0..warmup_size { 27 | let key: u32 = rng.gen(); 28 | let value: u32 = rng.gen(); 29 | 30 | cuckoo_map.insert_with_guard(key, value, &guard); 31 | } 32 | 33 | let insert_per_thread = (capacity as f32 * write_factor) as usize / num_thread; 34 | let remove_per_thread = (capacity as f32 * remove_factor) as usize / num_thread; 35 | 36 | let mut insert_entries = Vec::with_capacity(insert_per_thread * num_thread); 37 | for _ in 0..(insert_per_thread * num_thread) { 38 | let key: u32 = rng.gen(); 39 | let value: u32 = rng.gen(); 40 | insert_entries.push((key, value)); 41 | } 42 | let insert_entries = Arc::new(insert_entries); 43 | let mut handles = Vec::with_capacity(num_thread); 44 | let start = Instant::now(); 45 | for i in 0..num_thread { 46 | let map = cuckoo_map.clone(); 47 | let insert_entries = insert_entries.clone(); 48 | let handle = std::thread::spawn(move || { 49 | let mut remove_flag: f32 = 0.0; 50 | let mut remove_idx = 0; 51 | let num_remove_per_thread = remove_factor / write_factor; 52 | let mut rng = rand::thread_rng(); 53 | let guard = &pin(); 54 | let insert_entries = 55 | &insert_entries[i * insert_per_thread..(i + 1) * insert_per_thread]; 56 | for i in 0..insert_per_thread { 57 | // 1. insert a kv pair 58 | map.insert_with_guard(insert_entries[i].0, insert_entries[i].1, guard); 59 | 60 | // 2. read num_read_per_write kv pairs 61 | for _ in 0..num_read_per_write { 62 | let key_idx: usize = rng.gen::() % (i + 1); 63 | map.get(&insert_entries[key_idx].0, guard); 64 | } 65 | 66 | // 3. remove num_remove_per_write kv pairs 67 | remove_flag += num_remove_per_thread; 68 | if remove_flag >= 1.0 { 69 | map.remove_with_guard(&insert_entries[remove_idx].0, guard); 70 | remove_flag -= 1.0; 71 | remove_idx += 1; 72 | } 73 | } 74 | }); 75 | handles.push(handle); 76 | } 77 | 78 | for handle in handles { 79 | handle.join().unwrap(); 80 | } 81 | 82 | let duration = start.elapsed().as_millis() as f64; 83 | let ops = (insert_per_thread + remove_per_thread + insert_per_thread * num_read_per_write) 84 | * num_thread; 85 | guard.flush(); 86 | (ops, duration) 87 | } 88 | 89 | fn bench_read_write(num_thread: usize) { 90 | let capacity = 10_000_000; 91 | let warmup_factor = 0.01; 92 | let insert_factor = 0.2; 93 | let remove_factor = 0.0; 94 | let num_read_per_write = 19; 95 | let (ops, duration) = bench( 96 | capacity, 97 | capacity, 98 | warmup_factor, 99 | insert_factor, 100 | remove_factor, 101 | num_read_per_write, 102 | num_thread, 103 | ); 104 | 105 | let num_remove_per_write = remove_factor / insert_factor; 106 | 107 | println!( 108 | "num_thread: {}, init_capacity: {}, capacity: {}, ops: {}, search: {}%, insert: {}%, remove: {}%, duration: {}ms, throughput: {}op/ms", 109 | num_thread, capacity, capacity, ops, 110 | num_read_per_write as f32 / (num_read_per_write as f32 + num_remove_per_write + 1.0) * 100.0, 111 | 1.0 / (num_read_per_write as f32 + num_remove_per_write + 1.0)* 100.0, 112 | num_remove_per_write as f32 / (num_read_per_write as f32 + num_remove_per_write + 1.0) * 100.0, 113 | duration, (ops as f64 / duration) 114 | ); 115 | } 116 | 117 | #[test] 118 | #[ignore] 119 | fn bench_read_write_scale() { 120 | let cpu_cores = num_cpus::get(); 121 | println!("Bench: search + insert, num_cpu_cores: {}", cpu_cores); 122 | let mut num_thread = 1; 123 | loop { 124 | if num_thread > cpu_cores { 125 | break; 126 | } 127 | bench_read_write(num_thread); 128 | num_thread *= 2; 129 | } 130 | } 131 | 132 | fn bench_insert_only(num_thread: usize) { 133 | let capacity = 10_000_000; 134 | let warmup_factor = 0.0; 135 | let insert_factor = 0.4; 136 | let remove_factor = 0.0; 137 | let num_read_per_write = 0; 138 | let (ops, duration) = bench( 139 | capacity, 140 | capacity, 141 | warmup_factor, 142 | insert_factor, 143 | remove_factor, 144 | num_read_per_write, 145 | num_thread, 146 | ); 147 | let guard = pin(); 148 | guard.flush(); 149 | println!( 150 | "num_thread: {}, init_capacity: {}, capacity: {}, insert: {}, duration: {}ms, throughput: {}op/ms", 151 | num_thread, capacity, capacity, insert_factor * capacity as f32, 152 | duration, (ops as f64 / duration) 153 | ); 154 | } 155 | 156 | #[test] 157 | #[ignore] 158 | fn bench_insert_scale() { 159 | let cpu_cores = num_cpus::get(); 160 | println!("Bench: insert only, num_cpu_cores: {}", cpu_cores); 161 | let mut num_thread = 1; 162 | loop { 163 | if num_thread > cpu_cores { 164 | break; 165 | } 166 | bench_insert_only(num_thread); 167 | num_thread *= 2; 168 | } 169 | } 170 | 171 | fn bench_insert_resize(num_thread: usize) { 172 | let init_capacity = 5_000_000; 173 | let capacity = 10_000_000; 174 | let warmup_factor = 0.0; 175 | let insert_factor = 0.5; 176 | let remove_factor = 0.0; 177 | let num_read_per_write = 0; 178 | let (ops, duration) = bench( 179 | init_capacity, 180 | capacity, 181 | warmup_factor, 182 | insert_factor, 183 | remove_factor, 184 | num_read_per_write, 185 | num_thread, 186 | ); 187 | let guard = pin(); 188 | guard.flush(); 189 | println!( 190 | "num_thread: {}, init_capacity: {}, capacity: {}, insert: {}, duration: {}ms, throughput: {}op/ms", 191 | num_thread, init_capacity, capacity, insert_factor * capacity as f32, 192 | duration, (ops as f64 / duration) 193 | ); 194 | } 195 | 196 | #[test] 197 | #[ignore] 198 | fn bench_resize_scale() { 199 | let cpu_cores = num_cpus::get(); 200 | println!("Bench: insert with resize, num_cpu_cores: {}", cpu_cores); 201 | let mut num_thread = 1; 202 | loop { 203 | if num_thread > cpu_cores { 204 | break; 205 | } 206 | bench_insert_resize(num_thread); 207 | num_thread *= 2; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use lockfree_cuckoohash::{pin, LockFreeCuckooHash}; 2 | use rand::Rng; 3 | use std::collections::HashMap; 4 | use std::sync::atomic::{AtomicUsize, Ordering}; 5 | use std::sync::Arc; 6 | 7 | #[test] 8 | fn test_single_thread() { 9 | let capacity: usize = 100_000; 10 | let load_factor: f32 = 0.3; 11 | let remove_factor: f32 = 0.1; 12 | let size = (capacity as f32 * load_factor) as usize; 13 | 14 | let mut base_map: HashMap = HashMap::with_capacity(capacity); 15 | let cuckoo_map: LockFreeCuckooHash = LockFreeCuckooHash::with_capacity(capacity); 16 | 17 | let mut rng = rand::thread_rng(); 18 | let guard = pin(); 19 | 20 | for _ in 0..size { 21 | let key: u32 = rng.gen(); 22 | let value: u32 = rng.gen(); 23 | 24 | base_map.insert(key, value); 25 | cuckoo_map.insert_with_guard(key, value, &guard); 26 | 27 | let r: u8 = rng.gen(); 28 | let need_remove = (r % 10) < ((remove_factor * 10_f32) as u8); 29 | if need_remove { 30 | base_map.remove(&key); 31 | cuckoo_map.remove_with_guard(&key, &guard); 32 | } 33 | } 34 | 35 | assert_eq!(base_map.len(), cuckoo_map.size()); 36 | 37 | for (key, value) in base_map { 38 | let value2 = cuckoo_map.get(&key, &guard); 39 | assert_eq!(value, *value2.unwrap()); 40 | } 41 | } 42 | 43 | #[test] 44 | fn test_single_thread_resize() { 45 | let init_capacity: usize = 32; 46 | let size = 1024; 47 | 48 | let mut base_map: HashMap = HashMap::with_capacity(init_capacity); 49 | let cuckoo_map: LockFreeCuckooHash = LockFreeCuckooHash::with_capacity(init_capacity); 50 | 51 | let mut rng = rand::thread_rng(); 52 | let guard = pin(); 53 | 54 | for _ in 0..size { 55 | let mut key: u32 = rng.gen(); 56 | while base_map.contains_key(&key) { 57 | key = rng.gen(); 58 | } 59 | let value: u32 = rng.gen(); 60 | 61 | base_map.insert(key, value); 62 | cuckoo_map.insert_with_guard(key, value, &guard); 63 | } 64 | 65 | assert_eq!(base_map.len(), cuckoo_map.size()); 66 | assert_eq!(cuckoo_map.size(), size); 67 | for (key, value) in base_map { 68 | let value2 = cuckoo_map.get(&key, &guard); 69 | assert_eq!(value, *value2.unwrap()); 70 | } 71 | } 72 | 73 | #[test] 74 | fn test_multi_thread() { 75 | let capacity: usize = 1_000_000; 76 | let load_factor: f32 = 0.2; 77 | let num_thread: usize = 4; 78 | 79 | let size = (capacity as f32 * load_factor) as usize; 80 | let warmup_size = size / 3; 81 | 82 | let mut warmup_entries: Vec<(u32, u32)> = Vec::with_capacity(warmup_size); 83 | 84 | let mut new_insert_entries: Vec<(u32, u32)> = Vec::with_capacity(size - warmup_size); 85 | 86 | let mut base_map: HashMap = HashMap::with_capacity(capacity); 87 | let cuckoo_map: LockFreeCuckooHash = LockFreeCuckooHash::with_capacity(capacity); 88 | 89 | let mut rng = rand::thread_rng(); 90 | let guard = pin(); 91 | 92 | for _ in 0..warmup_size { 93 | let mut key: u32 = rng.gen(); 94 | while base_map.contains_key(&key) { 95 | key = rng.gen(); 96 | } 97 | let value: u32 = rng.gen(); 98 | base_map.insert(key, value); 99 | cuckoo_map.insert_with_guard(key, value, &guard); 100 | warmup_entries.push((key, value)); 101 | } 102 | 103 | for _ in 0..(size - warmup_size) { 104 | let mut key: u32 = rng.gen(); 105 | while base_map.contains_key(&key) { 106 | key = rng.gen(); 107 | } 108 | let value: u32 = rng.gen(); 109 | new_insert_entries.push((key, value)); 110 | base_map.insert(key, value); 111 | } 112 | 113 | let mut handles = Vec::with_capacity(num_thread); 114 | let insert_count = Arc::new(AtomicUsize::new(0)); 115 | let cuckoo_map = Arc::new(cuckoo_map); 116 | let warmup_entries = Arc::new(warmup_entries); 117 | let new_insert_entries = Arc::new(new_insert_entries); 118 | for _ in 0..num_thread { 119 | let insert_count = insert_count.clone(); 120 | let cuckoo_map = cuckoo_map.clone(); 121 | let warmup_entries = warmup_entries.clone(); 122 | let new_insert_entries = new_insert_entries.clone(); 123 | let handle = std::thread::spawn(move || { 124 | let guard = pin(); 125 | let mut entry_idx = insert_count.fetch_add(1, Ordering::SeqCst); 126 | let mut rng = rand::thread_rng(); 127 | while entry_idx < new_insert_entries.len() { 128 | // read 5 pairs ,then insert 1 pair. 129 | for _ in 0..5 { 130 | // let rnd_idx: usize = rng.gen_range(0, warmup_entries.len()); 131 | let rnd_idx: usize = rng.gen_range(0..warmup_entries.len()); 132 | let warmup_entry = &warmup_entries[rnd_idx]; 133 | let res = cuckoo_map.get(&warmup_entry.0, &guard); 134 | assert!(res.is_some()); 135 | assert_eq!(*res.unwrap(), warmup_entry.1); 136 | } 137 | let insert_pair = &new_insert_entries[entry_idx]; 138 | cuckoo_map.insert_with_guard(insert_pair.0, insert_pair.1, &guard); 139 | entry_idx = insert_count.fetch_add(1, Ordering::SeqCst); 140 | } 141 | }); 142 | handles.push(handle); 143 | } 144 | 145 | for handle in handles { 146 | handle.join().unwrap(); 147 | } 148 | 149 | for (k, v) in base_map { 150 | let v2 = cuckoo_map.get(&k, &guard); 151 | assert_eq!(v, *v2.unwrap()); 152 | } 153 | } 154 | 155 | #[test] 156 | fn test_multi_thread_resize() { 157 | let insert_per_thread = 100000; 158 | let num_thread = 8; 159 | let cuckoo_map: LockFreeCuckooHash = LockFreeCuckooHash::with_capacity(8); 160 | let mut base_map: HashMap = HashMap::new(); 161 | let mut entries: Vec<(u32, u32)> = Vec::with_capacity(insert_per_thread * num_thread); 162 | 163 | let mut rng = rand::thread_rng(); 164 | 165 | for _ in 0..(insert_per_thread * num_thread) { 166 | let mut key: u32 = rng.gen(); 167 | while base_map.contains_key(&key) { 168 | key = rng.gen(); 169 | } 170 | let value: u32 = rng.gen(); 171 | base_map.insert(key, value); 172 | entries.push((key, value)); 173 | } 174 | 175 | let mut handles = Vec::with_capacity(num_thread); 176 | let cuckoo_map = Arc::new(cuckoo_map); 177 | let entries = Arc::new(entries); 178 | for thread_idx in 0..num_thread { 179 | let cuckoo_map = cuckoo_map.clone(); 180 | let entries = entries.clone(); 181 | let handle = std::thread::spawn(move || { 182 | let guard = pin(); 183 | let begin = thread_idx * insert_per_thread; 184 | for i in begin..(begin + insert_per_thread) { 185 | cuckoo_map.insert_with_guard(entries[i].0, entries[i].1, &guard); 186 | } 187 | }); 188 | handles.push(handle); 189 | } 190 | 191 | for handle in handles { 192 | handle.join().unwrap(); 193 | } 194 | 195 | let guard = pin(); 196 | assert_eq!(base_map.len(), cuckoo_map.size()); 197 | for (k, v) in base_map { 198 | let v2 = cuckoo_map.get(&k, &guard); 199 | assert!(v2.is_some()); 200 | assert_eq!(v, *v2.unwrap()); 201 | } 202 | } 203 | --------------------------------------------------------------------------------