SpECTRE Documentation Coverage Report
Current view: top level - Parallel - StaticSpscQueue.hpp Coverage Total Hit
Commit: 0d1d717af84a81fb1284a20c4013708fe85cc2f0 Lines: 42.9 % 28 12
Test Date: 2026-10-22 17:29:42
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 2.0-1