From 86e93fa8bc438da6f5e17128cf63d13a5cff0b1d Mon Sep 17 00:00:00 2001 From: Arek Date: Tue, 16 Dec 2025 21:16:37 +0100 Subject: [PATCH] perf: Better arena performance --- .gitignore | 1 + cmd/gol/gol.go | 24 ++++---- gol/arena.go | 135 +++++++++++++++++++++++++++++++---------- gol/arena_test.go | 150 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 43 deletions(-) create mode 100644 gol/arena_test.go diff --git a/.gitignore b/.gitignore index f16fa55..ec084e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .vscode/** +coverage.out diff --git a/cmd/gol/gol.go b/cmd/gol/gol.go index de10e4e..3a47329 100644 --- a/cmd/gol/gol.go +++ b/cmd/gol/gol.go @@ -34,26 +34,26 @@ func main() { game(*initrandom) } -func initArena(front gol.Arena, initRandom bool) { +func initArena(front *gol.Arena, initRandom bool) { if initRandom { - for y := range front { - for x := range front[y] { - front[y][x] = rand.Intn(2) == 1 + for y := 0; y < front.Height(); y++ { + for x := 0; x < front.Width(); x++ { + front.Set(x, y, rand.Intn(2) == 1) } } } else { // draw initial objects // glider - front[0][3] = true - front[1][4] = true - front[2][2] = true - front[2][3] = true - front[2][4] = true + front.Set(3, 0, true) + front.Set(4, 1, true) + front.Set(2, 2, true) + front.Set(3, 2, true) + front.Set(4, 2, true) // cross - front[16][21] = true - front[17][21] = true - front[18][21] = true + front.Set(21, 16, true) + front.Set(21, 17, true) + front.Set(21, 18, true) } } diff --git a/gol/arena.go b/gol/arena.go index 5c16ddd..b99a543 100644 --- a/gol/arena.go +++ b/gol/arena.go @@ -3,17 +3,37 @@ package gol import ( "bytes" "os" + "runtime" "sync" ) -type Arena [][]bool +type Arena struct { + w, h int + data []bool +} -func New(ysize, xsize int) Arena { - arena := make([][]bool, ysize) - for i := range ysize { - arena[i] = make([]bool, xsize) +func New(ysize, xsize int) *Arena { + return &Arena{ + w: xsize, + h: ysize, + data: make([]bool, xsize*ysize), } - return arena +} + +func (a *Arena) Set(x, y int, val bool) { + a.data[y*a.w+x] = val +} + +func (a *Arena) Get(x, y int) bool { + return a.data[y*a.w+x] +} + +func (a *Arena) Width() int { + return a.w +} + +func (a *Arena) Height() int { + return a.h } // bufPool @@ -28,13 +48,14 @@ var ( spriteOff = ([]byte)("\u2022\u2022") ) -func (a Arena) PrintMe() { +func (a *Arena) PrintMe() { b := bufPool.Get().(*bytes.Buffer) b.Reset() - for i := range a { - for _, v := range a[i] { - if v { + for y := 0; y < a.h; y++ { + base := y * a.w + for x := 0; x < a.w; x++ { + if a.data[base+x] { b.Write(spriteOn) } else { b.Write(spriteOff) @@ -46,34 +67,86 @@ func (a Arena) PrintMe() { bufPool.Put(b) } -func (a Arena) NextGen(to Arena) { - for y := range a { - for x := range a[y] { - to[y][x] = a.life(x, y) - } +func (a *Arena) NextGen(to *Arena) { + var wg sync.WaitGroup + numWorkers := runtime.NumCPU() + rowsPerWorker := a.h / numWorkers + if rowsPerWorker == 0 { + rowsPerWorker = 1 + numWorkers = a.h } + + for i := 0; i < numWorkers; i++ { + startY := i * rowsPerWorker + endY := (i + 1) * rowsPerWorker + if i == numWorkers-1 { + endY = a.h + } + + wg.Add(1) + go func(startY, endY int) { + defer wg.Done() + a.processChunk(to, startY, endY) + }(startY, endY) + } + wg.Wait() } -func (a Arena) life(x, y int) bool { - ysize := len(a) - xsize := len(a[0]) +func (a *Arena) processChunk(to *Arena, startY, endY int) { + w, h := a.w, a.h + for y := startY; y < endY; y++ { + // Precompute y indices + yPrev := (y - 1 + h) % h + yNext := (y + 1) % h - count := 0 - for dy := -1; dy <= 1; dy++ { - for dx := -1; dx <= 1; dx++ { - if dx == 0 && dy == 0 { - continue - } - ny := (y + dy + ysize) % ysize - nx := (x + dx + xsize) % xsize - if a[ny][nx] { + yPrevOffset := yPrev * w + yCurrOffset := y * w + yNextOffset := yNext * w + + for x := 0; x < w; x++ { + // Precompute x indices + xPrev := (x - 1 + w) % w + xNext := (x + 1) % w + + count := 0 + + // Top row + if a.data[yPrevOffset+xPrev] { count++ } - if count > 3 { - return false // early exit + if a.data[yPrevOffset+x] { + count++ } + if a.data[yPrevOffset+xNext] { + count++ + } + + // Middle row (excluding self) + if a.data[yCurrOffset+xPrev] { + count++ + } + if a.data[yCurrOffset+xNext] { + count++ + } + + // Bottom row + if a.data[yNextOffset+xPrev] { + count++ + } + if a.data[yNextOffset+x] { + count++ + } + if a.data[yNextOffset+xNext] { + count++ + } + + if count > 3 { + to.data[yCurrOffset+x] = false + continue + } + + iAmAlive := a.data[yCurrOffset+x] + to.data[yCurrOffset+x] = count == 3 || (iAmAlive && count == 2) } } - iAmAlive := a[y][x] - return count == 3 || (iAmAlive && count == 2) } diff --git a/gol/arena_test.go b/gol/arena_test.go new file mode 100644 index 0000000..958b94c --- /dev/null +++ b/gol/arena_test.go @@ -0,0 +1,150 @@ +package gol + +import ( + "bytes" + "io" + "os" + "testing" +) + +func TestNew(t *testing.T) { + w, h := 10, 20 + a := New(h, w) + if a.Width() != w { + t.Errorf("expected width %d, got %d", w, a.Width()) + } + if a.Height() != h { + t.Errorf("expected height %d, got %d", h, a.Height()) + } +} + +func TestSetGet(t *testing.T) { + a := New(10, 10) + a.Set(1, 1, true) + if !a.Get(1, 1) { + t.Error("expected cell at 1,1 to be true") + } + if a.Get(0, 0) { + t.Error("expected cell at 0,0 to be false") + } +} + +func TestNextGen(t *testing.T) { + // Blinker pattern (period 2) + // . . . . . + // . . 1 . . + // . . 1 . . + // . . 1 . . + // . . . . . + // becomes + // . . . . . + // . . . . . + // . 1 1 1 . + // . . . . . + // . . . . . + + a := New(5, 5) + a.Set(2, 1, true) + a.Set(2, 2, true) + a.Set(2, 3, true) + + next := New(5, 5) + a.NextGen(next) + + if !next.Get(1, 2) || !next.Get(2, 2) || !next.Get(3, 2) { + t.Error("expected horizontal blinker") + } + if next.Get(2, 1) || next.Get(2, 3) { + t.Error("expected vertical cells to die") + } +} + +func TestRules(t *testing.T) { + // Neighbors indices around (1,1) + neighbors := [][2]int{ + {0, 0}, {1, 0}, {2, 0}, + {0, 1} /*...*/, {2, 1}, + {0, 2}, {1, 2}, {2, 2}, + } + + check := func(name string, alive bool, activeNeighbors int, expected bool) { + a := New(3, 3) + // Set center + a.Set(1, 1, alive) + // Set neighbors + for i := 0; i < activeNeighbors; i++ { + nx, ny := neighbors[i][0], neighbors[i][1] + a.Set(nx, ny, true) + } + + next := New(3, 3) + a.NextGen(next) + + if next.Get(1, 1) != expected { + t.Errorf("%s: expected %v, got %v (alive=%v, neighbors=%d)", name, expected, next.Get(1, 1), alive, activeNeighbors) + } + } + + // Underpopulation + check("Underpopulation 0", true, 0, false) + check("Underpopulation 1", true, 1, false) + + // Survival + check("Survival 2", true, 2, true) + check("Survival 3", true, 3, true) + + // Overpopulation + check("Overpopulation 4", true, 4, false) + check("Overpopulation 5", true, 5, false) + check("Overpopulation 8", true, 8, false) + + // Reproduction + check("Reproduction 3", false, 3, true) + + // Stay Dead + check("Stay Dead 2", false, 2, false) + check("Stay Dead 4", false, 4, false) +} + +func TestPrintMe(t *testing.T) { + // Capture stdout + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + a := New(2, 2) + a.Set(0, 0, true) + a.PrintMe() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + io.Copy(&buf, r) + + // We expect some output + if buf.Len() == 0 { + t.Error("expected output from PrintMe") + } +} + +func BenchmarkNextGen(b *testing.B) { + size := 100 + arena := New(size, size) + next := New(size, size) + + // Initialize with some pattern (e.g., random-ish) + for y := 0; y < size; y++ { + for x := 0; x < size; x++ { + if (x+y)%2 == 0 { + arena.Set(x, y, true) + } + } + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + arena.NextGen(next) + arena, next = next, arena + } +}