Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ jobs:
cache: true

- name: Run fuzz tests
run: go test -fuzz=Fuzz -fuzztime=30s ./...
run: |
go test -fuzz=FuzzAllocFree -fuzztime=30s ./...
go test -fuzz=FuzzAllocAligned -fuzztime=30s ./...
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,21 @@ if alloc.Failed() {
}
// use alloc.Offset
a.Free(alloc)

// GPU-aligned allocation (e.g., 256-byte for Vulkan uniform buffers)
aligned := a.AllocateAligned(4096, 256)
// aligned.Offset % 256 == 0
a.Free(aligned)
```

For concurrent use: `galloc.NewSync(size, maxAllocs)`.

## Architecture

```
galloc.go -- Allocator core: Allocate, Free, coalescing, bin management
galloc.go -- Allocator core: Allocate, AllocateAligned, Free, coalescing, bin management
smallfloat.go -- SmallFloat uint-to-bin encoding (256 bins, 3-bit mantissa)
sync.go -- SyncAllocator (mutex-wrapped Allocator)
sync.go -- SyncAllocator (mutex-wrapped Allocator + AllocateAligned)
doc.go -- Package documentation
```

Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-07-27

### Added

- `AllocateAligned(size, alignment)` — per-allocation aligned sub-allocation (TLSF-style over-allocate)
- `SyncAllocator.AllocateAligned` — thread-safe aligned allocation wrapper
- GPU alignment support: Vulkan (256), DX12 (4–65536), Metal (64–256)
- Alignment=0 or 1 fast-paths to `Allocate` with zero overhead
- Non-power-of-2 alignment panics (programmer error, same as `sync.Pool`)
- Fuzz testing for aligned allocation (`FuzzAllocAligned`)
- Benchmarks for aligned allocation (~50ns/op, 0 allocs/op)

## [0.1.0] - 2026-07-25

### Added
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ func main() {
}
```

## Aligned Allocation

GPU APIs require buffer offsets to be aligned (e.g., 256 bytes for Vulkan uniform buffers). `AllocateAligned` guarantees the returned offset is a multiple of the given alignment:

```go
// Vulkan uniform buffer: 256-byte alignment.
alloc := a.AllocateAligned(4096, 256)
fmt.Printf("Offset: %d (aligned: %v)\n", alloc.Offset, alloc.Offset%256 == 0)

// DX12 texture data: 512-byte alignment.
tex := a.AllocateAligned(65536, 512)

// Alignment of 0 or 1 is equivalent to Allocate (zero overhead).
plain := a.AllocateAligned(100, 1)

a.Free(alloc)
a.Free(tex)
a.Free(plain)
```

Alignment must be a power of two. The implementation over-allocates by up to `alignment - 1` bytes; these padding bytes are reclaimed when the allocation is freed.

## Algorithm

The allocator is based on [TLSF](http://www.gii.upv.es/tlsf/) (Two-Level Segregated Fit) principles adapted for offset-only allocation (no actual memory management).
Expand Down
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
- [x] Enterprise CI (3 OS, lint, formatting, fuzz, Codecov)
- [x] 22 tests, 6 benchmarks, fuzz testing

## v0.2.0 — Integration
## v0.2.0 — Alignment + Integration

- [ ] **FEAT-001: `AllocateAligned(size, alignment)` — per-allocation alignment** (ADR-001)
- [ ] Integration with `wgpu` Vulkan memory pools (replace BuddyAllocator)
- [ ] Integration with `wgpu` DX12 descriptor heaps
- [ ] Extended statistics (per-bin occupancy, fragmentation metrics)
- [ ] `AllocationInfo` with bin index and actual allocated size

## v0.3.0 — Optimization

- [ ] Trim approach for aligned allocation (zero-waste prefix reclaim)
- [ ] Benchmark suite vs BuddyAllocator (side-by-side comparison)
- [ ] Memory layout optimization (cache-friendly node pool)
- [ ] `usedBins [8]uint32` variant (fewer type casts on hot path)
Expand Down
21 changes: 21 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ func BenchmarkAllocMany(b *testing.B) {
}
}

func BenchmarkAllocateAligned256(b *testing.B) {
a := New(1024*1024*256, 1024)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
alloc := a.AllocateAligned(4096, 256)
a.Free(alloc)
}
}

func BenchmarkAllocateAligned1(b *testing.B) {
// Fast path: alignment=1 should be identical to Allocate.
a := New(1024*1024*256, 1024)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
alloc := a.AllocateAligned(256, 1)
a.Free(alloc)
}
}

func BenchmarkFragmented(b *testing.B) {
// Interleaved alloc/free pattern to stress coalescing.
const maxAllocs = 4096
Expand Down
54 changes: 54 additions & 0 deletions fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,57 @@ func FuzzAllocFree(f *testing.F) {
}
})
}

func FuzzAllocAligned(f *testing.F) {
f.Add(uint32(65536), uint8(20), uint16(256), uint8(8)) // align=256
f.Add(uint32(1024), uint8(5), uint16(64), uint8(2)) // align=4
f.Add(uint32(1024*1024), uint8(50), uint16(4096), uint8(9)) // align=512

f.Fuzz(func(t *testing.T, totalSize uint32, numOps uint8, allocSize uint16, alignShift uint8) {
if totalSize == 0 || totalSize > 1024*1024*16 {
return
}
// Clamp alignment to [1, 65536] as power of 2.
alignShift %= 17 // 0..16 → 1..65536
alignment := uint32(1) << alignShift

maxAllocs := uint32(numOps)*2 + 16
if maxAllocs > 65536 {
maxAllocs = 65536
}

a := New(totalSize, maxAllocs)

var live []Allocation
for i := uint8(0); i < numOps; i++ {
size := uint32(allocSize)
if size == 0 {
size = 1
}
alloc := a.AllocateAligned(size, alignment)
if !alloc.Failed() {
if alloc.Offset%alignment != 0 {
t.Fatalf("offset %d not aligned to %d", alloc.Offset, alignment)
}
live = append(live, alloc)
}

if len(live) > 4 && i%3 == 0 {
half := len(live) / 2
for _, alloc := range live[:half] {
a.Free(alloc)
}
live = live[half:]
}
}

for _, alloc := range live {
a.Free(alloc)
}

report := a.StorageReport()
if report.TotalFreeSpace != totalSize {
t.Errorf("after freeing all: TotalFreeSpace = %d, want %d", report.TotalFreeSpace, totalSize)
}
})
}
26 changes: 26 additions & 0 deletions galloc.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,32 @@ func (a *Allocator) Allocate(size uint32) Allocation {
}
}

// AllocateAligned reserves a contiguous region of the given size at an offset
// that is a multiple of alignment. Alignment must be a power of two; passing a
// non-power-of-two value (other than 0) will panic. Alignment of 0 or 1 is
// equivalent to [Allocate].
//
// The implementation over-allocates by up to alignment-1 bytes to guarantee an
// aligned offset within the block. These padding bytes are reclaimed when the
// allocation is freed.
//
// A size of 0 is treated as a valid allocation (matching [Allocate] behavior).
func (a *Allocator) AllocateAligned(size, alignment uint32) Allocation {
if alignment <= 1 {
return a.Allocate(size)
}
if alignment&(alignment-1) != 0 {
panic("galloc: alignment must be a power of two")
}
padded := size + alignment - 1
alloc := a.Allocate(padded)
if alloc.Failed() {
return alloc
}
alloc.Offset = (alloc.Offset + alignment - 1) &^ (alignment - 1)
return alloc
}

// Free releases a previously-made allocation, returning its space to the pool.
// Adjacent free regions are automatically coalesced.
//
Expand Down
Loading
Loading