ZZ zrewidował ten Gist . Przejdź do rewizji
1 file changed, 1 insertion, 1 deletion
Gemma4-C++-to-rust-test.md
| @@ -154,7 +154,7 @@ The `MASKS` and `SHIFTS` constants are used to isolate these 2-bit chunks. For e | |||
| 154 | 154 | #### 2. The Workflow | |
| 155 | 155 | * **Producer (`produce`)**: | |
| 156 | 156 | 1. Scans the bitfield for any buffer marked as `FREE`. | |
| 157 | - | 2. Uses **CAS (Compare-And-Swap)** via `compare_exchange_weak` to atomically change `FREE` $\rightarrow$ `WRITING`. | |
| 157 | + | 2. Uses **CAS (Compare-And-Swap)** via `compare_exchange_weak` to atomically change `FREE` > `WRITING`. | |
| 158 | 158 | 3. Writes the data into that specific buffer index. | |
| 159 | 159 | 4. Uses CAS to change `WRITING` > `READABLE`. | |
| 160 | 160 | * **Consumer (`consume`)**: | |
ZZ zrewidował ten Gist . Przejdź do rewizji
1 file changed, 3 insertions, 3 deletions
Gemma4-C++-to-rust-test.md
| @@ -156,12 +156,12 @@ The `MASKS` and `SHIFTS` constants are used to isolate these 2-bit chunks. For e | |||
| 156 | 156 | 1. Scans the bitfield for any buffer marked as `FREE`. | |
| 157 | 157 | 2. Uses **CAS (Compare-And-Swap)** via `compare_exchange_weak` to atomically change `FREE` $\rightarrow$ `WRITING`. | |
| 158 | 158 | 3. Writes the data into that specific buffer index. | |
| 159 | - | 4. Uses CAS to change `WRITING` $\rightarrow$ `READABLE`. | |
| 159 | + | 4. Uses CAS to change `WRITING` > `READABLE`. | |
| 160 | 160 | * **Consumer (`consume`)**: | |
| 161 | 161 | 1. Scans for any buffer marked as `READABLE`. | |
| 162 | - | 2. Uses CAS to atomically change `READABLE` $\rightarrow$ `READING`. | |
| 162 | + | 2. Uses CAS to atomically change `READABLE` > `READING`. | |
| 163 | 163 | 3. Copies the data out of that buffer index. | |
| 164 | - | 4. Uses CAS to change `READING` $\rightarrow$ `FREE`. | |
| 164 | + | 4. Uses CAS to change `READING` > `FREE`. | |
| 165 | 165 | ||
| 166 | 166 | #### 3. Rust-Specific Details | |
| 167 | 167 | * **`UnsafeCell<T>`**: In Rust, you cannot mutate a value inside a shared reference (`&self`) unless it is wrapped in a type that provides "interior mutability." Since we are handling synchronization ourselves via atomics, `UnsafeCell` is the most efficient way to tell the compiler: *"I know what I'm doing; allow me to mutate this memory."* | |
ZZ zrewidował ten Gist . Przejdź do rewizji
Brak zmian
ZZ zrewidował ten Gist . Przejdź do rewizji
1 file changed, 170 insertions
Gemma4-C++-to-rust-test.md(stworzono plik)
| @@ -0,0 +1,170 @@ | |||
| 1 | + | This is a lock-free **Triple Buffer** implementation. Its primary purpose is to allow one thread (the Producer) to write data and another thread (the Consumer) to read data without blocking each other, while ensuring the consumer always gets the most recent "complete" frame. | |
| 2 | + | ||
| 3 | + | Here is the translation into Rust. | |
| 4 | + | ||
| 5 | + | ### The Rust Implementation | |
| 6 | + | ||
| 7 | + | To implement this in Rust, we need `UnsafeCell` because we are mutating elements of an array across different threads based on a manual state machine rather than using a `Mutex`. We also use `repr(align(64))` to match the C++ `alignas(64)`, which prevents "false sharing" (where the CPU caches multiple buffers in one line, causing performance degradation). | |
| 8 | + | ||
| 9 | + | ```rust | |
| 10 | + | use std::cell::UnsafeCell; | |
| 11 | + | use std::sync::atomic::{AtomicU8, Ordering}; | |
| 12 | + | ||
| 13 | + | #[repr(align(64))] | |
| 14 | + | pub struct TripleBuffer<T> { | |
| 15 | + | // UnsafeCell allows interior mutability for lock-free access | |
| 16 | + | buffers: [UnsafeCell<T>; 3], | |
| 17 | + | state: AtomicU8, | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | impl<T: Copy> TripleBuffer<T> { | |
| 21 | + | const FREE: u8 = 0; | |
| 22 | + | const WRITING: u8 = 1; | |
| 23 | + | const READABLE: u8 = 2; | |
| 24 | + | const READING: u8 = 3; | |
| 25 | + | ||
| 26 | + | const MASKS: [u8; 3] = [0x03, 0x0C, 0x30]; | |
| 27 | + | const SHIFTS: [u8; 3] = [0, 2, 4]; | |
| 28 | + | ||
| 29 | + | pub fn new(initial_value: T) -> Self { | |
| 30 | + | Self { | |
| 31 | + | buffers: [ | |
| 32 | + | UnsafeCell::new(initial_value), | |
| 33 | + | UnsafeCell::new(initial_value), | |
| 34 | + | UnsafeCell::new(initial_value), | |
| 35 | + | ], | |
| 36 | + | state: AtomicU8::new(0), | |
| 37 | + | } | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | pub fn produce(&self, data: T) { | |
| 41 | + | let mut old_state: u8; | |
| 42 | + | let mut idx: usize; | |
| 43 | + | ||
| 44 | + | // Step 1: Find a FREE slot and mark it as WRITING | |
| 45 | + | loop { | |
| 46 | + | old_state = self.state.load(Ordering::Relaxed); | |
| 47 | + | idx = None; | |
| 48 | + | for i in 0..3 { | |
| 49 | + | if (old_state & Self::MASKS[i]) == Self::FREE { | |
| 50 | + | idx = Some(i); | |
| 51 | + | break; | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | let idx = match idx { | |
| 56 | + | Some(i) => i, | |
| 57 | + | None => continue, // Spin until a slot is free | |
| 58 | + | }; | |
| 59 | + | ||
| 60 | + | let new_state = old_state | (Self::WRITING << Self::SHIFTS[idx]); | |
| 61 | + | if self.state.compare_exchange_weak( | |
| 62 | + | old_state, new_state, Ordering::AcqRel, Ordering::Relaxed | |
| 63 | + | ).is_ok() { | |
| 64 | + | break; | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // Step 2: Write data to the buffer (Safe because we hold the WRITING state) | |
| 69 | + | unsafe { | |
| 70 | + | *self.buffers[idx].get() = data; | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // Step 3: Mark the slot as READABLE | |
| 74 | + | loop { | |
| 75 | + | old_state = self.state.load(Ordering::Relaxed); | |
| 76 | + | if (old_state & Self::MASKS[idx]) != Self::WRITING { | |
| 77 | + | break; | |
| 78 | + | } | |
| 79 | + | let new_state = (old_state & !Self::MASKS[idx]) | (Self::READABLE << Self::SHIFTS[idx]); | |
| 80 | + | if self.state.compare_exchange_weak( | |
| 81 | + | old_state, new_state, Ordering::AcqRel, Ordering::Relaxed | |
| 82 | + | ).is_ok() { | |
| 83 | + | break; | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | pub fn consume(&self, out: &mut T) -> bool { | |
| 89 | + | let mut old_state: u8; | |
| 90 | + | let mut idx: usize; | |
| 91 | + | ||
| 92 | + | // Step 1: Find a READABLE slot and mark it as READING | |
| 93 | + | loop { | |
| 94 | + | old_state = self.state.load(Ordering::Relaxed); | |
| 95 | + | idx = None; | |
| 96 | + | for i in 0..3 { | |
| 97 | + | if (old_state & Self::MASKS[i]) == Self::READABLE { | |
| 98 | + | idx = Some(i); | |
| 99 | + | break; | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | let idx = match idx { | |
| 104 | + | Some(i) => i, | |
| 105 | + | None => return false, // No complete frame available | |
| 106 | + | }; | |
| 107 | + | ||
| 108 | + | let new_state = old_state | (Self::READING << Self::SHIFTS[idx]); | |
| 109 | + | if self.state.compare_exchange_weak( | |
| 110 | + | old_state, new_state, Ordering::AcqRel, Ordering::Relaxed | |
| 111 | + | ).is_ok() { | |
| 112 | + | break; | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | ||
| 116 | + | // Step 2: Read data from the buffer (Safe because we hold the READING state) | |
| 117 | + | unsafe { | |
| 118 | + | *out = *self.buffers[idx].get(); | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // Step 3: Mark the slot as FREE | |
| 122 | + | loop { | |
| 123 | + | old_state = self.state.load(Ordering::Relaxed); | |
| 124 | + | if (old_state & Self::MASKS[idx]) != Self::READING { | |
| 125 | + | break; | |
| 126 | + | } | |
| 127 | + | let new_state = old_state & !Self::MASKS[idx]; // Set to FREE (0) | |
| 128 | + | if self.state.compare_exchange_weak( | |
| 129 | + | old_state, new_state, Ordering::AcqRel, Ordering::Relaxed | |
| 130 | + | ).is_ok() { | |
| 131 | + | break; | |
| 132 | + | } | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | true | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | // Required to allow the buffer to be shared between threads | |
| 140 | + | unsafe impl<T: Sync> Sync for TripleBuffer<T> {} | |
| 141 | + | ``` | |
| 142 | + | ||
| 143 | + | ### Explanation of the Logic | |
| 144 | + | ||
| 145 | + | #### 1. The State Machine (The "Bit-Packing" Trick) | |
| 146 | + | Instead of using three separate atomic variables, this code uses a single `u8` as a bitfield. Each buffer is allocated **2 bits** of space: | |
| 147 | + | * `00`: Free | |
| 148 | + | * `01`: Writing (Producer owns it) | |
| 149 | + | * `10`: Readable (Ready for Consumer) | |
| 150 | + | * `11`: Reading (Consumer owns it) | |
| 151 | + | ||
| 152 | + | The `MASKS` and `SHIFTS` constants are used to isolate these 2-bit chunks. For example, Buffer 1 is checked by shifting the state 2 bits to the right and masking with `0x03`. | |
| 153 | + | ||
| 154 | + | #### 2. The Workflow | |
| 155 | + | * **Producer (`produce`)**: | |
| 156 | + | 1. Scans the bitfield for any buffer marked as `FREE`. | |
| 157 | + | 2. Uses **CAS (Compare-And-Swap)** via `compare_exchange_weak` to atomically change `FREE` $\rightarrow$ `WRITING`. | |
| 158 | + | 3. Writes the data into that specific buffer index. | |
| 159 | + | 4. Uses CAS to change `WRITING` $\rightarrow$ `READABLE`. | |
| 160 | + | * **Consumer (`consume`)**: | |
| 161 | + | 1. Scans for any buffer marked as `READABLE`. | |
| 162 | + | 2. Uses CAS to atomically change `READABLE` $\rightarrow$ `READING`. | |
| 163 | + | 3. Copies the data out of that buffer index. | |
| 164 | + | 4. Uses CAS to change `READING` $\rightarrow$ `FREE`. | |
| 165 | + | ||
| 166 | + | #### 3. Rust-Specific Details | |
| 167 | + | * **`UnsafeCell<T>`**: In Rust, you cannot mutate a value inside a shared reference (`&self`) unless it is wrapped in a type that provides "interior mutability." Since we are handling synchronization ourselves via atomics, `UnsafeCell` is the most efficient way to tell the compiler: *"I know what I'm doing; allow me to mutate this memory."* | |
| 168 | + | * **`Ordering::AcqRel`**: This ensures that memory writes performed by the producer before the state change are visible to the consumer after they see the state change. | |
| 169 | + | * **`T: Copy`**: The C++ code uses `buffers[idx] = data`, which performs a copy. In Rust, I added the `Copy` trait bound for simplicity. If `T` is a large struct, you would use `std::ptr::copy_nonoverlapping` or wrap the data in `Box`. | |
| 170 | + | * **Alignment**: `#[repr(align(64))]` ensures the structure starts at a cache line boundary, reducing CPU pipeline stalls caused by multiple cores fighting over the same cache line. | |