| // Copyright 2024 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package maps_test | |
| import ( | |
| "fmt" | |
| "internal/abi" | |
| "internal/runtime/maps" | |
| "math" | |
| "testing" | |
| "unsafe" | |
| ) | |
| func TestCtrlSize(t *testing.T) { | |
| cs := unsafe.Sizeof(maps.CtrlGroup(0)) | |
| if cs != abi.MapGroupSlots { | |
| t.Errorf("ctrlGroup size got %d want abi.MapGroupSlots %d", cs, abi.MapGroupSlots) | |
| } | |
| } | |
| func TestMapPut(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](8) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| if m.Used() != 31 { | |
| t.Errorf("Used() used got %d want 31", m.Used()) | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| got, ok := m.Get(typ, unsafe.Pointer(&key)) | |
| if !ok { | |
| t.Errorf("Get(%d) got ok false want true", key) | |
| } | |
| gotElem := *(*uint64)(got) | |
| if gotElem != elem { | |
| t.Errorf("Get(%d) got elem %d want %d", key, gotElem, elem) | |
| } | |
| } | |
| } | |
| func TestSmallMapGrow(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](8) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 8; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| if m.TableCount() != 0 { | |
| t.Errorf("TableCount() got %d want 0", m.TableCount()) | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 10) | |
| for i := 0; i < 8; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| if m.TableCount() != 0 { | |
| t.Errorf("TableCount() got %d want 0", m.TableCount()) | |
| } | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| if m.TableCount() != 1 { | |
| t.Errorf("TableCount() got %d want 1", m.TableCount()) | |
| } | |
| } | |
| // Grow enough to cause a table split. | |
| func TestMapSplit(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](0) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 2*maps.MaxTableCapacity; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| if m.Used() != 2*maps.MaxTableCapacity { | |
| t.Errorf("Used() used got %d want 31", m.Used()) | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 2*maps.MaxTableCapacity; i++ { | |
| key += 1 | |
| elem += 1 | |
| got, ok := m.Get(typ, unsafe.Pointer(&key)) | |
| if !ok { | |
| t.Errorf("Get(%d) got ok false want true", key) | |
| } | |
| gotElem := *(*uint64)(got) | |
| if gotElem != elem { | |
| t.Errorf("Get(%d) got elem %d want %d", key, gotElem, elem) | |
| } | |
| } | |
| } | |
| func TestMapDelete(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](32) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| m.Delete(typ, unsafe.Pointer(&key)) | |
| } | |
| if m.Used() != 0 { | |
| t.Errorf("Used() used got %d want 0", m.Used()) | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| _, ok := m.Get(typ, unsafe.Pointer(&key)) | |
| if ok { | |
| t.Errorf("Get(%d) got ok true want false", key) | |
| } | |
| } | |
| } | |
| func TestTableClear(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](32) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| m.Clear(typ) | |
| if m.Used() != 0 { | |
| t.Errorf("Clear() used got %d want 0", m.Used()) | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| _, ok := m.Get(typ, unsafe.Pointer(&key)) | |
| if ok { | |
| t.Errorf("Get(%d) got ok true want false", key) | |
| } | |
| } | |
| } | |
| // +0.0 and -0.0 compare equal, but we must still must update the key slot when | |
| // overwriting. | |
| func TestTableKeyUpdate(t *testing.T) { | |
| m, typ := maps.NewTestMap[float64, uint64](8) | |
| zero := float64(0.0) | |
| negZero := math.Copysign(zero, -1.0) | |
| elem := uint64(0) | |
| m.Put(typ, unsafe.Pointer(&zero), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %f: %v\n", zero, m) | |
| } | |
| elem = 1 | |
| m.Put(typ, unsafe.Pointer(&negZero), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %f: %v\n", negZero, m) | |
| } | |
| if m.Used() != 1 { | |
| t.Errorf("Used() used got %d want 1", m.Used()) | |
| } | |
| it := new(maps.Iter) | |
| it.Init(typ, m) | |
| it.Next() | |
| keyPtr, elemPtr := it.Key(), it.Elem() | |
| if keyPtr == nil { | |
| t.Fatal("it.Key() got nil want key") | |
| } | |
| key := *(*float64)(keyPtr) | |
| elem = *(*uint64)(elemPtr) | |
| if math.Copysign(1.0, key) > 0 { | |
| t.Errorf("map key %f has positive sign", key) | |
| } | |
| if elem != 1 { | |
| t.Errorf("map elem got %d want 1", elem) | |
| } | |
| } | |
| // Put should reuse a deleted slot rather than consuming an empty slot. | |
| func TestTablePutDelete(t *testing.T) { | |
| // Put will reuse the first deleted slot it encounters. | |
| // | |
| // This is awkward to test because Delete will only install ctrlDeleted | |
| // if the group is full, otherwise it goes straight to empty. | |
| // | |
| // So first we must add to the table continuously until we happen to | |
| // fill a group. | |
| // Avoid small maps, they have no tables. | |
| m, typ := maps.NewTestMap[uint32, uint32](16) | |
| key := uint32(0) | |
| elem := uint32(256 + 0) | |
| for { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| // Normally a Put that fills a group would fill it with the | |
| // inserted key, so why search the whole map for a potentially | |
| // different key in a full group? | |
| // | |
| // Put may grow/split a table. Initial construction of the new | |
| // table(s) could result in a full group consisting of | |
| // arbitrary keys. | |
| fullKeyPtr := m.KeyFromFullGroup(typ) | |
| if fullKeyPtr != nil { | |
| // Found a full group. | |
| key = *(*uint32)(fullKeyPtr) | |
| elem = 256 + key | |
| break | |
| } | |
| } | |
| // Key is in a full group. Deleting it will result in a ctrlDeleted | |
| // slot. | |
| m.Delete(typ, unsafe.Pointer(&key)) | |
| // Re-insert key. This should reuse the deleted slot rather than | |
| // consuming space. | |
| tabWant := m.TableFor(typ, unsafe.Pointer(&key)) | |
| growthLeftWant := tabWant.GrowthLeft() | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| tabGot := m.TableFor(typ, unsafe.Pointer(&key)) | |
| growthLeftGot := tabGot.GrowthLeft() | |
| if tabGot != tabWant { | |
| // There shouldn't be a grow, as replacing a deleted slot | |
| // doesn't require more space. | |
| t.Errorf("Put(%d) grew table got %v want %v map %v", key, tabGot, tabWant, m) | |
| } | |
| if growthLeftGot != growthLeftWant { | |
| t.Errorf("GrowthLeft got %d want %d: map %v tab %v", growthLeftGot, growthLeftWant, m, tabGot) | |
| } | |
| } | |
| func TestTableIteration(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](8) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| got := make(map[uint32]uint64) | |
| it := new(maps.Iter) | |
| it.Init(typ, m) | |
| for { | |
| it.Next() | |
| keyPtr, elemPtr := it.Key(), it.Elem() | |
| if keyPtr == nil { | |
| break | |
| } | |
| key := *(*uint32)(keyPtr) | |
| elem := *(*uint64)(elemPtr) | |
| got[key] = elem | |
| } | |
| if len(got) != 31 { | |
| t.Errorf("Iteration got %d entries, want 31: %+v", len(got), got) | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| gotElem, ok := got[key] | |
| if !ok { | |
| t.Errorf("Iteration missing key %d", key) | |
| continue | |
| } | |
| if gotElem != elem { | |
| t.Errorf("Iteration key %d got elem %d want %d", key, gotElem, elem) | |
| } | |
| } | |
| } | |
| // Deleted keys shouldn't be visible in iteration. | |
| func TestTableIterationDelete(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](8) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| got := make(map[uint32]uint64) | |
| first := true | |
| deletedKey := uint32(1) | |
| it := new(maps.Iter) | |
| it.Init(typ, m) | |
| for { | |
| it.Next() | |
| keyPtr, elemPtr := it.Key(), it.Elem() | |
| if keyPtr == nil { | |
| break | |
| } | |
| key := *(*uint32)(keyPtr) | |
| elem := *(*uint64)(elemPtr) | |
| got[key] = elem | |
| if first { | |
| first = false | |
| // If the key we intended to delete was the one we just | |
| // saw, pick another to delete. | |
| if key == deletedKey { | |
| deletedKey++ | |
| } | |
| m.Delete(typ, unsafe.Pointer(&deletedKey)) | |
| } | |
| } | |
| if len(got) != 30 { | |
| t.Errorf("Iteration got %d entries, want 30: %+v", len(got), got) | |
| } | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| wantOK := true | |
| if key == deletedKey { | |
| wantOK = false | |
| } | |
| gotElem, gotOK := got[key] | |
| if gotOK != wantOK { | |
| t.Errorf("Iteration key %d got ok %v want ok %v", key, gotOK, wantOK) | |
| continue | |
| } | |
| if wantOK && gotElem != elem { | |
| t.Errorf("Iteration key %d got elem %d want %d", key, gotElem, elem) | |
| } | |
| } | |
| } | |
| // Deleted keys shouldn't be visible in iteration even after a grow. | |
| func TestTableIterationGrowDelete(t *testing.T) { | |
| m, typ := maps.NewTestMap[uint32, uint64](8) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| got := make(map[uint32]uint64) | |
| first := true | |
| deletedKey := uint32(1) | |
| it := new(maps.Iter) | |
| it.Init(typ, m) | |
| for { | |
| it.Next() | |
| keyPtr, elemPtr := it.Key(), it.Elem() | |
| if keyPtr == nil { | |
| break | |
| } | |
| key := *(*uint32)(keyPtr) | |
| elem := *(*uint64)(elemPtr) | |
| got[key] = elem | |
| if first { | |
| first = false | |
| // If the key we intended to delete was the one we just | |
| // saw, pick another to delete. | |
| if key == deletedKey { | |
| deletedKey++ | |
| } | |
| // Double the number of elements to force a grow. | |
| key := uint32(32) | |
| elem := uint64(256 + 32) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| // Then delete from the grown map. | |
| m.Delete(typ, unsafe.Pointer(&deletedKey)) | |
| } | |
| } | |
| // Don't check length: the number of new elements we'll see is | |
| // unspecified. | |
| // Check values only of the original pre-iteration entries. | |
| key = uint32(0) | |
| elem = uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| wantOK := true | |
| if key == deletedKey { | |
| wantOK = false | |
| } | |
| gotElem, gotOK := got[key] | |
| if gotOK != wantOK { | |
| t.Errorf("Iteration key %d got ok %v want ok %v", key, gotOK, wantOK) | |
| continue | |
| } | |
| if wantOK && gotElem != elem { | |
| t.Errorf("Iteration key %d got elem %d want %d", key, gotElem, elem) | |
| } | |
| } | |
| } | |
| func testTableIterationGrowDuplicate(t *testing.T, grow int) { | |
| m, typ := maps.NewTestMap[uint32, uint64](8) | |
| key := uint32(0) | |
| elem := uint64(256 + 0) | |
| for i := 0; i < 31; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| got := make(map[uint32]uint64) | |
| it := new(maps.Iter) | |
| it.Init(typ, m) | |
| for i := 0; ; i++ { | |
| it.Next() | |
| keyPtr, elemPtr := it.Key(), it.Elem() | |
| if keyPtr == nil { | |
| break | |
| } | |
| key := *(*uint32)(keyPtr) | |
| elem := *(*uint64)(elemPtr) | |
| if elem != 256+uint64(key) { | |
| t.Errorf("iteration got key %d elem %d want elem %d", key, elem, 256+uint64(key)) | |
| } | |
| if _, ok := got[key]; ok { | |
| t.Errorf("iteration got key %d more than once", key) | |
| } | |
| got[key] = elem | |
| // Grow halfway through iteration. | |
| if i == 16 { | |
| key := uint32(32) | |
| elem := uint64(256 + 32) | |
| for i := 0; i < grow; i++ { | |
| key += 1 | |
| elem += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| } | |
| } | |
| } | |
| // Don't check length: the number of new elements we'll see is | |
| // unspecified. | |
| } | |
| // Grow should not allow duplicate keys to appear. | |
| func TestTableIterationGrowDuplicate(t *testing.T) { | |
| // Small grow, only enough to cause table grow. | |
| t.Run("grow", func(t *testing.T) { testTableIterationGrowDuplicate(t, 32) }) | |
| // Large grow, to cause table split. | |
| t.Run("split", func(t *testing.T) { testTableIterationGrowDuplicate(t, 2*maps.MaxTableCapacity) }) | |
| } | |
| func TestAlignUpPow2(t *testing.T) { | |
| tests := []struct { | |
| in uint64 | |
| want uint64 | |
| overflow bool | |
| }{ | |
| { | |
| in: 0, | |
| want: 0, | |
| }, | |
| { | |
| in: 3, | |
| want: 4, | |
| }, | |
| { | |
| in: 4, | |
| want: 4, | |
| }, | |
| { | |
| in: 1 << 63, | |
| want: 1 << 63, | |
| }, | |
| { | |
| in: (1 << 63) - 1, | |
| want: 1 << 63, | |
| }, | |
| { | |
| in: (1 << 63) + 1, | |
| overflow: true, | |
| }, | |
| } | |
| for _, tc := range tests { | |
| got, overflow := maps.AlignUpPow2(tc.in) | |
| if got != tc.want { | |
| t.Errorf("alignUpPow2(%d) got %d, want %d", tc.in, got, tc.want) | |
| } | |
| if overflow != tc.overflow { | |
| t.Errorf("alignUpPow2(%d) got overflow %v, want %v", tc.in, overflow, tc.overflow) | |
| } | |
| } | |
| } | |
| // Verify that a map with zero-size slot is safe to use. | |
| func TestMapZeroSizeSlot(t *testing.T) { | |
| m, typ := maps.NewTestMap[struct{}, struct{}](16) | |
| key := struct{}{} | |
| elem := struct{}{} | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| got, ok := m.Get(typ, unsafe.Pointer(&key)) | |
| if !ok { | |
| t.Errorf("Get(%d) got ok false want true", key) | |
| } | |
| gotElem := *(*struct{})(got) | |
| if gotElem != elem { | |
| t.Errorf("Get(%d) got elem %d want %d", key, gotElem, elem) | |
| } | |
| tab := m.TableFor(typ, unsafe.Pointer(&key)) | |
| start := tab.GroupsStart() | |
| length := tab.GroupsLength() | |
| end := unsafe.Pointer(uintptr(start) + length*typ.GroupSize - 1) // inclusive to ensure we have a valid pointer | |
| if uintptr(got) < uintptr(start) || uintptr(got) > uintptr(end) { | |
| t.Errorf("elem address outside groups allocation; got %p want [%p, %p]", got, start, end) | |
| } | |
| } | |
| func TestMapIndirect(t *testing.T) { | |
| type big [abi.MapMaxKeyBytes + abi.MapMaxElemBytes]byte | |
| m, typ := maps.NewTestMap[big, big](8) | |
| key := big{} | |
| elem := big{} | |
| elem[0] = 128 | |
| for i := 0; i < 31; i++ { | |
| key[0] += 1 | |
| elem[0] += 1 | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %v: %v\n", key, m) | |
| } | |
| } | |
| if m.Used() != 31 { | |
| t.Errorf("Used() used got %d want 31", m.Used()) | |
| } | |
| key = big{} | |
| elem = big{} | |
| elem[0] = 128 | |
| for i := 0; i < 31; i++ { | |
| key[0] += 1 | |
| elem[0] += 1 | |
| got, ok := m.Get(typ, unsafe.Pointer(&key)) | |
| if !ok { | |
| t.Errorf("Get(%v) got ok false want true", key) | |
| } | |
| gotElem := *(*big)(got) | |
| if gotElem != elem { | |
| t.Errorf("Get(%v) got elem %v want %v", key, gotElem, elem) | |
| } | |
| } | |
| } | |
| // Delete should clear element. See https://go.dev/issue/25936. | |
| func TestMapDeleteClear(t *testing.T) { | |
| m, typ := maps.NewTestMap[int64, int64](8) | |
| key := int64(0) | |
| elem := int64(128) | |
| m.Put(typ, unsafe.Pointer(&key), unsafe.Pointer(&elem)) | |
| if maps.DebugLog { | |
| fmt.Printf("After put %d: %v\n", key, m) | |
| } | |
| got, ok := m.Get(typ, unsafe.Pointer(&key)) | |
| if !ok { | |
| t.Errorf("Get(%d) got ok false want true", key) | |
| } | |
| gotElem := *(*int64)(got) | |
| if gotElem != elem { | |
| t.Errorf("Get(%d) got elem %d want %d", key, gotElem, elem) | |
| } | |
| m.Delete(typ, unsafe.Pointer(&key)) | |
| gotElem = *(*int64)(got) | |
| if gotElem != 0 { | |
| t.Errorf("Delete(%d) failed to clear element. got %d want 0", key, gotElem) | |
| } | |
| } | |
| var alwaysFalse bool | |
| var escapeSink any | |
| func escape[T any](x T) T { | |
| if alwaysFalse { | |
| escapeSink = x | |
| } | |
| return x | |
| } | |
| const ( | |
| belowMax = abi.MapGroupSlots * 3 / 2 // 1.5 * group max = 2 groups @ 75% | |
| atMax = (2 * abi.MapGroupSlots * maps.MaxAvgGroupLoad) / abi.MapGroupSlots // 2 groups at 7/8 full. | |
| ) | |
| func TestTableGroupCount(t *testing.T) { | |
| // Test that maps of different sizes have the right number of | |
| // tables/groups. | |
| type mapCount struct { | |
| tables int | |
| groups uint64 | |
| } | |
| type mapCase struct { | |
| initialLit mapCount | |
| initialHint mapCount | |
| after mapCount | |
| } | |
| var testCases = []struct { | |
| n int // n is the number of map elements | |
| escape mapCase // expected values for escaping map | |
| }{ | |
| { | |
| n: -(1 << 30), | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{0, 0}, | |
| after: mapCount{0, 0}, | |
| }, | |
| }, | |
| { | |
| n: -1, | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{0, 0}, | |
| after: mapCount{0, 0}, | |
| }, | |
| }, | |
| { | |
| n: 0, | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{0, 0}, | |
| after: mapCount{0, 0}, | |
| }, | |
| }, | |
| { | |
| n: 1, | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{0, 0}, | |
| after: mapCount{0, 1}, | |
| }, | |
| }, | |
| { | |
| n: abi.MapGroupSlots, | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{0, 0}, | |
| after: mapCount{0, 1}, | |
| }, | |
| }, | |
| { | |
| n: abi.MapGroupSlots + 1, | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{1, 2}, | |
| after: mapCount{1, 2}, | |
| }, | |
| }, | |
| { | |
| n: belowMax, // 1.5 group max = 2 groups @ 75% | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{1, 2}, | |
| after: mapCount{1, 2}, | |
| }, | |
| }, | |
| { | |
| n: atMax, // 2 groups at max | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{1, 2}, | |
| after: mapCount{1, 2}, | |
| }, | |
| }, | |
| { | |
| n: atMax + 1, // 2 groups at max + 1 -> grow to 4 groups | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{1, 4}, | |
| after: mapCount{1, 4}, | |
| }, | |
| }, | |
| { | |
| n: 2 * belowMax, // 3 * group max = 4 groups @75% | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{1, 4}, | |
| after: mapCount{1, 4}, | |
| }, | |
| }, | |
| { | |
| n: 2*atMax + 1, // 4 groups at max + 1 -> grow to 8 groups | |
| escape: mapCase{ | |
| initialLit: mapCount{0, 0}, | |
| initialHint: mapCount{1, 8}, | |
| after: mapCount{1, 8}, | |
| }, | |
| }, | |
| } | |
| testMap := func(t *testing.T, m map[int]int, n int, initial, after mapCount) { | |
| mm := *(**maps.Map)(unsafe.Pointer(&m)) | |
| gotTab := mm.TableCount() | |
| if gotTab != initial.tables { | |
| t.Errorf("initial TableCount got %d want %d", gotTab, initial.tables) | |
| } | |
| gotGroup := mm.GroupCount() | |
| if gotGroup != initial.groups { | |
| t.Errorf("initial GroupCount got %d want %d", gotGroup, initial.groups) | |
| } | |
| for i := 0; i < n; i++ { | |
| m[i] = i | |
| } | |
| gotTab = mm.TableCount() | |
| if gotTab != after.tables { | |
| t.Errorf("after TableCount got %d want %d", gotTab, after.tables) | |
| } | |
| gotGroup = mm.GroupCount() | |
| if gotGroup != after.groups { | |
| t.Errorf("after GroupCount got %d want %d", gotGroup, after.groups) | |
| } | |
| } | |
| t.Run("mapliteral", func(t *testing.T) { | |
| for _, tc := range testCases { | |
| t.Run(fmt.Sprintf("n=%d", tc.n), func(t *testing.T) { | |
| t.Run("escape", func(t *testing.T) { | |
| m := escape(map[int]int{}) | |
| testMap(t, m, tc.n, tc.escape.initialLit, tc.escape.after) | |
| }) | |
| }) | |
| } | |
| }) | |
| t.Run("nohint", func(t *testing.T) { | |
| for _, tc := range testCases { | |
| t.Run(fmt.Sprintf("n=%d", tc.n), func(t *testing.T) { | |
| t.Run("escape", func(t *testing.T) { | |
| m := escape(make(map[int]int)) | |
| testMap(t, m, tc.n, tc.escape.initialLit, tc.escape.after) | |
| }) | |
| }) | |
| } | |
| }) | |
| t.Run("makemap", func(t *testing.T) { | |
| for _, tc := range testCases { | |
| t.Run(fmt.Sprintf("n=%d", tc.n), func(t *testing.T) { | |
| t.Run("escape", func(t *testing.T) { | |
| m := escape(make(map[int]int, tc.n)) | |
| testMap(t, m, tc.n, tc.escape.initialHint, tc.escape.after) | |
| }) | |
| }) | |
| } | |
| }) | |
| t.Run("makemap64", func(t *testing.T) { | |
| for _, tc := range testCases { | |
| t.Run(fmt.Sprintf("n=%d", tc.n), func(t *testing.T) { | |
| t.Run("escape", func(t *testing.T) { | |
| m := escape(make(map[int]int, int64(tc.n))) | |
| testMap(t, m, tc.n, tc.escape.initialHint, tc.escape.after) | |
| }) | |
| }) | |
| } | |
| }) | |
| } | |
| func TestTombstoneGrow(t *testing.T) { | |
| tableSizes := []int{16, 32, 64, 128, 256} | |
| for _, tableSize := range tableSizes { | |
| for _, load := range []string{"low", "mid", "high"} { | |
| capacity := tableSize * 7 / 8 | |
| var initialElems int | |
| switch load { | |
| case "low": | |
| initialElems = capacity / 8 | |
| case "mid": | |
| initialElems = capacity / 2 | |
| case "high": | |
| initialElems = capacity | |
| } | |
| t.Run(fmt.Sprintf("tableSize=%d/elems=%d/load=%0.3f", tableSize, initialElems, float64(initialElems)/float64(tableSize)), func(t *testing.T) { | |
| allocs := testing.AllocsPerRun(1, func() { | |
| // Fill the map with elements. | |
| m := make(map[int]int, capacity) | |
| for i := range initialElems { | |
| m[i] = i | |
| } | |
| // This is the heart of our test. | |
| // Loop over the map repeatedly, deleting a key then adding a not-yet-seen key | |
| // while keeping the map at a ~constant number of elements (+/-1). | |
| nextKey := initialElems | |
| for range 100000 { | |
| for k := range m { | |
| delete(m, k) | |
| break | |
| } | |
| m[nextKey] = nextKey | |
| nextKey++ | |
| if len(m) != initialElems { | |
| t.Fatal("len(m) should remain constant") | |
| } | |
| } | |
| }) | |
| // The make has 4 allocs (map, directory, table, groups). | |
| // Each growth has 2 allocs (table, groups). | |
| // We allow two growths if we start full, 1 otherwise. | |
| // Fail (somewhat arbitrarily) if there are more than that. | |
| allowed := float64(4 + 1*2) | |
| if initialElems == capacity { | |
| allowed += 2 | |
| } | |
| if allocs > allowed { | |
| t.Fatalf("got %v allocations, allowed %v", allocs, allowed) | |
| } | |
| }) | |
| } | |
| } | |
| } |