Skip to content

Commit 4093179

Browse files
author
mrabine
committed
add allocator documentation and update others
1 parent 23fb4e0 commit 4093179

File tree

6 files changed

+303
-10
lines changed

6 files changed

+303
-10
lines changed

content/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ The framework is a collection of specialized modules that build upon one another
4141

4242
| Module | Purpose | Highlights |
4343
| :--- | :--- | :--- |
44-
| **[`core`]({{< ref "core" >}})** | **Foundation** | Epoll Reactor, TCP/UDP/TLS, Unix Sockets, Thread Pools, Mutexes |
44+
| **[`core`]({{< ref "core" >}})** | **Foundation** | Epoll Reactor, TCP/UDP/TLS, Unix Sockets, Thread Pools, Lock-Free Queues & Allocator |
4545
| **[`fabric`]({{< ref "fabric" >}})** | **Network Control** | Netlink Interface Manager, ARP client, DNS Resolver |
4646
| **[`crypto`]({{< ref "crypto" >}})** | **Security** | OpenSSL Wrappers, HMAC, Digital Signatures, Base64 |
4747
| **[`data`]({{< ref "data" >}})** | **Serialization** | High-perf JSON (DOM/SAX), MessagePack, Zlib Streams |

content/docs/_index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: "Documentation"
3-
weight: 1
3+
weight: 10
44
---
55

66
# Documentation
@@ -30,7 +30,7 @@ Explore the framework's modular architecture.
3030

3131
**Discover:**
3232

33-
* **Core** - Foundation components: reactor, sockets, threads, concurrency primitives
33+
* **Core** - Foundation components: reactor, sockets, threads, lock-free queues, arena allocator, concurrency primitives
3434
* **Fabric** - Network control: interface management, ARP, DNS resolution
3535
* **Crypto** - Security: encryption, hashing, signatures, Base64
3636
* **Data** - Serialization: JSON, MessagePack, compression

content/docs/modules/_index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ This modular organization provides:
1919

2020
**Foundation components and runtime primitives**
2121

22-
The core module provides essential building blocks including the epoll-based reactor, socket abstractions (TCP, UDP, TLS, Unix), threading primitives, lock-free queues, timers, and utilities.
22+
The core module provides essential building blocks including the epoll-based reactor, socket abstractions (TCP, UDP, TLS, Unix), threading primitives, lock-free queues, a lock-free arena allocator, timers, and utilities.
2323

2424
**Key Components:**
2525
- Reactor pattern with epoll
2626
- TCP/UDP/TLS sockets
2727
- Thread pools and synchronization
2828
- Lock-free shared memory queues
29+
- Lock-free arena allocator (slab pools)
2930
- High-precision timers
3031

3132
👉 See: [Core Module]({{< ref "core" >}})

content/docs/modules/core/_index.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,18 @@ See [CPU Topology]({{< ref "cpu" >}}) for full documentation.
100100

101101
---
102102

103-
## 🔄 Lock-Free Queues
103+
## 🔄 Lock-Free Queues & Allocator
104104

105-
High-performance lock-free ring buffer queues built on top of the memory backends:
105+
High-performance lock-free primitives built on top of the memory backends:
106106

107-
- **`Spsc`** — single-producer / single-consumer (lowest overhead)
108-
- **`Mpsc`** — multi-producer / single-consumer
109-
- **`Mpmc`** — multi-producer / multi-consumer
107+
- **`Spsc`** — single-producer / single-consumer queue (lowest overhead)
108+
- **`Mpsc`** — multi-producer / single-consumer queue
109+
- **`Mpmc`** — multi-producer / multi-consumer queue
110+
- **`BasicArena`** — lock-free slab allocator with multiple fixed-size pools
110111

111112
Backed by either **`LocalMem`** (anonymous private memory) or **`ShmMem`** (POSIX shared memory). Supports blocking and non-blocking push/pop, NUMA binding, and memory locking.
112113

113-
See [Memory]({{< ref "memory" >}}), [Queue]({{< ref "queue" >}}), and [Backoff]({{< ref "backoff" >}}) for full documentation.
114+
See [Memory]({{< ref "memory" >}}), [Queue]({{< ref "queue" >}}), [Allocator]({{< ref "allocator" >}}), and [Backoff]({{< ref "backoff" >}}) for full documentation.
114115

115116
---
116117

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
---
2+
title: "Allocator"
3+
weight: 25
4+
---
5+
6+
# Allocator
7+
8+
Join provides a **lock-free slab allocator** built on top of the memory backends.
9+
The arena manages one or more fixed-size pools and dispatches allocations to the
10+
smallest pool that can satisfy each request.
11+
12+
Arenas are:
13+
14+
* lock-free (ABA-safe tagged CAS on each pool's free-list)
15+
* cache-line aligned (all chunk headers are naturally aligned)
16+
* NUMA-aware and hugepage-capable (inherited from the memory backend)
17+
* safe for concurrent use across threads and processes
18+
19+
They are available through two backend policies:
20+
21+
* **LocalMem** — anonymous private memory (single process)
22+
* **ShmMem** — POSIX named shared memory (multi-process)
23+
24+
---
25+
26+
## Concepts
27+
28+
### Chunk
29+
30+
The smallest unit of allocation. Each chunk has a fixed `Size` (must be a power of 2
31+
and a multiple of `alignof(std::max_align_t)`). Chunks are stored in a flat array
32+
inside a pool segment and reused via a lock-free free-list.
33+
34+
### Pool
35+
36+
A `BasicPool<Count, Size>` manages `Count` chunks of `Size` bytes each over a
37+
contiguous region of the backend memory. Allocation and deallocation are O(1) and
38+
lock-free using an ABA-safe tagged index CAS.
39+
40+
### Arena
41+
42+
A `BasicArena<Backend, Count, Sizes...>` owns the memory backend and composes
43+
multiple pools of increasing sizes. On allocation it selects the smallest pool whose
44+
chunk size fits the request. If that pool is exhausted it **promotes** the request to
45+
the next larger pool.
46+
47+
---
48+
49+
## Creating an arena
50+
51+
### Type aliases
52+
53+
Each memory backend exposes a convenient `Allocator` alias:
54+
55+
```cpp
56+
LocalMem::Allocator<Count, Sizes...>
57+
ShmMem::Allocator<Count, Sizes...>
58+
```
59+
60+
These expand to `BasicArena<LocalMem, Count, Sizes...>` and
61+
`BasicArena<ShmMem, Count, Sizes...>` respectively.
62+
63+
### Local arena (single process)
64+
65+
```cpp
66+
#include <join/allocator.hpp>
67+
68+
using namespace join;
69+
70+
// 3 pools: 64 B, 256 B, 1024 B — 128 chunks each
71+
LocalMem::Allocator<128, 64, 256, 1024> arena;
72+
```
73+
74+
Pool sizes must be provided in **ascending order** and each must be a **power of 2**.
75+
76+
---
77+
78+
### Shared arena (multi-process)
79+
80+
```cpp
81+
#include <join/allocator.hpp>
82+
83+
using namespace join;
84+
85+
ShmMem::Allocator<128, 64, 256, 1024> arena("/my_arena");
86+
```
87+
88+
The first process to attach initializes all pools.
89+
Subsequent processes reuse the existing layout — they wait (with exponential backoff)
90+
until initialization is complete before accessing any pool.
91+
92+
⚠️ Call `ShmMem::unlink("/my_arena")` during application teardown.
93+
94+
---
95+
96+
## Allocating memory
97+
98+
### `allocate` — with pool promotion
99+
100+
Returns a chunk from the first pool whose size fits the request.
101+
If that pool is exhausted, the request is promoted to the next larger pool.
102+
103+
```cpp
104+
void* p = arena.allocate(48); // uses the 64 B pool; falls back to 256 B if full
105+
```
106+
107+
Returns `nullptr` if no pool can satisfy the request (all candidates exhausted).
108+
109+
---
110+
111+
### `tryAllocate` — without pool promotion
112+
113+
Returns a chunk only from the exact pool whose size fits, without promoting.
114+
Returns `nullptr` if the exact pool is full, even if a larger pool has free chunks.
115+
116+
```cpp
117+
void* p = arena.tryAllocate(48); // uses the 64 B pool only
118+
```
119+
120+
Use this when you need strict control over which pool is used, for example to
121+
avoid unintentional memory pressure on larger pools.
122+
123+
---
124+
125+
## Deallocating memory
126+
127+
```cpp
128+
arena.deallocate(p);
129+
```
130+
131+
The arena walks its pools in order and returns the chunk to whichever pool owns it.
132+
Passing `nullptr` is a no-op. Passing a pointer that does not belong to any pool
133+
is silently ignored (base case of the recursive ownership check).
134+
135+
---
136+
137+
## NUMA binding and memory locking
138+
139+
These methods delegate directly to the underlying memory backend.
140+
141+
```cpp
142+
// bind arena memory to NUMA node 0
143+
if (arena.mbind(0) == -1)
144+
{
145+
std::cerr << join::lastError.message() << "\n";
146+
}
147+
148+
// lock arena memory in RAM
149+
if (arena.mlock() == -1)
150+
{
151+
std::cerr << join::lastError.message() << "\n";
152+
}
153+
```
154+
155+
---
156+
157+
## Size requirements
158+
159+
The total memory footprint of an arena is the sum of each pool's footprint:
160+
161+
```
162+
per-pool = sizeof(SegmentHeader) + Count × sizeof(Chunk<Size>)
163+
total = Σ per-pool across all Sizes
164+
```
165+
166+
The backend size is computed automatically at compile time — you do not need to
167+
calculate or pass it manually.
168+
169+
---
170+
171+
## Constraints
172+
173+
| Constraint | Reason |
174+
|---|---|
175+
| `Size` must be a power of 2 | Enables aligned storage and fast index arithmetic |
176+
| `Size ≥ sizeof(uint64_t)` | Free-list next-index is stored inside the chunk |
177+
| `Size % alignof(std::max_align_t) == 0` | Guarantees safe placement of any type |
178+
| `Sizes...` must be in ascending order | Enables the linear pool-selection scan |
179+
| `Count ≤ UINT32_MAX` | Tagged index uses a 32-bit pool index |
180+
| Element type must fit in `Size` | The caller is responsible for matching sizes to types |
181+
182+
---
183+
184+
## Error handling
185+
186+
`allocate` and `tryAllocate` return `nullptr` on failure — no exception is thrown.
187+
`deallocate` is `noexcept`.
188+
189+
Constructor failures (backend `mmap` / `shm_open`) throw `std::system_error`.
190+
191+
---
192+
193+
## Move semantics
194+
195+
Arenas are **move-only** — copy construction and copy assignment are deleted.
196+
197+
```cpp
198+
LocalMem::Allocator<128, 64, 256> a;
199+
LocalMem::Allocator<128, 64, 256> b = std::move(a); // a is now empty
200+
```
201+
202+
---
203+
204+
## Complete example
205+
206+
```cpp
207+
#include <join/allocator.hpp>
208+
209+
using namespace join;
210+
211+
struct Sample
212+
{
213+
int32_t id;
214+
float value;
215+
};
216+
217+
static_assert (sizeof (Sample) <= 64);
218+
219+
int main ()
220+
{
221+
// local arena: 1 pool of 64 B chunks, 256 slots
222+
LocalMem::Allocator<256, 64> arena;
223+
224+
// pin to NUMA node 0 and lock in RAM
225+
arena.mbind (0);
226+
arena.mlock ();
227+
228+
// allocate and construct
229+
void* raw = arena.allocate (sizeof (Sample));
230+
if (!raw)
231+
{
232+
return 1; // pool exhausted
233+
}
234+
235+
auto* s = new (raw) Sample{42, 3.14f};
236+
237+
// use s ...
238+
239+
// return to pool (no destructor called — type must be trivially destructible)
240+
arena.deallocate (s);
241+
}
242+
```
243+
244+
---
245+
246+
## Best practices
247+
248+
* Choose `Size` values that match the actual types you store — oversized chunks waste pool capacity
249+
* Prefer `tryAllocate` in real-time paths to avoid silent promotion to a larger pool
250+
* Use `allocate` when graceful degradation under pressure is acceptable
251+
* On NUMA systems, call `mbind()` immediately after construction and before first use
252+
* Use `mlock()` on latency-critical paths to prevent page faults
253+
* For shared arenas, always call `ShmMem::unlink()` on teardown
254+
255+
---
256+
257+
## Summary
258+
259+
| Feature | LocalMem backend | ShmMem backend |
260+
| -------------------------- | :--------------: | :------------: |
261+
| Lock-free allocation |||
262+
| Pool promotion |||
263+
| No-promotion allocation |||
264+
| Multiple pool sizes |||
265+
| NUMA binding |||
266+
| Memory locking |||
267+
| Cross-process access |||
268+
| Move semantics |||
269+
| Compile-time size checking |||

content/docs/modules/core/queue.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,26 @@ queue.mlock();
212212

213213
---
214214

215+
## Move semantics
216+
217+
`BasicQueue` is **neither copyable nor movable** — copy and move constructors and assignment operators are all explicitly deleted.
218+
Queues must be constructed in-place and cannot be transferred.
219+
220+
```cpp
221+
// ❌ Does not compile
222+
LocalMem::Spsc::Queue<Sample> a(1024);
223+
LocalMem::Spsc::Queue<Sample> b = std::move(a);
224+
```
225+
226+
If you need to share a queue between scopes, use a reference, a pointer, or wrap it in a `std::unique_ptr`.
227+
228+
```cpp
229+
// ✅ Share via pointer
230+
auto queue = std::make_unique<LocalMem::Spsc::Queue<Sample>>(1024);
231+
```
232+
233+
---
234+
215235
## Error handling
216236

217237
Functions returning `-1` set `join::lastError`:
@@ -243,6 +263,7 @@ if (queue.tryPush(s) == -1)
243263
* Use `tryPush` / `tryPop` in real-time contexts to avoid unbounded spin
244264
* Use `push` / `pop` when latency is not a concern and blocking is acceptable
245265
* Use **ShmMem** backends for inter-process queues; call `ShmMem::unlink()` on teardown
266+
* Construct queues in-place or wrap them in `std::unique_ptr` — they cannot be moved or copied
246267

247268
---
248269

@@ -260,3 +281,4 @@ if (queue.tryPush(s) == -1)
260281
| Memory locking ||||
261282
| Blocking push/pop ||||
262283
| Non-blocking push/pop ||||
284+
| Move semantics ||||

0 commit comments

Comments
 (0)