diff --git a/pkg/ecs/component-bit-table.go b/pkg/ecs/component-bit-table.go index 40ec418..2307b9d 100644 --- a/pkg/ecs/component-bit-table.go +++ b/pkg/ecs/component-bit-table.go @@ -19,95 +19,135 @@ import ( "math/bits" ) -//const ( -// pageSizeShift = 10 -// pageSize = 1 << pageSizeShift -// initialBookSize = 1 // Starting with a small initial book size -//) +const ( + uintShift = 7 - 64/bits.UintSize + pageSizeMask = pageSize - 1 +) func NewComponentBitTable(maxComponentsLen int) ComponentBitTable { bitsetSize := ((maxComponentsLen - 1) / bits.UintSize) + 1 return ComponentBitTable{ - bits: make([][]uint, 0, initialBookSize), - lookup: make(map[Entity]int, pageSize), - bitsetSize: bitsetSize, + bitsetsBook: make([][]uint, 0, initialBookSize), + entitiesBook: make([][]Entity, 0, initialBookSize), + lookup: NewPagedMap[Entity, int](), + bitsetSize: bitsetSize, + pageSize: bitsetSize * pageSize, } } type ComponentBitTable struct { - bits [][]uint - lookup map[Entity]int - length int - bitsetSize int + bitsetsBook [][]uint + entitiesBook [][]Entity + lookup PagedMap[Entity, int] + length int + bitsetSize int + pageSize int } func (b *ComponentBitTable) Create(entity Entity) { - bitsId, ok := b.lookup[entity] - if !ok { - b.extend() - bitsId = b.length - b.lookup[entity] = bitsId - b.length += b.bitsetSize + assert.False(b.lookup.Has(entity), "entity already exists") + + b.extend() + bitsId := b.length + b.lookup.Set(entity, bitsId) + pageId, entityId := b.getPageIDAndEntityIndex(bitsId) + b.entitiesBook[pageId][entityId] = entity + b.length++ +} + +func (b *ComponentBitTable) Delete(entity Entity) { + bitsetIndex, ok := b.lookup.Get(entity) + assert.True(ok, "entity not found") + + // Get the index of the last entity + lastIndex := b.length - 1 + + // If this is not the last entity, swap with the last one + if bitsetIndex != lastIndex { + lastPageId, lastEntityId := b.getPageIDAndEntityIndex(lastIndex) + lastBitsetId := lastEntityId * b.bitsetSize + deletePageId, deleteEntityId := b.getPageIDAndEntityIndex(bitsetIndex) + deleteBitsetId := deleteEntityId * b.bitsetSize + + // Copy bitset from last entity to the deleted entity's position + for i := 0; i < b.bitsetSize; i++ { + b.bitsetsBook[deletePageId][deleteBitsetId+i] = b.bitsetsBook[lastPageId][lastBitsetId+i] + b.bitsetsBook[lastPageId][lastBitsetId+i] = 0 + } + + // Get the last entity and update its position in lookup + lastEntity := b.entitiesBook[lastPageId][lastEntityId] + b.entitiesBook[deletePageId][deleteEntityId] = lastEntity + b.lookup.Set(lastEntity, bitsetIndex) } + + b.lookup.Delete(entity) + b.length-- } // Set sets the bit at the given index to 1. func (b *ComponentBitTable) Set(entity Entity, componentId ComponentId) { - bitsId, ok := b.lookup[entity] - if !ok { - b.extend() - bitsId = b.length - b.lookup[entity] = bitsId - b.length += b.bitsetSize - } - chunkId := bitsId >> pageSizeShift - bitsetId := bitsId % pageSize - offset := int(componentId / bits.UintSize) - b.bits[chunkId][bitsetId+offset] |= 1 << (componentId % bits.UintSize) + bitsId, ok := b.lookup.Get(entity) + assert.True(ok, "entity not found") + + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) + offset := int(componentId) >> uintShift + b.bitsetsBook[pageId][bitsetId+offset] |= 1 << (componentId % bits.UintSize) } // Unset clears the bit at the given index (sets it to 0). func (b *ComponentBitTable) Unset(entity Entity, componentId ComponentId) { - bitsId, ok := b.lookup[entity] + bitsId, ok := b.lookup.Get(entity) assert.True(ok, "entity not found") - chunkId := bitsId >> pageSizeShift - bitsetId := bitsId % pageSize - offset := int(componentId / bits.UintSize) - b.bits[chunkId][bitsetId+offset] &= ^(1 << (componentId % bits.UintSize)) + + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) + offset := int(componentId) >> uintShift + b.bitsetsBook[pageId][bitsetId+offset] &= ^(1 << (componentId % bits.UintSize)) } func (b *ComponentBitTable) Test(entity Entity, componentId ComponentId) bool { - bitsId, ok := b.lookup[entity] + bitsId, ok := b.lookup.Get(entity) if !ok { return false } - chunkId := bitsId >> pageSizeShift - bitsetId := bitsId % pageSize - offset := int(componentId / bits.UintSize) - return (b.bits[chunkId][bitsetId+offset] & (1 << (componentId % bits.UintSize))) != 0 -} - -func (b *ComponentBitTable) extend() { - lastChunkId := b.length >> pageSizeShift - if lastChunkId == len(b.bits) && b.length%pageSize == 0 { - b.bits = append(b.bits, make([]uint, b.bitsetSize*pageSize)) - } + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) + offset := int(componentId) >> uintShift + return (b.bitsetsBook[pageId][bitsetId+offset] & (1 << (componentId % bits.UintSize))) != 0 } func (b *ComponentBitTable) AllSet(entity Entity, yield func(ComponentId) bool) { - bitsId, ok := b.lookup[entity] + bitsId, ok := b.lookup.Get(entity) if !ok { return } - chunkId := bitsId >> pageSizeShift - bitsetId := bitsId % pageSize + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) for i := 0; i < b.bitsetSize; i++ { - for j := 0; j < bits.UintSize; j++ { - if (b.bits[chunkId][bitsetId+i]>>j)&1 == 1 { + set := b.bitsetsBook[pageId][bitsetId+i] + j := 0 + for set != 0 { + if set&1 == 1 { if !yield(ComponentId(i*bits.UintSize + j)) { return } } + set >>= 1 + j++ } } } + +func (b *ComponentBitTable) extend() { + lastChunkId, lastEntityId := b.getPageIDAndEntityIndex(b.length) + if lastChunkId == len(b.bitsetsBook) && lastEntityId == 0 { + b.bitsetsBook = append(b.bitsetsBook, make([]uint, b.pageSize)) + b.entitiesBook = append(b.entitiesBook, make([]Entity, pageSize)) + } +} + +func (b *ComponentBitTable) getPageIDAndBitsetIndex(index int) (int, int) { + return index >> pageSizeShift, (index & pageSizeMask) * b.bitsetSize +} + +func (b *ComponentBitTable) getPageIDAndEntityIndex(index int) (int, int) { + return index >> pageSizeShift, index & pageSizeMask +} diff --git a/pkg/ecs/component-bit-table_test.go b/pkg/ecs/component-bit-table_test.go index f1daebb..a5eae59 100644 --- a/pkg/ecs/component-bit-table_test.go +++ b/pkg/ecs/component-bit-table_test.go @@ -37,12 +37,8 @@ func TestNewComponentBitTable(t *testing.T) { t.Run(tt.name, func(t *testing.T) { table := NewComponentBitTable(tt.maxComponentsLen) - if table.bitsetSize != tt.expectedBitsetSize { - t.Errorf("Expected bitsetSize %d, got %d", tt.expectedBitsetSize, table.bitsetSize) - } - - if cap(table.bits) != initialBookSize { - t.Errorf("Expected %d preallocated chunks, got %d", initialBookSize, cap(table.bits)) + if cap(table.bitsetsBook) != initialBookSize { + t.Errorf("Expected %d preallocated chunks, got %d", initialBookSize, cap(table.bitsetsBook)) } }) } @@ -53,20 +49,20 @@ func TestComponentBitTable_Set(t *testing.T) { // Set a bit for a new entity entity1 := Entity(1) + table.Create(entity1) table.Set(entity1, ComponentId(5)) // Verify bit was set - bitsId, ok := table.lookup[entity1] + bitsId, ok := table.lookup.Get(entity1) if !ok { t.Fatalf("Entity %d not found in lookup", entity1) } - chunkId := bitsId / pageSize - bitsetId := bitsId % pageSize - offset := int(ComponentId(5) / bits.UintSize) + pageId, bitsetId := table.getPageIDAndBitsetIndex(bitsId) + offset := int(ComponentId(5) >> uintShift) mask := uint(1 << (ComponentId(5) % bits.UintSize)) - if (table.bits[chunkId][bitsetId+offset] & mask) == 0 { + if (table.bitsetsBook[pageId][bitsetId+offset] & mask) == 0 { t.Errorf("Expected bit to be set for entity %d, component %d", entity1, 5) } @@ -77,6 +73,7 @@ func TestComponentBitTable_Set(t *testing.T) { // Set bits for a different entity entity2 := Entity(2) + table.Create(entity2) table.Set(entity2, ComponentId(5)) } @@ -84,27 +81,27 @@ func TestComponentBitTable_Unset(t *testing.T) { table := NewComponentBitTable(100) entity := Entity(1) + table.Create(entity) // Set and then unset table.Set(entity, ComponentId(5)) table.Set(entity, ComponentId(10)) table.Unset(entity, ComponentId(5)) // Verify bit was unset - bitsId := table.lookup[entity] - chunkId := bitsId / pageSize - bitsetId := bitsId % pageSize - offset := int(ComponentId(5) / bits.UintSize) + bitsId, _ := table.lookup.Get(entity) + pageId, bitsetId := table.getPageIDAndBitsetIndex(bitsId) + offset := int(ComponentId(5) >> uintShift) mask := uint(1 << (ComponentId(5) % bits.UintSize)) - if (table.bits[chunkId][bitsetId+offset] & mask) != 0 { + if (table.bitsetsBook[pageId][bitsetId+offset] & mask) != 0 { t.Errorf("Expected bit to be unset for entity %d, component %d", entity, 5) } // Verify other bit is still set - offset = int(ComponentId(10) / bits.UintSize) + offset = int(ComponentId(10) >> uintShift) mask = uint(1 << (ComponentId(10) % bits.UintSize)) - if (table.bits[chunkId][bitsetId+offset] & mask) == 0 { + if (table.bitsetsBook[pageId][bitsetId+offset] & mask) == 0 { t.Errorf("Expected bit to still be set for entity %d, component %d", entity, 10) } } @@ -119,6 +116,7 @@ func TestComponentBitTable_Test(t *testing.T) { // Set up an entity with some components entity := Entity(42) + table.Create(entity) table.Set(entity, ComponentId(5)) table.Set(entity, ComponentId(64)) @@ -143,9 +141,10 @@ func TestComponentBitTable_Test(t *testing.T) { // Test components at boundaries entity2 := Entity(43) + table.Create(entity2) table.Set(entity2, ComponentId(0)) - table.Set(entity2, ComponentId(64)) // Last bit in first uint - table.Set(entity2, ComponentId(65)) // First bit in second uint + table.Set(entity2, ComponentId(64)) // First bit in second uint + table.Set(entity2, ComponentId(65)) // Second bit in second uint if !table.Test(entity2, ComponentId(0)) { t.Error("Test should return true for set component at uint boundary (0)") @@ -161,6 +160,7 @@ func TestComponentBitTable_Test(t *testing.T) { func TestComponentBitTable_AllSet(t *testing.T) { table := NewComponentBitTable(200) entity := Entity(1) + table.Create(entity) // Set several components expectedComponents := []ComponentId{5, 10, 64, 128, 199} @@ -206,34 +206,32 @@ func TestComponentBitTable_AllSet(t *testing.T) { func TestComponentBitTable_extend(t *testing.T) { // Create a table with a small chunk size for testing - table := NewComponentBitTable(20) - // Set bits to force extension - for i := 0; i < pageSize*table.bitsetSize; i++ { - table.Set(Entity(i), ComponentId(1)) - } + table := NewComponentBitTable(bits.UintSize) - if len(table.bits) > 1 { - t.Errorf("Expected table to be not extended, got %d chunks", len(table.bits)) + // Create entities to fill the first chunk + for i := 0; i < pageSize; i++ { + e := Entity(i) + table.Create(e) + table.Set(e, ComponentId(i%bits.UintSize)) } - - table.Set(Entity(pageSize*table.bitsetSize), ComponentId(1)) - - if len(table.bits) != 2 { - t.Errorf("Expected table to extend up to 2 chunks, got %d chunks", len(table.bits)) + if len(table.bitsetsBook) != 1 { + t.Errorf("Expected table to extend, got %d chunks", len(table.bitsetsBook)) } - for i := pageSize * table.bitsetSize; i < pageSize*table.bitsetSize*2; i++ { - table.Set(Entity(i), ComponentId(1)) + // Create entity to force extension + table.Create(pageSize) + if len(table.bitsetsBook) <= 1 { + t.Errorf("Expected table to be extended, got %d chunks", len(table.bitsetsBook)) } - if len(table.bits) != 2 { - t.Errorf("Expected table to extend up to 2 chunks, got %d chunks", len(table.bits)) + // Create more entities + for i := pageSize + 1; i < (pageSize*2)+1; i++ { + e := Entity(i) + table.Create(e) + table.Set(e, ComponentId(1)) } - - table.Set(Entity(pageSize*table.bitsetSize*2), ComponentId(1)) - - if len(table.bits) != 3 { - t.Errorf("Expected table to extend up to 3 chunks, got %d chunks", len(table.bits)) + if len(table.bitsetsBook) <= 2 { + t.Errorf("Expected table to extend beyond 2 chunks, got %d chunks", len(table.bitsetsBook)) } } @@ -253,19 +251,173 @@ func TestComponentBitTable_EdgeCases(t *testing.T) { // Test setting across bit boundaries entity := Entity(42) - table.Set(entity, ComponentId(0)) // Last bit in first uint - table.Set(entity, ComponentId(64)) // Last bit in first uint - table.Set(entity, ComponentId(65)) // First bit in second uint + table.Create(entity) + table.Set(entity, ComponentId(0)) // First bit in first uint + table.Set(entity, ComponentId(63)) // Last bit in first uint + table.Set(entity, ComponentId(64)) // First bit in second uint - // Verify both bits + // Verify all bits var found []ComponentId table.AllSet(entity, func(id ComponentId) bool { found = append(found, id) return true }) - if len(found) != 3 || !contains(found, ComponentId(0)) || !contains(found, ComponentId(64)) || !contains(found, ComponentId(65)) { - t.Errorf("Expected components 0, 64 and 65, got %v", found) + if len(found) != 3 || !contains(found, ComponentId(0)) || !contains(found, ComponentId(63)) || !contains(found, ComponentId(64)) { + t.Errorf("Expected components 0, 63 and 64, got %v", found) + } +} + +func TestComponentBitTable_Create(t *testing.T) { + table := NewComponentBitTable(100) + entity := Entity(42) + + // Create entity + table.Create(entity) + + // Verify entity is in lookup + _, ok := table.lookup.Get(entity) + if !ok { + t.Fatalf("Entity %d not found in lookup after Create", entity) + } + + // Check that entity ID is stored in entities book + bitsId, _ := table.lookup.Get(entity) + pageId, entityId := table.getPageIDAndEntityIndex(bitsId) + + if table.entitiesBook[pageId][entityId] != entity { + t.Errorf("Expected entity ID %d stored in entities book, got %d", + entity, table.entitiesBook[pageId][entityId]) + } +} + +func TestComponentBitTable_Delete(t *testing.T) { + table := NewComponentBitTable(100) + entity := Entity(42) + table.Create(entity) + + // Set multiple components + table.Set(entity, ComponentId(5)) + table.Set(entity, ComponentId(10)) + + // Ensure components are set + if !table.Test(entity, ComponentId(5)) || !table.Test(entity, ComponentId(10)) { + t.Fatalf("Expected components to be set for entity %d", entity) + } + + // Delete the entity + table.Delete(entity) + + // Verify entity is no longer in lookup + _, ok := table.lookup.Get(entity) + if ok { + t.Errorf("Entity %d should be removed from lookup after deletion", entity) + } + + // Test should return false for deleted entity + if table.Test(entity, ComponentId(5)) || table.Test(entity, ComponentId(10)) { + t.Errorf("Test should return false for deleted entity %d", entity) + } +} + +func TestComponentBitTable_DeleteWithSwap(t *testing.T) { + table := NewComponentBitTable(100) + + // Create two entities + entity1 := Entity(1) + entity2 := Entity(2) + table.Create(entity1) + table.Create(entity2) + + // Set different components for each + table.Set(entity1, ComponentId(5)) + table.Set(entity1, ComponentId(10)) + table.Set(entity2, ComponentId(15)) + table.Set(entity2, ComponentId(20)) + + // Get entity2's bits ID before deletion of entity1 + entity2BitsId, _ := table.lookup.Get(entity2) + + // Delete the first entity - should swap with entity2 + table.Delete(entity1) + + // Verify entity1 is gone + _, ok := table.lookup.Get(entity1) + if ok { + t.Errorf("Entity %d should be removed from lookup", entity1) + } + + // Verify entity2's data is still accessible + if !table.Test(entity2, ComponentId(15)) || !table.Test(entity2, ComponentId(20)) { + t.Errorf("Entity %d should still have its components after swap", entity2) + } + + // Entity2's lookup entry should now point to entity1's old position + newEntity2BitsId, ok := table.lookup.Get(entity2) + if !ok { + t.Fatalf("Entity %d not found after swap", entity2) + } + + // entity2 should have been moved to entity1's position + if newEntity2BitsId == entity2BitsId { + t.Errorf("Entity %d position should have changed after swap", entity2) + } + + // Ensure entity is stored in the entity book + pageId, entityId := table.getPageIDAndEntityIndex(newEntity2BitsId) + if table.entitiesBook[pageId][entityId] != entity2 { + t.Errorf("Entity ID %d not correctly stored after swap, got %d", + entity2, table.entitiesBook[pageId][entityId]) + } +} + +func TestComponentBitTable_MultipleOperations(t *testing.T) { + table := NewComponentBitTable(100) + + // Create, set, delete several entities in sequence + for i := 1; i <= 5; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i)) + table.Set(entity, ComponentId(i+10)) + } + + // Delete entity 2 and 4 + table.Delete(Entity(2)) + table.Delete(Entity(4)) + + // Verify entities 1, 3, 5 still exist with correct components + for _, id := range []Entity{1, 3, 5} { + if !table.Test(id, ComponentId(int(id))) || !table.Test(id, ComponentId(int(id)+10)) { + t.Errorf("Entity %d should still have its components", id) + } + } + + // Verify entities 2 and 4 are gone + for _, id := range []Entity{2, 4} { + if _, ok := table.lookup.Get(id); ok { + t.Errorf("Entity %d should have been deleted", id) + } + } + + // Create new entities + for i := 6; i <= 7; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i)) + } + + // Verify new entities have correct components + for i := 6; i <= 7; i++ { + entity := Entity(i) + if !table.Test(entity, ComponentId(i)) { + t.Errorf("Entity %d should have component %d set", entity, i) + } + } + + // The length should be 5 (entities 1, 3, 5, 6, 7) + if table.length != 5 { + t.Errorf("Expected length 5, got %d", table.length) } } diff --git a/pkg/ecs/component-table_bench_test.go b/pkg/ecs/component-table_bench_test.go index cfde95a..3eff852 100644 --- a/pkg/ecs/component-table_bench_test.go +++ b/pkg/ecs/component-table_bench_test.go @@ -2,37 +2,107 @@ package ecs import "testing" -const testEntitiesLen = 100_000 +const testEntitiesLen Entity = 100_000 +const maxComponentsLen = 1024 -func BenchmarkComponentBoolTable_SetAndTest(b *testing.B) { +func BenchmarkComponentBitTable_SetAndTest(b *testing.B) { // using a fixed maximum components length - table := NewComponentBoolTable(1024) + table := NewComponentBitTable(maxComponentsLen) b.ReportAllocs() for b.Loop() { - for i := 0; i < testEntitiesLen; i++ { - entity := Entity(i) - comp := ComponentId(i % 1024) - table.Set(entity, comp) - if !table.Test(entity, comp) { - b.Fatalf("ByteTable: expected entity %d to have component %d set", entity, comp) + for i := Entity(0); i < testEntitiesLen; i++ { + comp := ComponentId(i % maxComponentsLen) + table.Create(i) + table.Set(i, comp) + if !table.Test(i, comp) { + b.Fatalf("BitTable: expected entity %d to have component %d set", i, comp) } } + b.StopTimer() + for i := Entity(0); i < testEntitiesLen; i++ { + table.Delete(i) + } + b.StartTimer() } +} + +func BenchmarkComponentBitTable_SetTestDelete(b *testing.B) { + table := NewComponentBitTable(maxComponentsLen) + //for i := Entity(1); i < testEntitiesLen; i++ { + // table.Create(Entity(i)) + //} + // using a fixed maximum components length + b.ReportAllocs() + for b.Loop() { + b.StopTimer() + for i := Entity(0); i < testEntitiesLen; i++ { + table.Create(i) + } + b.StartTimer() + for i := Entity(0); i < testEntitiesLen; i++ { + comp := ComponentId(i % maxComponentsLen) + table.Set(i, comp) + if !table.Test(i, comp) { + b.Fatalf("BitTable: expected entity %d to have component %d set", i, comp) + } + table.Delete(i) + } + } } -func BenchmarkComponentBitTable_SetAndTest(b *testing.B) { +func BenchmarkComponentBitTable_Delete(b *testing.B) { // using a fixed maximum components length - table := NewComponentBitTable(1024) + table := NewComponentBitTable(maxComponentsLen) + // Setup - create and set components for all entities + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i%maxComponentsLen)) + } + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + // Delete all entities + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Delete(entity) + } + + // Recreate for next iteration + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i%maxComponentsLen)) + } + } +} + +func BenchmarkComponentBitTable_AllSet(b *testing.B) { + table := NewComponentBitTable(maxComponentsLen) + // Prepare entities with multiple components each + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Create(entity) + // Give each entity 100 components + for j := 0; j < 100; j++ { + table.Set(entity, ComponentId(j%maxComponentsLen)) + } + } + b.ReportAllocs() + b.ResetTimer() for b.Loop() { - for i := 0; i < testEntitiesLen; i++ { + for i := Entity(0); i < testEntitiesLen; i++ { entity := Entity(i) - comp := ComponentId(i % 1024) - table.Set(entity, comp) - if !table.Test(entity, comp) { - b.Fatalf("BitTable: expected entity %d to have component %d set", entity, comp) + count := 0 + table.AllSet(entity, func(id ComponentId) bool { + count++ + return true + }) + if count != 100 { + b.Fatalf("Expected 3 components, got %d", count) } } } diff --git a/pkg/ecs/entity-manager.go b/pkg/ecs/entity-manager.go index 2dbdc4f..35269fc 100644 --- a/pkg/ecs/entity-manager.go +++ b/pkg/ecs/entity-manager.go @@ -75,6 +75,7 @@ func (e *EntityManager) Create() Entity { e.mx.Lock() defer e.mx.Unlock() var newId = e.generateEntityID() + e.componentBitTable.Create(newId) e.size++ @@ -88,6 +89,7 @@ func (e *EntityManager) Delete(entity Entity) { e.components[id].Delete(entity) return true }) + e.componentBitTable.Delete(entity) e.deletedEntityIDs = append(e.deletedEntityIDs, entity) e.size-- diff --git a/pkg/ecs/paged-map.go b/pkg/ecs/paged-map.go index 60f51f0..dc19047 100644 --- a/pkg/ecs/paged-map.go +++ b/pkg/ecs/paged-map.go @@ -74,6 +74,18 @@ func (m *PagedMap[K, V]) Delete(key K) { } } +func (m *PagedMap[K, V]) Has(key K) bool { + pageID, index := m.getPageIDAndIndex(key) + if pageID >= len(m.book) { + return false + } + page := &m.book[pageID] + if page.data == nil { + return false + } + return page.data[index].ok +} + func (m *PagedMap[K, V]) getPageIDAndIndex(key K) (pageID int, index int) { return int(uint64(key) >> pageSizeShift), int(uint64(key) % pageSize) }