You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bench, docs: expand benchmark suite and update performance metrics
- Added std::stack+malloc and GNU Obstack to the benchmark suite.
- Replaced monolithic tests with Small and Large payload scenarios.
- Integrated a stateful DummyStack to prevent compiler dead-loop folding.
- Added CPU hardware performance counters from perf to the analysis.
- Generated new clean-line throughput charts and fully updated the README.
**An absurdly fast, header-only, platform-agnostic, and safe LIFO stack allocator utilizing an inverted bi-directional buffer layout with dynamic metadata scaling.**
33
+
**A zero-compromise, header-only C/C++ LIFO stack allocator that outperforms compile-time templates and system baselines in both execution speed and memory footprint simultaneously — even under full runtime safety.**
34
34
35
35
## TL;DR
36
36
37
-
**What is it?** A portable, extremely fast, single-header stack allocator that completely decouples control paths from aligned user payloads. It eliminates inline metadata overhead entirely by growing tracking offsets forward and aligned payloads backward.
37
+
**What is it?** A portable, single-header LIFO stack allocator that completely decouples control paths from aligned user payloads. It eliminates inline metadata overhead entirely by growing tracking offsets forward and aligned payloads backward.
38
38
39
-
**Why use it?** To achieve deterministic **O(1)** allocation and deallocation speeds that outperform traditional C allocators (like `wb_alloc`) by up to **4.1x+** and compile-time optimized C++ templates (like `Trebi`) by up to **4.7x+** in trusted mode (while remaining **2.5x+ faster** than both even with full runtime defensive safety active), all while retaining up to an **8x smaller metadata footprint**.
39
+
**Why use it?** Traditional memory allocators force you to trade execution speed for runtime safety, or memory footprint for performance. `easy_stack` breaks this triple trade-off, delivering:
40
+
***Highest-in-Class Performance**: Outperforms compile-time C++ templates (like `Trebi`) by up to **2.7x+** and standard heap managers (like `malloc`) by up to **10x+** in algorithmic speed.
41
+
***Zero-Cost Safety**: Even with full defensive runtime safety, bounds checking, and API sanitization active, it retains a performance advantage over *unprotected* competitor alternatives.
42
+
***Maximum Memory Efficiency**: Uses up to an **8x smaller metadata footprint** compared to traditional inline headers and wastes exactly zero bytes on alignment padding in the control zone.
40
43
41
44
**How to use it?**`#define EASY_STACK_IMPLEMENTATION` in one `.c` file, then just `#include "easy_stack.h"`.
42
45
@@ -130,66 +133,92 @@ Traditional stack allocators suffix or prefix each payload with inline metadata
130
133
131
134
## Benchmarks & Performance
132
135
133
-
To verify execution speed, cache resilience, and scalability under load, `easy_stack` was benchmarked against popular alternatives:
134
-
***wb_alloc (Bundy):** A widely-used, minimalist C arena allocator.
135
-
***Trebi StackAllocator:** A standard C++ LIFO stack allocator (compiled with `-flto` for maximum devirtualization).
136
+
To evaluate execution speed, cache resilience, and scalability under different workloads, `easy_stack` was benchmarked against popular alternatives.
136
137
137
138
### Test Environment
138
-
***CPU:** AMD Ryzen 7 4700U (8 Cores / 8 Threads, Zen 2 @ up to 4.1 GHz)
139
-
***Compiler:** GCC 15.2 with `-O3 -flto -DNDEBUG`
140
-
***Scenario:** 2,000,000 iterations per run (executing up to 480,000,000 allocator operations) of nested allocations with randomized sizes (16-160 bytes) scaled across three distinct stack allocation depths (15, 30, and 100). Best of 25 runs.
139
+
***CPU**: AMD Ryzen 7 4700U (8 Cores / 8 Threads, Zen 2 @ up to 4.1 GHz)
140
+
***Compiler**: GCC 16.1 with -O3 -flto -DNDEBUG
141
+
***Scenario**: 2,000,000 iterations per run (executing up to 480,000,000 allocator operations) of nested allocations with randomized sizes scaled across three distinct stack allocation depths (15, 30, and 100). Best of 25 runs.
142
+
143
+
### Tested Allocators:
144
+
1.**EasyStack (Contract)**: Trusted mode with validations delegated to assertions (compiled out in release).
145
+
2.**EasyStack (Defensive)**: Default safety mode with full runtime bounds and API sanitization active.
146
+
3.**Trebi LIFO**: A highly-optimized C++ template-based LIFO stack allocator.
147
+
4.**GNU Obstack**: The glibc standard stack allocator (highly optimized C system baseline).
148
+
5.**wb_alloc (Bundy)**: A popular fixed-size C arena/stack allocator.
149
+
6.**std::stack + malloc**: The default standard library heap-allocated baseline.
141
150
142
-
### 1. Throughput & Cache-Line Scaling (Speed)
143
-
144
-
Rather than benchmarking only a single shallow depth, the suite tests scaling across three critical architectural boundaries. This highlights how cache layout and processor prefetching impact execution speed as stack depth increases.
145
-
146
-
#### Phase A: The Standard 64 KB Workload (16-bit Metadata)
151
+
---
147
152
148
-
With a 64 KB capacity, `easy_stack` configures itself to use ultra-compact **Type 1 (16-bit)** metadata offsets.
153
+
### Methodology & Metrics
149
154
150
-
<palign="center">
151
-
<imgsrc="https://raw.githubusercontent.com/EasyMem/easy_stack/refs/heads/main/.github/assets/throughput_64kb_chart.png"width="750"alt="Throughput Scaling vs Stack Allocation Depth at 64KB" />
152
-
</p>
155
+
We evaluate performance using two distinct metrics to provide a transparent picture of real-world overhead:
156
+
***RAW Throughput**: The total execution time including loop control and function-call overhead. This represents the actual performance experienced by an application calling these routines.
157
+
***PURE Algorithmic Throughput**: Calculated by subtracting the baseline harness overhead (measured via a stateful dummy call wrapper). This isolates the pure overhead of the allocator's internal logic (pointer arithmetic, dynamic metadata scaling, alignment padding math, and safety checks).
153
158
154
-
At shallow depths (15 objects), the entire active allocator footprint (header + 30 bytes of metadata) fits completely into a **single 64-byte L1 cache line**. This yields a hardware-limit speed of **939 Million ops/sec** (~1.06 ns per allocation/free cycle) in Contract mode, outperforming the C++ template LIFO allocator by **371%**.
159
+
To prevent compile-time folding, dead-code elimination (DCE), or loop hoisting by aggressive GCC 16 optimizations, the benchmark utilizes a strict **Data Dependency Chain**. Every allocation writes a dynamic payload bound to its physical runtime address, and these payloads are read back and accumulated into a volatile global checksum sink prior to deallocation.
155
160
156
161
---
157
162
158
-
#### Phase B: The Transparent 1 MB Workload (32-bit Metadata)
159
-
160
-
> *"Wait a minute! You're cheating! By using a tiny 64 KB buffer, you guarantee that all metadata easily fits inside L1 cache. Let's see what happens on a larger stack."*
163
+
### 1. Small Payload Workloads (16 – 128 Bytes)
164
+
*Designed to simulate standard stack frame allocations, temporary object creation, and shallow trees.*
161
165
162
-
The objection is logically sound. To address this, the stack capacity was scaled up.
In **Defensive** and **Contract** modes, the capacity was increased to **1 MB**, automatically triggering the transition to 4-byte metadata offsets (`meta_type = 2`).
170
+
#### RAW Results (Best of 25 runs, 2,000,000 iterations/run)
This ensures that at depth 100, the metadata array alone spans multiple cache lines, removing any artificial L1 prefetching advantages. Here is how the allocator scales under the heavier 32-bit math path:
192
+
### 2. Large Payload Workloads (512 – 4096 Bytes)
193
+
*Designed to simulate heavy SIMD vectors, DMA buffers, and large temporary data arrays.*
173
194
174
195
<palign="center">
175
-
<imgsrc="https://raw.githubusercontent.com/EasyMem/easy_stack/refs/heads/main/.github/assets/throughput_1mb_chart.png"width="750"alt="Throughput Scaling vs Stack Allocation Depth at 1MB" />
Comparing the 64 KB buffer (16-bit) vs the 1 MB buffer (32-bit), the throughput drop in Contract mode is **less than 0.5%** at depth 15 (**934 Million ops/sec** vs 939 Million) and only **2.7%** at depth 100 (**717 Million** vs 738 Million). This demonstrates that dynamically widening metadata cells has a negligible performance impact on modern superscalar architectures.
In Defensive (Safety) mode, throughput remains completely flat at a rock-solid **~520-550 Million ops/sec** across both 64 KB and 1 MB buffers, regardless of stack allocation depth.
185
-
186
-
The execution of defensive `if` branches and bounds checks occupies the ALU instruction pipeline. During this execution window, the CPU's prefetcher asynchronously loads upcoming metadata cache lines in the background. By the time safety checks are evaluated, the memory is already waiting in L1, resulting in a **zero-cycle memory stall**.
187
-
188
-
***Absolute Dominance:**
189
-
Even under full runtime safety checks and larger capacities, **EasyStack remains up to 191% faster than Trebi (C++) and 153% faster than wb_alloc (C)**.
199
+
#### RAW Results (Best of 25 runs, 2,000,000 iterations/run)
Traditional stack allocators prefix each block with a fixed 16-byte inline header (on 64-bit systems). On small allocations (common for temporary stacks), this inline overhead consumes up to **50%+ of your buffer**.
195
224
@@ -198,15 +227,44 @@ Traditional stack allocators prefix each block with a fixed 16-byte inline heade
198
227
Below is a comparison of usable payload space in a **10 KB buffer** across different allocation sizes:
***For 8-byte allocations:**`easy_stack` lets you fit **2.40x more objects** into the same memory buffer (80% usable memory vs. 33%).
205
-
***For 16-byte allocations:**`easy_stack` lets you fit **1.77x more objects** into the same memory buffer (89% usable memory vs. 50%).
233
+
***For 8-byte allocations:**`easy_stack` lets you fit **2.40x more objects** into the same memory buffer (80% usable memory vs. 33%).
234
+
***For 16-byte allocations:**`easy_stack` lets you fit **1.77x more objects** into the same memory buffer (89% usable memory vs. 50%).
206
235
207
-
---
208
236
*Note: The chart above represents the absolute best-case scenario for the traditional allocator, assuming perfect power-of-two allocation sizes with 0 alignment padding. In real-world workloads with non-power-of-two sizes, traditional inline-header allocators suffer from internal fragmentation (wasting up to 7-15 bytes of alignment padding per block), which widens the efficiency gap even further in favor of `easy_stack`.*
209
237
238
+
---
239
+
240
+
### 4. Hardware-Level Profiling (perf)
241
+
242
+
To verify the microarchitectural efficiency of the allocator and isolate the hardware reasons behind these execution speeds, the benchmark suite (with `BENCH_ONLY_EASYSTACK` active) was profiled using Linux `perf` hardware performance counters on a Zen-architecture CPU.
243
+
244
+
The raw hardware counters and derived efficiency metrics across all tested stack depths are presented below:
|**Cache Miss Rate (%)**|**0.64%**|**2.28%**|**1.24%**|
257
+
258
+
---
259
+
260
+
### Architectural Analysis of Hardware Metrics:
261
+
262
+
***Instruction Pipeline Saturation (IPC)**: Operating at **`3.08 - 3.13 IPC`** (calculated as instructions / cycles) is an exceptional result. While complex system-level workloads typically average `1.2 - 1.8 IPC` due to CPU pipeline stalls waiting for RAM data, EasyStack keeps the processor executing more than 3 instructions per clock cycle with near-zero execution stalls or data dependencies.
263
+
264
+
***Near-Zero Branch Predictor Penalties**: Across billions of branches executed, the processor encountered a misprediction rate of just **`0.00013% - 0.00015%`** (calculated as branch-misses / branches). By eliminating heavy lookup loops, tree traversals, and dynamic boundary checks, the critical path compiles into a highly predictable, linear instruction stream, allowing the CPU to speculation-execute allocations hundreds of cycles ahead.
265
+
266
+
***L1 Data Cache Residency**: Cache misses remain extremely low, dropping to **`0.64%`** at depth 15 and hovering at just **`1.24%`** at maximum depth (calculated as cache-misses / cache-references). By segregating metadata from aligned user payloads (Inverted Layout), the active metadata array is tightly packed and remains resident in standard 64-byte L1 CPU cache lines, bypassing main memory latency entirely.
0 commit comments