├── .gitignore ├── Cargo.toml ├── README.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | *Cargo.lock 2 | target 3 | flamegraph* 4 | perf* 5 | cachegrind* 6 | callgrind* 7 | dhat* 8 | massif* 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "optimistic-cell" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Tyler Neely "] 6 | description = "a simple lock-like structure for low-overhead optimistic concurrency" 7 | license = "GPL-3.0" 8 | repository = "https://github.com/komora-io/optimistic-cell" 9 | keywords = ["multi-threaded", "performance", "occ", "optimistic"] 10 | categories = ["concurrency", "data-structures", "rust-patterns"] 11 | readme = "README.md" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # optimistic-cell 2 | 3 | 4 | 5 | A highly cache-efficient lock-like container for working with concurrent data where 6 | reads may take place optimistically and without modifying any cachelines. 7 | 8 | Due to the fact that reads may access data that races with writes and is 9 | only validated later, only items that are marked as `Copy` may be read optimistically. 10 | 11 | The write guard is essentially just a plain spinlock. 12 | 13 | ```rust 14 | let n: u32 = 128 * 1024 * 1024; 15 | let concurrency = 4; 16 | 17 | let cell = &OptimisticCell::new(0); 18 | let barrier = &std::sync::Barrier::new(concurrency as _); 19 | 20 | let before = std::time::Instant::now(); 21 | std::thread::scope(|s| { 22 | let mut threads = vec![]; 23 | for _ in 0..concurrency { 24 | let thread = s.spawn(move || { 25 | barrier.wait(); 26 | 27 | for _ in 0..n { 28 | let read_1 = cell.read(); 29 | 30 | let mut lock = cell.lock(); 31 | *lock += 1; 32 | drop(lock); 33 | 34 | let read_2 = cell.read(); 35 | 36 | assert_ne!(read_1, read_2); 37 | } 38 | }); 39 | 40 | threads.push(thread); 41 | } 42 | for thread in threads { 43 | thread.join().unwrap(); 44 | } 45 | }); 46 | dbg!(before.elapsed()); 47 | ``` 48 | 49 | Workload scalability is highly dependent on the frequency of writes, and the 50 | duration that a writer may hold the cell locked for before dropping it. 51 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A lock-like structure that allows concurrent access to 2 | //! the contents with very efficient cache coherency behavior. 3 | 4 | use std::cell::UnsafeCell; 5 | use std::ops::{Deref, DerefMut}; 6 | use std::sync::atomic::{fence, AtomicU64, Ordering}; 7 | 8 | const LOCK_BIT: u64 = 1 << 63; 9 | const LOCK_MASK: u64 = u64::MAX ^ LOCK_BIT; 10 | 11 | pub struct OptimisticCell { 12 | guard: AtomicU64, 13 | state: UnsafeCell, 14 | } 15 | 16 | unsafe impl Send for OptimisticCell {} 17 | unsafe impl Sync for OptimisticCell {} 18 | 19 | pub struct OptimisticWriteGuard<'a, T> { 20 | cell: &'a OptimisticCell, 21 | previous_unlocked_guard_state: u64, 22 | } 23 | 24 | impl<'a, T> Deref for OptimisticWriteGuard<'a, T> { 25 | type Target = T; 26 | fn deref(&self) -> &T { 27 | unsafe { &*self.cell.state.get() } 28 | } 29 | } 30 | 31 | impl<'a, T> DerefMut for OptimisticWriteGuard<'a, T> { 32 | fn deref_mut(&mut self) -> &mut T { 33 | unsafe { &mut *self.cell.state.get() } 34 | } 35 | } 36 | 37 | impl<'a, T> Drop for OptimisticWriteGuard<'a, T> { 38 | fn drop(&mut self) { 39 | let new_guard_state = (self.previous_unlocked_guard_state + 1) ^ LOCK_MASK; 40 | let old = self.cell.guard.swap(new_guard_state, Ordering::Release); 41 | assert_eq!(old, self.previous_unlocked_guard_state ^ LOCK_BIT); 42 | } 43 | } 44 | 45 | impl OptimisticCell { 46 | #[inline] 47 | fn status(&self) -> (bool, u64) { 48 | let guard_value = self.guard.load(Ordering::Acquire); 49 | let is_locked = guard_value & LOCK_BIT != 0; 50 | let timestamp = guard_value & LOCK_MASK; 51 | 52 | (is_locked, timestamp) 53 | } 54 | 55 | pub fn new(item: T) -> OptimisticCell { 56 | OptimisticCell { 57 | guard: 0.into(), 58 | state: UnsafeCell::new(item), 59 | } 60 | } 61 | 62 | pub fn read(&self) -> T 63 | where 64 | T: Copy, 65 | { 66 | self.read_with(|item| *item) 67 | } 68 | 69 | pub fn read_with R>(&self, read_function: F) -> R 70 | where 71 | T: Copy, 72 | { 73 | loop { 74 | let (before_is_locked, before_timestamp) = self.status(); 75 | if before_is_locked { 76 | std::hint::spin_loop(); 77 | continue; 78 | } 79 | 80 | let state: &T = unsafe { &*self.state.get() }; 81 | let ret = read_function(state); 82 | 83 | // NB: a Release fence is important for keeping the above read from 84 | // being reordered below this validation check. 85 | fence(Ordering::Release); 86 | 87 | let (after_is_locked, after_timestamp) = self.status(); 88 | 89 | if after_is_locked || after_timestamp != before_timestamp { 90 | std::hint::spin_loop(); 91 | continue; 92 | } 93 | 94 | return ret; 95 | } 96 | } 97 | 98 | pub fn lock(&self) -> OptimisticWriteGuard<'_, T> { 99 | loop { 100 | let prev = self.guard.fetch_or(LOCK_BIT, Ordering::Acquire); 101 | let already_locked = prev & LOCK_BIT != 0; 102 | 103 | if !already_locked { 104 | return OptimisticWriteGuard { 105 | previous_unlocked_guard_state: prev, 106 | cell: self, 107 | }; 108 | } 109 | 110 | std::hint::spin_loop(); 111 | } 112 | } 113 | } 114 | 115 | #[test] 116 | fn concurrent_test() { 117 | let n: u32 = 128 * 1024 * 1024; 118 | let concurrency = 2; 119 | 120 | let cell = &OptimisticCell::new(0); 121 | let barrier = &std::sync::Barrier::new(concurrency as _); 122 | 123 | let before = std::time::Instant::now(); 124 | std::thread::scope(|s| { 125 | let mut threads = vec![]; 126 | for _ in 0..concurrency { 127 | let thread = s.spawn(move || { 128 | barrier.wait(); 129 | 130 | for _ in 0..n { 131 | let read_1 = cell.read(); 132 | 133 | let mut lock = cell.lock(); 134 | *lock += 1; 135 | drop(lock); 136 | 137 | let read_2 = cell.read(); 138 | 139 | assert_ne!(read_1, read_2); 140 | } 141 | }); 142 | 143 | threads.push(thread); 144 | } 145 | for thread in threads { 146 | thread.join().unwrap(); 147 | } 148 | }); 149 | dbg!(before.elapsed()); 150 | } 151 | --------------------------------------------------------------------------------