From 9eae8af295985d7bd03445bc5cec4155c3eb687f Mon Sep 17 00:00:00 2001 From: Equationzhao Date: Thu, 13 Aug 2026 17:20:34 +0800 Subject: [PATCH] perf: hoist cap==0 check off full emplace GCC 13 was calling EQZ_COLD ensure_writable on every overwrite. Keep the throw out of line; only invoke it when capacity is 0. Co-authored-by: Cursor --- docs/DESIGN.md | 4 +++- include/eqz/circular_buffer.hpp | 11 ++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 95aac40..3fa8598 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -69,7 +69,7 @@ Fill the buffer before `std::iota(begin(), end(), 1)`, or call `resize(n)` first ## Hot path - Not-full `emplace_back` / `emplace_front`: test `size_ < capacity_`, construct into the slot, then publish the cursor and `size_`. -- Full `emplace_*`: `ensure_writable()` (cold; throws `length_error` only when `capacity()==0`), then `overwrite_at` (in-place assign), then bump cursors. Full `emplace_front` writes `drop(first_)` before publishing `first_`. +- Full `emplace_*`: if `capacity()==0`, call cold `ensure_writable()` (throws `length_error`); else `overwrite_at` (in-place assign) with no call, then bump cursors. Full `emplace_front` writes `drop(first_)` before publishing `first_`. - `bump` / `drop` wrap with `buff_` / `limit_`. `ptr_at` uses a linearized fast path when `last_ > first_` or `first_ == buff_`. - Skip `allocator_traits::destroy` when `T` is trivially destructible. - `EQZ_ALWAYS_INLINE` on bump/drop/`ptr_at`/emplace/`overwrite_at`/`[]`/`front`/`back`/`empty`/`full`/`pop_*`. gcc/clang only; other compilers get `inline`. @@ -80,6 +80,8 @@ Measured slower or incorrect on the Boost suite, so these stay out: - Boost-style `T*` linear iterators (logical index iterators stay). - Skipping the empty-buffer cursor reset on the last `pop_*`. - Inlining `throw` into `emplace_*` (puts EH in the hot function). +- Dropping `EQZ_COLD` from `ensure_writable` while the full path still calls it (GCC 13 `copy/full` ~11 µs). +- Always-inlining the cap==0 check with a separate cold throw helper (same GCC copy regression). - `[[likely]]` / `[[unlikely]]` on `full()` or wrap (moves EH / branch layout). - Duplicating `bump` as in-place `++last_` inside `emplace_back`. - Computing the base as `limit_ - capacity_` instead of caching `buff_`. diff --git a/include/eqz/circular_buffer.hpp b/include/eqz/circular_buffer.hpp index 11b5478..0960bae 100644 --- a/include/eqz/circular_buffer.hpp +++ b/include/eqz/circular_buffer.hpp @@ -633,7 +633,9 @@ class circular_buffer { ++size_; return *slot; } - ensure_writable(); + if (capacity_ == 0) { + ensure_writable(); + } T* slot = last_; overwrite_at(slot, std::forward(args)...); last_ = bump(slot); @@ -650,7 +652,9 @@ class circular_buffer { ++size_; return *slot; } - ensure_writable(); + if (capacity_ == 0) { + ensure_writable(); + } T* slot = drop(first_); overwrite_at(slot, std::forward(args)...); first_ = last_ = slot; @@ -1052,7 +1056,8 @@ class circular_buffer { limit_ = buff_ + n; } - /// Cold path: `capacity()==0` throws. Full `emplace_*` call this instead of inlining `throw`. + /// Throws when `capacity()==0`. Full `emplace_*` call this only on that edge + /// so the overwrite path does not `call` a cold function every iteration. EQZ_COLD constexpr void ensure_writable() const { if (capacity_ == 0) { throw std::length_error("eqz::circular_buffer: capacity is 0");