zero_gc_allocator 1.0.0 copy "zero_gc_allocator: ^1.0.0" to clipboard
zero_gc_allocator: ^1.0.0 copied to clipboard

Ultra-high-performance manual memory management for Dart/Flutter using FFI, offering arena, pool, and slab allocators outside the Dart GC for zero GC pressure and deterministic O(1) allocation latency [...]

zero_gc_allocator #

Ultra-high-performance manual memory management for Dart/Flutter.
Allocate on the native heap — completely outside Dart's Garbage Collector.
Zero stop-the-world pauses. Deterministic O(1) allocation latency.

pub version Dart 3+ License: MIT


The Problem #

Dart's Garbage Collector uses "Stop-The-World" (STW) pauses: at unpredictable moments, the GC halts all threads for 0.5 ms–50 ms to reclaim heap objects. For most applications this is invisible. For these applications, it is catastrophic:

Use Case Budget GC Pause Impact
120 fps game 8.3 ms/frame 1 GC pause = 6 dropped frames
HFT order book < 1 μs/op 1 GC pause = 50,000+ missed ticks
WebSocket server (1M conns) sub-ms GC scales with heap size
Binary protocol parser < 100 ns/msg GC pauses invalidate SLAs

The Solution #

zero_gc_allocator reserves a large block of native memory (via dart:ffi) in one syscall. Subsequent allocations use a bump-pointer strategy — a single integer increment — that the GC never observes.

One syscall (e.g. 1 GB)
     ↓
┌────────────────────────────────────────────────────────┐
│               Native Heap Block                        │
├──────────┬──────────┬──────────────┬───────────────────┤
│ alloc #1 │ alloc #2 │  alloc #3    │       FREE        │
│  (2 ns)  │  (2 ns)  │   (2 ns)     │                   │
└──────────┴──────────┴──────────────┴───────────────────┘
                                      ↑
                                   cursor

arena.reset() → cursor = 0    (O(1), no GC)
arena.dispose() → free block  (O(1), one syscall)

Quick Start #

import 'package:zero_gc_allocator/zero_gc_allocator.dart';

void main() {
  // 1. Reserve 1 GB native memory (one syscall)
  final arena = ZeroGcArena(size: 1.gb);

  // 2. O(1) typed allocation — completely GC-free
  final prices = arena.allocTyped<Double>(1000000);
  for (int i = 0; i < 1000000; i++) {
    (prices + i).value = 49234.50 + i * 0.01;
  }

  // 3. Scoped temporary allocation (O(1) cleanup)
  final scope = arena.saveCheckpoint();
  final temp = arena.alloc(4.mb);
  arena.restoreCheckpoint(scope); // 4 MB freed instantly

  // 4. Reset entire arena for reuse (O(1))
  arena.reset();

  // 5. Dispose when done
  arena.dispose();
}

Installation #

# pubspec.yaml
dependencies:
  zero_gc_allocator: ^1.0.0
dart pub get

Allocator Types #

Type alloc free reset Individual free Best For
ZeroGcArena O(1) N/A O(1) ❌ Bulk scoped allocs
ZeroGcPool O(1) O(1) O(n) ✅ Same-size objects
ZeroGcSlab<T> O(1) O(1) O(n) ✅ typed Typed structs
BumpAllocator O(1) N/A O(1) ❌ Raw low-level

ZeroGcArena #

The primary allocator. One contiguous block, bump-pointer strategy.

// Create
final arena = ZeroGcArena(size: 512.mb);
final arena = ZeroGcArena(size: 1.gb, alignment: 16); // SIMD-safe base

// Raw bytes
final ptr = arena.alloc(1024);
final ptr = arena.alloc(64, alignment: 64); // cache-line aligned

// Typed arrays
final floats  = arena.allocTyped<Float>(256);   // 256 × 4 B = 1 KB
final doubles = arena.allocTyped<Double>(1000); // 1000 × 8 B = 8 KB
final ints    = arena.allocTyped<Int32>(512);

// Uninitialized (slightly faster when you'll overwrite everything)
final buf = arena.allocUninit(4096);

// Scoped regions (nested checkpoints)
final region = arena.saveCheckpoint();
final tmp = arena.alloc(8.mb);
processTemp(tmp);
arena.restoreCheckpoint(region); // O(1) — tmp freed

// Reset (O(1))
arena.reset();                     // all allocations freed, block reused
arena.reset(zeroMemory: true);     // also zeroes bytes (security-sensitive)

// Inspect
print(arena.usedBytes);            // bytes consumed
print(arena.remainingBytes);       // bytes available
print(arena.capacity);             // total size
print(arena.stats);                // full stats table
print(arena.stats.utilizationPercent); // e.g. "73.42%"

// Dispose
arena.dispose();

ZeroGcPool #

Fixed-block-size pool with embedded free-list. Supports individual free().

// 50,000 slots of 48 bytes each ≈ 2.4 MB
final pool = ZeroGcPool(blockSize: 48, capacity: 50000);

final slot = pool.alloc();         // O(1) — pops free-list head
writeData(slot);

pool.free(slot);                   // O(1) — pushes back to free-list

print(pool.usedSlots);             // currently allocated
print(pool.freeSlots);             // available
print(pool.isFull);                // true when exhausted

pool.reset();                      // O(n) — reset all slots to free
pool.dispose();                    // O(1) — free native block

ZeroGcSlab<T> #

Type-safe pool for structs of uniform type. Returns Pointer<T> directly.

// Vec3 slab: 50K slots × (3 × Float32) = 600 KB
final vec3Slab = ZeroGcSlab<Float>(elementCount: 50000, stride: 3);

final v = vec3Slab.alloc();  // → Pointer<Float> to 3 floats
v[0] = 1.0; // x
v[1] = 0.0; // y
v[2] = 0.0; // z

vec3Slab.free(v);            // O(1)

// Mat4 slab: 1000 slots × (16 × Float32) = 64 KB
final matSlab = ZeroGcSlab<Float>(elementCount: 1000, stride: 16);
final mat = matSlab.alloc(); // → Pointer<Float> to 16 floats

Typed Wrappers #

NativeList<T> #

final prices = NativeList<Double>(arena: arena, length: 1000000);

// Element access
(prices[0]).value = 49234.50;
final v = (prices[999999]).value;

// Sub-list view (zero-copy)
final batch = prices.subList(0, 1000); // elements [0..999]

// Info
print(prices.sizeInBytes); // 1000000 × 8 = 8 MB
print(prices.basePointer); // raw Pointer<Double>

NativeString #

final s = NativeString.fromDart('Hello, world!', arena: arena);
print(s.toDartString()); // → "Hello, world!"
print(s.byteLength);     // → 13 (UTF-8 byte count)

// Concatenation without Dart string allocation
final header = NativeString.concat(
  ['Content-Length: ', size.toString(), '\r\n'],
  arena: arena,
);

NativeBuffer #

final buf = NativeBuffer(arena: arena, size: 1024);

// Random-access read/write
buf.writeUint32LE(0, 0xDEADBEEF);
buf.writeFloat64LE(4, 3.14159);
final magic = buf.readUint32LE(0); // → 0xDEADBEEF

// Sequential append
buf.appendUint8(0x01);
buf.appendUint32LE(0xCAFE);
buf.appendFloat64LE(price);

// Zero-copy view
final view = buf.asByteData(); // ByteData sharing native memory
final list = buf.asUint8List(); // Uint8List view

Extensions #

Size Literals #

1.bytes   // 1
1.kb      // 1,024
1.mb      // 1,048,576
1.gb      // 1,073,741,824
2.5.mb    // 2,621,440
512.mb.toSizeString() // → "512.00 MB"

Pointer Extensions #

final ptr = arena.alloc(256);

ptr.fill(256, 0xAB);           // fill bytes
ptr.zeroFill(256);             // zero out
ptr.copyFrom(other, 128);      // memcpy
ptr.memEquals(other, 128);     // memcmp == 0
ptr.isAlignedTo(64);           // address % 64 == 0
ptr.castAt<Uint32>(8);         // Pointer<Uint32> at byte offset 8
ptr.hexDump(64);               // formatted hex dump
ptr.toHexAddress();            // "0x7FFF1234ABCD"

Debug: Memory Guard #

Detects buffer overflows in debug/test builds:

final guard = MemoryGuard(
  ZeroGcArena(size: 1.mb),
  guardSize: 16,
);

final buf = guard.alloc(64);
// ... your code ...

guard.verify();  // throws MemoryOverflowDetected if buf was overflowed

// Or:
if (!guard.isIntact()) {
  print('Buffer overflow detected!');
}

guard.dispose();

Real-World Examples #

Game Loop (120 fps) #

final frameArena   = ZeroGcArena(size: 32.mb);  // per-frame temp data
final particlePool = ZeroGcPool(blockSize: 32, capacity: 100000);

while (running) {
  final frame = frameArena.saveCheckpoint();

  final verts  = frameArena.allocTyped<Float>(vertexCount * 3);
  final cmds   = frameArena.allocTyped<Int32>(drawCallCount * 4);
  final lights = frameArena.allocTyped<Float>(lightCount * 8);

  render(verts, cmds, lights);

  frameArena.restoreCheckpoint(frame); // all freed, O(1), zero GC
}

HFT Order Book #

final orderPool = ZeroGcPool(blockSize: 48, capacity: 1000000);

final orderAddr = orderPool.alloc().address;
Pointer<Double>.fromAddress(orderAddr + 8).value = 49234.50; // price
Pointer<Int64>.fromAddress(orderAddr + 16).value = 100;       // qty

// Cancel — O(1), no GC
orderPool.free(Pointer<Uint8>.fromAddress(orderAddr));

WebSocket Server #

final recvPool = ZeroGcPool(blockSize: 4096, capacity: 1000000);
final sendPool = ZeroGcPool(blockSize: 4096, capacity: 1000000);
final metaSlab = ZeroGcSlab<Int64>(elementCount: 1000000, stride: 8);

// On connect — O(1), no GC
final recv = recvPool.alloc();
final send = sendPool.alloc();
final meta = metaSlab.alloc();
(meta + 0).value = connectionId;

// On disconnect — O(1), no GC
recvPool.free(recv);
sendPool.free(send);
metaSlab.free(meta);

Performance #

Measured on Apple M2 Pro, Dart 3.4, -O2 optimizations:

Operation Dart GC ZeroGcArena Speedup
1M × 8-byte allocs ~50 ms + STW pauses ~2 ms 25× + zero STW
Single alloc (ns) 50–500 ns (amortized) 2–5 ns 10–100×
Reset 8 MB ~80 ms (GC) 0.001 ms 80,000×
GC pause 0.5–50 ms 0 ms ∞

STW pauses are not included in Dart GC numbers above — they add 0.5–50 ms unpredictably.


Safety Rules #

Violating these rules causes undefined behavior (crash, corruption, or silent data corruption). No exception is thrown — the program is wrong.

  1. Never use a Pointer after arena.dispose().
  2. Never use a Pointer after arena.reset().
  3. Never write more than blockSize bytes into a pool slot.
  4. Arenas are NOT thread-safe. One arena per Isolate.
  5. NativeBuffer.asByteData() views are invalidated on reset/dispose.
  6. Only pass pool.free(ptr) pointers that came from that same pool.

Platform Support #

Platform Supported
Android (arm64, arm, x64) ✅
iOS (arm64) ✅
macOS (arm64, x64) ✅
Linux (x64, arm64) ✅
Windows (x64) ✅
Web / WASM ❌ (dart:ffi not available)

Architecture #

zero_gc_allocator
│
├── ZeroGcArena           ← Primary: bump-pointer arena
│   ├── alloc / allocTyped / allocUninit
│   ├── saveCheckpoint / restoreCheckpoint
│   └── reset / dispose
│
├── ZeroGcPool            ← Fixed-size blocks, individual free
│   ├── alloc / free
│   └── reset / dispose
│
├── ZeroGcSlab<T>         ← Type-safe pool
│   ├── alloc / free (typed Pointer<T>)
│   └── reset / dispose
│
├── BumpAllocator         ← Raw low-level engine (ZeroGcArena wrapper)
│
├── Typed wrappers
│   ├── TypedPtr<T>       ← Single value / array with arena lifecycle
│   ├── NativeList<T>     ← Bounds-checked typed array
│   ├── NativeString      ← UTF-8 native string
│   └── NativeBuffer      ← Binary I/O buffer (read/write/append)
│
├── Extensions
│   ├── SizeExtension     ← 1.gb, 512.mb, 4.kb
│   └── PtrExtensions     ← fill, copyFrom, hexDump, castAt<T>
│
├── MemoryGuard           ← Debug overflow detector
│
└── Utilities
    ├── AlignmentUtils    ← alignUp, alignDown, isPowerOfTwo
    └── PlatformUtils     ← Platform detection

License #

MIT — see LICENSE


Built with love for the 0.001% of Dart developers who need to count nanoseconds.

0
likes
130
points
12
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Ultra-high-performance manual memory management for Dart/Flutter using FFI, offering arena, pool, and slab allocators outside the Dart GC for zero GC pressure and deterministic O(1) allocation latency in latency-critical systems.

Repository (GitHub)
View/report issues

Topics

#ffi #memory #performance #native #allocator

License

MIT (license)

Dependencies

ffi

More

Packages that depend on zero_gc_allocator