Blob zrewidował ten Gist . Przejdź do rewizji
1 file changed, 2 insertions
tb_conversation.md
| @@ -1,6 +1,8 @@ | |||
| 1 | 1 | # C++ Atomic Triple Buffer Implementation | |
| 2 | 2 | Model: qwen3.6-27b-mtp@q4_k_s | |
| 3 | + | ||
| 3 | 4 | Created: 7/21/2026, 12:18:42 AM | |
| 5 | + | ||
| 4 | 6 | Exported from: LM Studio 0.4.19+2 | |
| 5 | 7 | ||
| 6 | 8 | ### System | |
Blob zrewidował ten Gist . Przejdź do rewizji
1 file changed, 127 insertions
tb_conversation.md(stworzono plik)
| @@ -0,0 +1,127 @@ | |||
| 1 | + | # C++ Atomic Triple Buffer Implementation | |
| 2 | + | Model: qwen3.6-27b-mtp@q4_k_s | |
| 3 | + | Created: 7/21/2026, 12:18:42 AM | |
| 4 | + | Exported from: LM Studio 0.4.19+2 | |
| 5 | + | ||
| 6 | + | ### System | |
| 7 | + | Be as concise as possible in your responses. Prefer showing with code over explaining, but do not completely omit explanations. Do not use emojis in your responses. | |
| 8 | + | ||
| 9 | + | ### User | |
| 10 | + | Create a C++ triple buffer class which safely moves data between two threads using atomics. Template this class on the data type to be buffered between the two threads. This data structure should have the same semantics as a "double buffer" common in graphics, where the swapped buffer always contains the latest data. This should not behave like a queue with 3 slots. | |
| 11 | + | ||
| 12 | + | ### Assistant | |
| 13 | + | ```cpp | |
| 14 | + | #include <atomic> | |
| 15 | + | #include <cstdint> | |
| 16 | + | ||
| 17 | + | template<typename T> | |
| 18 | + | class TripleBuffer { | |
| 19 | + | alignas(64) T buffers[3]; | |
| 20 | + | std::atomic<uint8_t> state{0}; // 2 bits/buf: 0=free, 1=writing, 2=readable, 3=reading | |
| 21 | + | ||
| 22 | + | static constexpr uint8_t FREE = 0, WRITING = 1, READABLE = 2, READING = 3; | |
| 23 | + | static constexpr uint8_t MASKS[] = {0x03, 0x0C, 0x30}; | |
| 24 | + | static constexpr uint8_t SHIFTS[] = {0, 2, 4}; | |
| 25 | + | ||
| 26 | + | public: | |
| 27 | + | void produce(const T& data) { | |
| 28 | + | uint8_t old_state, new_state; | |
| 29 | + | int idx = -1; | |
| 30 | + | do { | |
| 31 | + | old_state = state.load(std::memory_order_relaxed); | |
| 32 | + | for (int i = 0; i < 3; ++i) | |
| 33 | + | if ((old_state & MASKS[i]) == FREE) { idx = i; break; } | |
| 34 | + | if (idx == -1) continue; // Spin until a slot frees up | |
| 35 | + | new_state = old_state | (WRITING << SHIFTS[idx]); | |
| 36 | + | } while (!state.compare_exchange_weak(old_state, new_state, | |
| 37 | + | std::memory_order_acq_rel, std::memory_order_relaxed)); | |
| 38 | + | ||
| 39 | + | buffers[idx] = data; | |
| 40 | + | ||
| 41 | + | do { | |
| 42 | + | old_state = state.load(std::memory_order_relaxed); | |
| 43 | + | if ((old_state & MASKS[idx]) != WRITING) break; | |
| 44 | + | new_state = (old_state & ~MASKS[idx]) | (READABLE << SHIFTS[idx]); | |
| 45 | + | } while (!state.compare_exchange_weak(old_state, new_state, | |
| 46 | + | std::memory_order_acq_rel, std::memory_order_relaxed)); | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | bool consume(T& out) { | |
| 50 | + | uint8_t old_state, new_state; | |
| 51 | + | int idx = -1; | |
| 52 | + | do { | |
| 53 | + | old_state = state.load(std::memory_order_relaxed); | |
| 54 | + | for (int i = 0; i < 3; ++i) | |
| 55 | + | if ((old_state & MASKS[i]) == READABLE) { idx = i; break; } | |
| 56 | + | if (idx == -1) return false; // No complete frame available | |
| 57 | + | new_state = old_state | (READING << SHIFTS[idx]); | |
| 58 | + | } while (!state.compare_exchange_weak(old_state, new_state, | |
| 59 | + | std::memory_order_acq_rel, std::memory_order_relaxed)); | |
| 60 | + | ||
| 61 | + | out = buffers[idx]; | |
| 62 | + | ||
| 63 | + | do { | |
| 64 | + | old_state = state.load(std::memory_order_relaxed); | |
| 65 | + | if ((old_state & MASKS[idx]) != READING) break; | |
| 66 | + | new_state = old_state & ~MASKS[idx]; // Mark FREE | |
| 67 | + | } while (!state.compare_exchange_weak(old_state, new_state, | |
| 68 | + | std::memory_order_acq_rel, std::memory_order_relaxed)); | |
| 69 | + | ||
| 70 | + | return true; | |
| 71 | + | } | |
| 72 | + | }; | |
| 73 | + | ``` | |
| 74 | + | ||
| 75 | + | **Semantics:** | |
| 76 | + | - Producer claims a `FREE` slot → writes data → marks `READABLE`. Consumer claims `READABLE` → copies data → marks `FREE`. | |
| 77 | + | - The third buffer acts as a spare. If the consumer hasn't finished reading when the producer finishes writing, the old frame waits in the spare instead of being overwritten. This preserves double-buffer swap semantics (consumer always sees complete data) without blocking either thread. | |
| 78 | + | - `alignas(64)` prevents false sharing. `acq_rel` on CAS ensures cross-thread visibility of state and payload. | |
| 79 | + | ||
| 80 | + | ### User | |
| 81 | + | this is fucking insane and there's no way it's correct | |
| 82 | + | ||
| 83 | + | ### Assistant | |
| 84 | + | ```cpp | |
| 85 | + | #include <atomic> | |
| 86 | + | #include <cstdint> | |
| 87 | + | ||
| 88 | + | template<typename T> | |
| 89 | + | class TripleBuffer { | |
| 90 | + | struct alignas(64) Slot { T data; }; | |
| 91 | + | Slot buffers[3]; | |
| 92 | + | ||
| 93 | + | std::atomic<uint8_t> front{0}; | |
| 94 | + | std::atomic<uint8_t> back{1}; | |
| 95 | + | std::atomic<bool> ready{false}; | |
| 96 | + | static constexpr uint8_t NEXT[] = {1, 2, 0}; | |
| 97 | + | ||
| 98 | + | public: | |
| 99 | + | void produce(const T& data) { | |
| 100 | + | uint8_t b = back.load(std::memory_order_relaxed); | |
| 101 | + | buffers[b].data = data; | |
| 102 | + | ready.store(true, std::memory_order_release); | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | bool consume(T& out) { | |
| 106 | + | if (!ready.load(std::memory_order_acquire)) return false; | |
| 107 | + | ||
| 108 | + | uint8_t b = back.load(std::memory_order_relaxed); | |
| 109 | + | front.store(b, std::memory_order_relaxed); | |
| 110 | + | back.store(NEXT[b], std::memory_order_relaxed); | |
| 111 | + | ||
| 112 | + | out = buffers[front.load(std::memory_order_relaxed)].data; | |
| 113 | + | ready.store(false, std::memory_order_relaxed); | |
| 114 | + | return true; | |
| 115 | + | } | |
| 116 | + | }; | |
| 117 | + | ``` | |
| 118 | + | ||
| 119 | + | **Why the previous version failed:** | |
| 120 | + | - Bit-packed state transitions used broken `do-while` loops that could exit without updating state. | |
| 121 | + | - Memory ordering mismatched between CAS and actual buffer writes/reads. | |
| 122 | + | - Overly complex for SPSC semantics. | |
| 123 | + | ||
| 124 | + | **How this works:** | |
| 125 | + | - Producer writes to `back`, then signals `ready` with release semantics. | |
| 126 | + | - Consumer checks `ready` (acquire), rotates indices (`front <- back`, `back <- spare`), reads new front, and clears flag. | |
| 127 | + | - Cycles through all 3 buffers: Back → Front → Spare → Back. Guarantees consumer always receives a complete frame without blocking or tearing. Assumes single-producer/single-consumer with one produce call per cycle (standard for graphics triple buffering). | |