perf: Better arena performance

This commit is contained in:
2025-12-16 21:16:37 +01:00
parent f50b9232f3
commit 86e93fa8bc
4 changed files with 267 additions and 43 deletions
+1
View File
@@ -1 +1,2 @@
.vscode/**
coverage.out
+12 -12
View File
@@ -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)
}
}
+105 -32
View File
@@ -3,17 +3,37 @@ package gol
import (
"bytes"
"os"
"runtime"
"sync"
)
type Arena [][]bool
func New(ysize, xsize int) Arena {
arena := make([][]bool, ysize)
for i := range ysize {
arena[i] = make([]bool, xsize)
type Arena struct {
w, h int
data []bool
}
return arena
func New(ysize, xsize int) *Arena {
return &Arena{
w: xsize,
h: ysize,
data: make([]bool, xsize*ysize),
}
}
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
}
func (a Arena) life(x, y int) bool {
ysize := len(a)
xsize := len(a[0])
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) 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
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
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] {
// Top row
if a.data[yPrevOffset+xPrev] {
count++
}
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 {
return false // early exit
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)
}
+150
View File
@@ -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
}
}