kelindar/tile

Function "Within" is slower than iterating with "At"

Xkonti opened this issue · 0 comments

Xkonti commented

I noticed that when using the Within on the grid to iterate through values of all tiles it's slower than when iterating by simply using At. Here are benchmarks I quickly wrote:

Benchmark testing the performance of the built-in Within:

func BenchmarkGridWithin(b *testing.B) {
	// Generate map
	theMap := tile.NewGridOf[tileData](900, 900)
	theMap.Each(func(pos point, currentTile tile.Tile[tileData]) {
		currentTile.Write(getRandomSliceIndex(tileImages))
	})

	// Benchmark
	chunkSize := 9
	for i := 0; i < b.N; i++ {
		pos := point{X: int16((i * 9) % 900), Y: int16((i * 45) % 900)}
		theMap.Within(pos, pos.Add(point{X: chunkSize * 3, Y: chunkSize * 3}), func(tilePos point, currentTile tile.Tile[tileData]) {
			value := currentTile.Value()
			_ = tileImages[int(value)]
		})
	}
}

Benchmark using At where whole chunk is scanned with a simple double for loop:

func BenchmarkGridAtScan(b *testing.B) {
	// Generate map
	theMap := tile.NewGridOf[tileData](900, 900)
	theMap.Each(func(pos point, currentTile tile.Tile[tileData]) {
		currentTile.Write(getRandomSliceIndex(tileImages))
	})

	// Benchmark
	chunkSize := 9
	for i := 0; i < b.N; i++ {
		pos := point{X: int16((i * 9) % 900), Y: int16((i * 45) % 900)}
		for x := pos.X; x < pos.X+chunkSize*3; x++ {
			for y := pos.Y; y < pos.Y+chunkSize*3; y++ {
				currentTile, ok := theMap.At(x, y)
				if !ok {
					continue
				}

				value := currentTile.Value()
				_ = tileImages[int(value)]
			}
		}
	}
}

A benchmark that tries to get values using At but working on a page at a time:

func BenchmarkGridAtPage(b *testing.B) {
	// Generate map
	theMap := tile.NewGridOf[tileData](900, 900)
	theMap.Each(func(pos point, currentTile tile.Tile[tileData]) {
		currentTile.Write(getRandomSliceIndex(tileImages))
	})

	// Benchmark
	chunkSize := 9
	for i := 0; i < b.N; i++ {
		pos := point{X: int16((i * 9) % 900), Y: int16((i * 45) % 900)}
                // Iterate through pages
		for x := pos.X; x < pos.X+chunkSize*3; x += 3 {
			for y := pos.Y; y < pos.Y+chunkSize*3; y += 3 {
                                // Get all tiles in a page
				for tileX := x; tileX < x+3; tileX++ {
					for tileY := y; tileY < y+3; tileY++ {
						currentTile, ok := theMap.At(tileX, tileY)
						if !ok {
							continue
						}

						value := currentTile.Value()
						_ = tileImages[int(value)]
					}
				}
			}
		}
	}
}

Results show that At is significantly faster than Within. Also accessing tile's values page-at-a-time increases performance by only ≈0.82%:

goos: windows
goarch: amd64
pkg: tile-game
cpu: 12th Gen Intel(R) Core(TM) i9-12900KF
BenchmarkGridWithin-24            531561              2251 ns/op
BenchmarkGridAtScan-24            656180              1826 ns/op
BenchmarkGridAtPage-24            661200              1811 ns/op

I'm not sure if that's intended behavior but something tells me there's an opportunity for adding an optimized values getter.