# Performance Considerations When Using Fabrica-Util’s Bitmap for Game State Tracking

> Explore performance considerations for fabrica-util's Bitmap in game state tracking. Achieve thread-safe, easy game state management with this structure, balancing speed and concurrency.

- Repository: [Pantheon/fabrica-util](https://github.com/go-pantheon/fabrica-util)
- Tags: performance
- Published: 2026-03-02

---

**Fabrica-util's `Bitmap` trades raw speed for thread-safety and ease of use, making it ideal for moderate-contention game state tracking but requiring careful architectural decisions under high concurrency.**

The `bitmap` package in `go-pantheon/fabrica-util` provides a compact, thread-safe data structure for tracking boolean game states such as occupied tiles, active entities, or triggered flags. While its API is straightforward, understanding the implementation details in [`bitmap/bitmap.go`](https://github.com/go-pantheon/fabrica-util/blob/main/bitmap/bitmap.go) is essential to avoid bottlenecks in performance-critical game loops. Below is a comprehensive analysis of the architectural trade-offs and optimization strategies.

## Memory Layout and Footprint

The `Bitmap` stores bits in a contiguous slice of bytes (`[]byte`), allocating exactly `ceil(size/8)` bytes where *size* is the bit count specified in `NewBitmap`. For a typical 10,000-cell game grid, the memory footprint is approximately 1.25 KB—negligible compared to entity or texture data.

The struct caches the original size in an `int64` field to optimize bounds checking:

```go
type Bitmap struct {
    mutex sync.Mutex
    bits  []byte
    size  int64
}

```

This design eliminates the need to compute slice length during operations, but the byte-slice indirection means every access requires pointer arithmetic.

## Thread-Safety Overhead and Contention

**Every public method acquires a single coarse-grained mutex** (`b.mutex.Lock()`) for the entire operation duration. The lock protects the byte slice across `Set`, `Clear`, `IsSet`, `MSet`, and `Count` operations.

In [`bitmap/bitmap.go`](https://github.com/go-pantheon/fabrica-util/blob/main/bitmap/bitmap.go), the `Set` method demonstrates this pattern:

```go
func (b *Bitmap) Set(index int64) {
    b.validateIndex(index)
    b.mutex.Lock()
    defer b.mutex.Unlock()
    b.bits[index/8] |= 1 << (index % 8)
}

```

This serialization becomes a bottleneck under heavy contention—such as thousands of goroutines updating a shared world map each frame. Notably, **read operations (`IsSet`) also acquire the lock**, eliminating any lock-free fast path for read-heavy workloads.

### Mitigation Strategies

- **Batch updates**: Use `MSet` to modify multiple bits under a single lock acquisition rather than calling `Set` repeatedly.
- **Sharding**: Partition large game worlds into multiple independent `Bitmap` instances (e.g., one per map region) so goroutines operate on different mutexes.
- **Lock-free alternatives**: For read-heavy scenarios, consider implementing a custom bitmap using `sync/atomic` word-level operations, as the current package does not provide lock-free variants.

## Bounds Checking Costs

All methods invoke `validateIndex(index)`, which panics on out-of-range access. While the panic itself is cheap when indexes are valid, the branch and function call overhead occurs on every operation. In tight loops processing entity updates, **pre-validate indexes** using lookup tables or spatial hashing to avoid redundant checks.

## Bulk Operations with MSet

The `MSet` method optimizes batch updates by validating indexes outside the critical section before acquiring the lock:

```go
func (b *Bitmap) MSet(indexes []int64) {
    // Validation occurs without holding the lock
    for _, index := range indexes { 
        b.validateIndex(index) 
    }
    
    b.mutex.Lock()
    defer b.mutex.Unlock()
    for _, index := range indexes {
        b.bits[index/8] |= 1 << (index % 8)
    }
}

```

**Performance tip**: When processing entity death lists or batch terrain modifications, prefer `MSet` over multiple `Set` calls to minimize lock overhead.

## Counting Set Bits Performance

The `Count` method performs a linear scan of the entire byte slice using `math/bits.OnesCount8`:

```go
func (b *Bitmap) Count() int64 {
    b.mutex.Lock()
    defer b.mutex.Unlock()
    var count int64
    for _, byteVal := range b.bits {
        count += int64(bits.OnesCount8(byteVal))
    }
    return count
}

```

Complexity is **O(N)** in the number of bytes. For large bitmaps (e.g., 10⁶+ bits), calling `Count` every frame becomes expensive. **Optimization strategies** include caching the count after modifications, using dirty flags to skip unchanged bitmaps, or counting only every N frames for approximate metrics.

## CPU Cache and Access Patterns

The contiguous byte slice provides excellent cache locality for sequential scans (as in `Count`), but random accesses (`Set`/`IsSet`) may cause cache misses if index distribution is sparse. **Sharding by spatial locality**—keeping hot regions in separate bitmap instances—improves cache line utilization and reduces false sharing between goroutines.

## Error Handling and Panics

The API uses panics for misuse (negative sizes, out-of-range indexes) rather than returning errors. While this simplifies the API, **uncontrolled panics can crash game servers** when processing untrusted client input. Wrap high-level entry points in `recover()` blocks when indexes cannot be guaranteed valid, though this adds minor overhead to the call stack.

## Summary

- **Memory efficiency**: Bitmap uses `size/8` bytes, making it suitable for dense boolean state tracking.
- **Lock granularity**: A single mutex serializes all operations; use `MSet` for batches or shard bitmaps for high concurrency.
- **Validation overhead**: Bounds checking occurs on every access; pre-validate indexes in performance-critical paths.
- **Counting cost**: `Count` scans the entire structure linearly; cache results or count intermittently for large bitmaps.
- **Cache optimization**: Contiguous storage helps sequential access but shard by region to improve random access locality.
- **Stability**: Panics on invalid input require defensive `recover()` wrappers in production game servers.

## Frequently Asked Questions

### Is fabrica-util's Bitmap lock-free?

No, the implementation uses a `sync.Mutex` to protect all operations. Every read and write acquires the same lock, making it unsuitable for high-contention scenarios without sharding or batching strategies.

### How much memory does a 10,000 cell bitmap use?

Approximately 1.25 KB (10,000 bits ÷ 8 bits per byte). The overhead of the struct itself is negligible, consisting of a mutex, slice header, and size field.

### When should I use MSet vs individual Set calls?

Use `MSet` when updating multiple bits known in advance (e.g., clearing a list of destroyed entity IDs). It validates all indexes outside the critical section and acquires the lock only once, significantly reducing contention compared to sequential `Set` calls.

### Can I use Bitmap for real-time game state without performance issues?

Yes, for moderate concurrency (dozens of goroutines) and small-to-medium state sizes (under 100,000 bits). For massively concurrent simulations or real-time strategy games with thousands of simultaneous updates, implement sharding or consider lock-free atomic bitmaps based on the bit-twiddling patterns in [`bitmap/bitmap.go`](https://github.com/go-pantheon/fabrica-util/blob/main/bitmap/bitmap.go).