Skip to content

perf: let the recycling frame allocator cache a wide fan-out - #485

Merged
sgerbino merged 1 commit into
cppalliance:developfrom
sgerbino:pr/recycling-free-list
Nov 13, 2026
Merged

sgerbino merged 1 commit into
cppalliance:developfrom
sgerbino:pr/recycling-free-list

Conversation

@sgerbino

@sgerbino sgerbino commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Each size class used to keep at most 16 blocks per thread and 16 more in the shared pool. A fan-out with more live frames than that sent every further allocation and free through the shared pool's mutex to the upstream allocator.

Changes

  • Intrusive free lists. Cached blocks are linked through their own storage, so a bucket's capacity no longer depends on a fixed array. Each pool caches up to 256 KiB per size class: 4096 blocks of 64 bytes, down to 128 of 2048 bytes.
  • Refill is a pointer swap. A thread that runs out still takes the shared pool's whole bucket, but no longer copies its entries under the lock.
  • Decay. A thread keeps a burst's blocks only while it goes on using them.
    • Each bucket tracks the fewest blocks it held since the last check: that many sat unused all period.
    • Every 4096 frees plus refilled blocks on the thread, half of those idle blocks move to the shared pool, or go back to ::operator delete when the shared pool has no room.
    • Allocations served from the thread's own pool aren't counted, so that path is unchanged.
    • The walk through the blocks happens outside the lock.

Worst-case cached memory is bounded by the caps: (threads + 1) × 1.5 MiB.

Tests

New tests cover a fan-out wider than the old caps, an idle burst that decays, a recurring burst that stays cached, an idle burst while the shared pool is full, and a thread that only allocates.

Related

cppalliance/corosio#416 pairs with this branch for its benchmarks.

Summary by CodeRabbit

  • Performance
    • Memory allocation and deallocation now use a more efficient caching strategy, which may reduce overhead during repeated operations.
  • Memory Management
    • Cached memory is periodically adjusted, with unused blocks returned to shared storage or released to the system.

Each size class kept at most 16 blocks per thread and 16 more in the
shared pool. A fan-out with more live frames than that sent every
further allocation and free through the shared pool's mutex to the
upstream allocator.

Cached blocks are now linked through their own storage, so a bucket's
capacity no longer depends on a fixed array. Each pool caches up to
256 KiB per size class.

A thread keeps a burst's blocks only while it goes on using them. Each
bucket tracks the fewest blocks it held since its last check. Every few
thousand frees and refilled blocks on the thread, half of those idle
blocks move to the shared pool, where any thread can reuse them, or go
back to the upstream allocator when the shared pool has no room.
@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The recycling memory resource now stores cached blocks in intrusive per-size-class lists with capacity derived from a 256 KiB limit. It tracks arrivals and periodically moves idle blocks to the global pool or releases them.

Changes: Thread-local cache lifecycle

Layer / File(s) Summary
Per-class bucket model
include/boost/capy/ex/recycling_memory_resource.hpp
The fixed pointer arrays are replaced by intrusive lists with count and low-water tracking. Each size class has a calculated capacity based on the 256 KiB limit.
Cache operations and decay
include/boost/capy/ex/recycling_memory_resource.hpp, src/ex/recycling_memory_resource.cpp
Allocation and deallocation update local and global buckets. After the arrival threshold is reached, decay removes half of each bucket’s low-water count. It transfers a share only when the global bucket can accept it in full; otherwise, it releases the share with operator delete.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant LocalPool
  participant GlobalPool
  participant OperatorDelete as operator delete
  LocalPool->>LocalPool: Unlink half of each bucket's low-water count
  LocalPool->>GlobalPool: Lock and check whether each share fits
  alt Entire share fits
    LocalPool->>GlobalPool: Transfer cached blocks
  else Share does not fit
    LocalPool->>OperatorDelete: Release cached blocks outside the lock
  end
Loading

Suggested reviewers: vinniefalco, mvandeberg

Merge Risk: Minimal · up to d1da1

This change lets the recycling frame allocator cache more blocks per size class and gradually release idle ones. The review found no concrete defect that should block merging, and the description reports no test results.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: letting the recycling frame allocator cache a wide fan-out. The objectives confirm the change raises the per-size-class cache from 16 blocks to up to 256 KiB, using in…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Title check

Explanation

The title names the main change: letting the recycling frame allocator cache a wide fan-out. The objectives confirm the change raises the per-size-class cache from 16 blocks to up to 256 KiB, using intrusive free lists and a decay policy. The title is concise, specific, and matches the changeset. The conventional 'perf:' prefix is acceptable.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit checks the buckets in the morning light
Intrusive lists keep cached blocks in sight
The local pool counts arrivals as they flow
Half the idle blocks move when decay says so
The global pool takes what fits its space
The rest returns to its allocated place

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between d62f5de and d1da16a.

Files ignored due to path filters: 1

  • test/unit/ex/recycling_memory_resource.cpp is excluded by !**/test/**
Files selected for processing: 2

  • include/boost/capy/ex/recycling_memory_resource.hpp
  • src/ex/recycling_memory_resource.cpp

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@greptile-apps

greptile-apps Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Replaces fixed pointer arrays with linked lists stored inside cached blocks and raises each size class's cache limit to 256 KiB.

  • Wide frame bursts stay in the recycling pools.
  • Idle cached blocks flow back for reuse.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Allocate] --> B{Local bucket has a block?}
    B -->|Yes| C[Return one block]
    B -->|No| D[Take shared bucket under lock]
    D --> E{Got a block?}
    E -->|Yes| F[Count refill and check decay]
    E -->|No| G[Allocate upstream]
    H[Free] --> I[Count arrival and check decay]
    I --> J{Local bucket has room?}
    J -->|Yes| K[Cache locally]
    J -->|No| L{Shared bucket has room?}
    L -->|Yes| M[Cache under lock]
    L -->|No| N[Release upstream]
    F --> O[Move half the idle blocks]
    I --> O
    O --> P[Share under lock or release outside lock]
Loading

Reviews (1) · Last reviewed commit: "perf: let the recycling frame allocator ..." · Reviewed by Greptile

@cppalliance-bot

Copy link
Copy Markdown

An automated preview of the documentation is available at https://485.capy.prtest3.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-11-13 12:14:32 UTC

@codecov

codecov Bot commented Nov 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.49%. Comparing base (d62f5de) to head (d1da16a).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop     #485      +/-   ##
===========================================
+ Coverage    98.45%   98.49%   +0.03%     
===========================================
  Files          135      135              
  Lines         7250     7364     +114     
===========================================
+ Hits          7138     7253     +115     
+ Misses         112      111       -1     
Flag Coverage Δ
linux 98.48% <ø> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...nclude/boost/capy/ex/recycling_memory_resource.hpp 94.91% <100.00%> (+1.03%) ⬆️
src/ex/recycling_memory_resource.cpp 100.00% <100.00%> (ø)

... and 3 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d62f5de...d1da16a. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cppalliance-bot

Copy link
Copy Markdown

GCOVR code coverage report https://485.capy.prtest3.cppalliance.org/gcovr/index.html
LCOV code coverage report https://485.capy.prtest3.cppalliance.org/genhtml/index.html
Coverage Diff Report https://485.capy.prtest3.cppalliance.org/diff-report/index.html

Build time: 2026-11-13 12:24:05 UTC

@sgerbino
sgerbino merged commit 2545490 into cppalliance:develop Nov 13, 2026
82 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Beast2 Nov 13, 2026
@sgerbino
sgerbino deleted the pr/recycling-free-list branch November 13, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants