SpECTRE Documentation Coverage Report
Current view: top level - Parallel - StaticSpscQueue.hpp Hit Total Coverage
Commit: 107e15b340886ae54549b1baa4bfc92e676f667e Lines: 12 28 42.9 %
Date: 2026-09-17 16:38:56
Legend: Lines: hit not hit

          Line data    Source code
       1           0 : // Distributed under the MIT License.
       2             : // See LICENSE.txt for details.
       3             : 
       4             : /*
       5             :  *The original code is distributed under the following copyright and license:
       6             :  *
       7             :  * Copyright (c) 2020 Erik Rigtorp <erik@rigtorp.se>
       8             :  *
       9             :  * Permission is hereby granted, free of charge, to any person obtaining a copy
      10             :  * of this software and associated documentation files (the "Software"), to deal
      11             :  * in the Software without restriction, including without limitation the rights
      12             :  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      13             :  * copies of the Software, and to permit persons to whom the Software is
      14             :  * furnished to do so, subject to the following conditions:
      15             :  *
      16             :  * The above copyright notice and this permission notice shall be included in
      17             :  * all copies or substantial portions of the Software.
      18             :  *
      19             :  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      20             :  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      21             :  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      22             :  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      23             :  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      24             :  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
      25             :  * SOFTWARE.
      26             :  *
      27             :  *
      28             :  * SXS Modifications:
      29             :  * 1. Casing to match SpECTRE conventions
      30             :  * 2. Static capacity
      31             :  * 3. Storage is std::array
      32             :  * 4. Switch to west-const
      33             :  *
      34             :  */
      35             : 
      36             : #pragma once
      37             : 
      38             : #include <atomic>
      39             : #include <cassert>
      40             : #include <cstddef>
      41             : #include <new>  // Placement new
      42             : #include <stdexcept>
      43             : #include <type_traits>
      44             : 
      45             : #include "Utilities/ErrorHandling/Assert.hpp"
      46             : #include "Utilities/Requires.hpp"
      47             : 
      48             : namespace Parallel {
      49             : /*!
      50             :  * \brief A static capacity runtime-sized single-producer single-consumer
      51             :  * lockfree queue.
      52             :  *
      53             :  * As long as only one thread reads and writes simultaneously the queue is
      54             :  * threadsafe. Which threads read and write can change throughout program
      55             :  * execution, the important thing is that there is no instance during the
      56             :  * execution where more than one thread tries to read and where more than one
      57             :  * thread tries to write.
      58             :  *
      59             :  * \note This class is intentionally not serializable since handling
      60             :  * threadsafety around serialization requires careful thought of the individual
      61             :  * circumstances.
      62             :  */
      63             : template <typename T, size_t Capacity>
      64           1 : class StaticSpscQueue {
      65             :  private:
      66             : #ifdef __cpp_lib_hardware_interference_size
      67             :   static constexpr size_t cache_line_size_ =
      68             :       std::hardware_destructive_interference_size;
      69             : #else
      70           0 :   static constexpr size_t cache_line_size_ = 64;
      71             : #endif
      72             : 
      73             :   // Padding to avoid false sharing between slots_ and adjacent allocations
      74           0 :   static constexpr size_t padding_ = (cache_line_size_ - 1) / sizeof(T) + 1;
      75             : 
      76             :  public:
      77           0 :   StaticSpscQueue() = default;
      78           0 :   ~StaticSpscQueue() {
      79             :     // Destruct objects in the buffer.
      80             :     while (front()) {
      81             :       pop();
      82             :     }
      83             :   }
      84             : 
      85           0 :   StaticSpscQueue(const StaticSpscQueue&) = delete;
      86           0 :   StaticSpscQueue& operator=(const StaticSpscQueue&) = delete;
      87           0 :   StaticSpscQueue(StaticSpscQueue&&) = delete;
      88           0 :   StaticSpscQueue& operator=(StaticSpscQueue&&) = delete;
      89             : 
      90             :   /// Construct a new element at the end of the queue in place.
      91             :   ///
      92             :   /// Uses placement new for in-place construction.
      93             :   ///
      94             :   /// \warning This may overwrite existing elements if `capacity()` is
      95             :   /// exceeded without warning.
      96             :   template <typename... Args>
      97           1 :   void emplace(Args&&... args) noexcept(
      98             :       std::is_nothrow_constructible_v<T, Args&&...>) {
      99             :     static_assert(std::is_constructible_v<T, Args&&...>,
     100             :                   "T must be constructible with Args&&...");
     101             :     const auto write_index = write_index_.load(std::memory_order_relaxed);
     102             :     auto next_write_index = write_index + 1;
     103             :     if (next_write_index == capacity_) {
     104             :       next_write_index = 0;
     105             :     }
     106             :     while (next_write_index == read_index_cache_) {
     107             :       read_index_cache_ = read_index_.load(std::memory_order_acquire);
     108             :     }
     109             :     // Destroy the object this slot already holds (see `data_`).
     110             :     data_[write_index + padding_].~T();
     111             :     new (&data_[write_index + padding_]) T(std::forward<Args>(args)...);
     112             :     write_index_.store(next_write_index, std::memory_order_release);
     113             :   }
     114             : 
     115             :   /// Construct a new element at the end of the queue in place.
     116             :   ///
     117             :   /// Uses placement new for in-place construction.
     118             :   ///
     119             :   /// Returns `true` if the emplacement succeeded and `false` if it did
     120             :   /// not. If it failed then the queue is currently full.
     121             :   template <typename... Args>
     122           1 :   [[nodiscard]] bool try_emplace(Args&&... args) noexcept(
     123             :       std::is_nothrow_constructible_v<T, Args&&...>) {
     124             :     static_assert(std::is_constructible_v<T, Args&&...>,
     125             :                   "T must be constructible with Args&&...");
     126             :     const auto write_index = write_index_.load(std::memory_order_relaxed);
     127             :     auto next_write_index = write_index + 1;
     128             :     if (next_write_index == capacity_) {
     129             :       next_write_index = 0;
     130             :     }
     131             :     if (next_write_index == read_index_cache_) {
     132             :       read_index_cache_ = read_index_.load(std::memory_order_acquire);
     133             :       if (next_write_index == read_index_cache_) {
     134             :         return false;
     135             :       }
     136             :     }
     137             :     // Destroy the object this slot already holds (see `data_`).
     138             :     data_[write_index + padding_].~T();
     139             :     new (&data_[write_index + padding_]) T(std::forward<Args>(args)...);
     140             :     write_index_.store(next_write_index, std::memory_order_release);
     141             :     return true;
     142             :   }
     143             : 
     144             :   /// Push a new element to the end of the queue.
     145             :   ///
     146             :   /// Uses `emplace()` internally.
     147             :   ///
     148             :   /// \warning This may overwrite existing elements if `capacity()` is
     149             :   /// exceeded without warning.
     150           1 :   void push(const T& v) noexcept(std::is_nothrow_copy_constructible_v<T>) {
     151             :     static_assert(std::is_copy_constructible_v<T>,
     152             :                   "T must be copy constructible");
     153             :     emplace(v);
     154             :   }
     155             : 
     156             :   /// Push a new element to the end of the queue.
     157             :   ///
     158             :   /// Uses `emplace()` internally.
     159             :   ///
     160             :   /// \warning This may overwrite existing elements if `capacity()` is
     161             :   /// exceeded without warning.
     162             :   template <typename P, Requires<std::is_constructible_v<T, P&&>> = nullptr>
     163           1 :   void push(P&& v) noexcept(std::is_nothrow_constructible_v<T, P&&>) {
     164             :     emplace(std::forward<P>(v));
     165             :   }
     166             : 
     167             :   /// Push a new element to the end of the queue. Returns `false` if the queue
     168             :   /// is at capacity and does not push the new object, otherwise returns `true`.
     169             :   ///
     170             :   /// Uses `try_emplace()` internally.
     171           1 :   [[nodiscard]] bool try_push(const T& v) noexcept(
     172             :       std::is_nothrow_copy_constructible_v<T>) {
     173             :     static_assert(std::is_copy_constructible_v<T>,
     174             :                   "T must be copy constructible");
     175             :     return try_emplace(v);
     176             :   }
     177             : 
     178             :   /// Push a new element to the end of the queue. Returns `false` if the queue
     179             :   /// is at capacity and does not push the new object, otherwise returns `true`.
     180             :   ///
     181             :   /// Uses `try_emplace()` internally.
     182             :   template <typename P, Requires<std::is_constructible_v<T, P&&>> = nullptr>
     183           1 :   [[nodiscard]] bool try_push(P&& v) noexcept(
     184             :       std::is_nothrow_constructible_v<T, P&&>) {
     185             :     return try_emplace(std::forward<P>(v));
     186             :   }
     187             : 
     188             :   /// Returns the first element from the queue.
     189             :   ///
     190             :   /// \note Returns `nullptr` if the queue is empty.
     191           1 :   [[nodiscard]] T* front() noexcept {
     192             :     const auto read_index = read_index_.load(std::memory_order_relaxed);
     193             :     if (read_index == write_index_cache_) {
     194             :       write_index_cache_ = write_index_.load(std::memory_order_acquire);
     195             :       if (write_index_cache_ == read_index) {
     196             :         return nullptr;
     197             :       }
     198             :     }
     199             :     return &data_[read_index + padding_];
     200             :   }
     201             : 
     202             :   /// Removes the first element from the queue.
     203           1 :   void pop() {
     204             :     static_assert(std::is_nothrow_destructible_v<T>,
     205             :                   "T must be nothrow destructible");
     206             :     const auto read_index = read_index_.load(std::memory_order_relaxed);
     207             : #ifdef SPECTRE_DEBUG
     208             :     const auto write_index = write_index_.load(std::memory_order_acquire);
     209             :     ASSERT(write_index != read_index,
     210             :            "Can't pop an element from an empty queue. read_index: "
     211             :                << read_index << " write_index " << write_index);
     212             : #endif  // SPECTRE_DEBUG
     213             :     // Leave a live object behind (see `data_`).
     214             :     data_[read_index + padding_].~T();
     215             :     new (&data_[read_index + padding_]) T{};
     216             :     auto next_read_index = read_index + 1;
     217             :     if (next_read_index == capacity_) {
     218             :       next_read_index = 0;
     219             :     }
     220             :     if (read_index == write_index_cache_) {
     221             :       write_index_cache_ = next_read_index;
     222             :     }
     223             :     read_index_.store(next_read_index, std::memory_order_release);
     224             :   }
     225             : 
     226             :   /// Returns the size of the queue at a particular hardware state.
     227             :   ///
     228             :   /// Note that while this can be checked in a threadsafe manner, it is up to
     229             :   /// the user to guarantee that another thread does not change the queue
     230             :   /// between when `size()` is called and how the result is used.
     231           1 :   [[nodiscard]] size_t size() const noexcept {
     232             :     std::ptrdiff_t diff = static_cast<std::ptrdiff_t>(
     233             :                               write_index_.load(std::memory_order_acquire)) -
     234             :                           static_cast<std::ptrdiff_t>(
     235             :                               read_index_.load(std::memory_order_acquire));
     236             :     if (diff < 0) {
     237             :       diff += static_cast<std::ptrdiff_t>(capacity_);
     238             :     }
     239             :     return static_cast<size_t>(diff);
     240             :   }
     241             : 
     242             :   /// Returns `true` if the queue may be empty, otherwise `false`.
     243             :   ///
     244             :   /// Note that while this can be checked in a threadsafe manner, it is up to
     245             :   /// the user to guarantee that another thread does not change the queue
     246             :   /// between when `empty()` is called and how the result is used.
     247           1 :   [[nodiscard]] bool empty() const noexcept {
     248             :     return write_index_.load(std::memory_order_acquire) ==
     249             :            read_index_.load(std::memory_order_acquire);
     250             :   }
     251             : 
     252             :   /// Returns the capacity of the queue.
     253           1 :   [[nodiscard]] size_t capacity() const noexcept { return capacity_ - 1; }
     254             : 
     255             :  private:
     256           0 :   static constexpr size_t capacity_ = Capacity + 1;
     257             :   // Every slot holds a live object, so the array's destructor destroys each
     258             :   // element exactly once. `emplace` and `pop` maintain this.
     259           0 :   std::array<T, capacity_ + 2 * padding_> data_{};
     260             : 
     261             :   // Align to cache line size in order to avoid false sharing
     262             :   // read_index_cache_ and write_index_cache_ is used to reduce the amount of
     263             :   // cache coherency traffic
     264           0 :   alignas(cache_line_size_) std::atomic<size_t> write_index_{0};
     265           0 :   alignas(cache_line_size_) size_t read_index_cache_{0};
     266           0 :   alignas(cache_line_size_) std::atomic<size_t> read_index_{0};
     267           0 :   alignas(cache_line_size_) size_t write_index_cache_{0};
     268             : 
     269             :   // Padding to avoid adjacent allocations from sharing a cache line with
     270             :   // write_index_cache_
     271             :   // NOLINTNEXTLINE(modernize-avoid-c-arrays)
     272           0 :   char padding_data_[cache_line_size_ - sizeof(write_index_cache_)]{};
     273             : };
     274             : }  // namespace Parallel

Generated by: LCOV version 1.14