Skip to content

Commit fb61b83

Browse files
committed
Reduce struct field lookup hashing
1 parent 33a868b commit fb61b83

3 files changed

Lines changed: 105 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
records.
77
- Reduced allocations when recurring decoded strings share a primary cache
88
slot.
9+
- Reduced struct decoding time by using compact field-name fingerprints before
10+
falling back to full string hashing.
911
- Rejected impossible or malformed large container sizes before allocating
1012
destination maps and slices, while reducing preflight overhead for common
1113
strings and booleans and avoiding preflight when caller-provided slice

internal/decoder/reflection.go

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,7 +1085,11 @@ func (d *ReflectionDecoder) decodeStructWithFields(
10851085
}
10861086
// The string() does not create a copy due to this compiler
10871087
// optimization: https://github.com/golang/go/issues/3512
1088-
fieldInfo, ok := fields.namedFields[string(key)]
1088+
fingerprint := fieldKeyFingerprint(key)
1089+
fieldInfo, ok := fields.fieldForFingerprint(fingerprint)
1090+
if ok && (fieldInfo == nil || fieldInfo.name != string(key)) {
1091+
fieldInfo, ok = fields.namedFields[string(key)]
1092+
}
10891093
if !ok {
10901094
offset, err = d.nextValueOffset(offset, 1)
10911095
if err != nil {
@@ -1373,8 +1377,69 @@ type fieldInfo struct {
13731377
}
13741378

13751379
type fieldsType struct {
1376-
namedFields map[string]*fieldInfo // Map from field name to field info
1377-
validationErr error
1380+
validationErr error
1381+
namedFields map[string]*fieldInfo // Map from field name to field info
1382+
fingerprintFields []fingerprintField
1383+
}
1384+
1385+
type fingerprintField struct {
1386+
field *fieldInfo
1387+
fingerprint uint64
1388+
}
1389+
1390+
func (fs *fieldsType) fieldForFingerprint(fingerprint uint64) (*fieldInfo, bool) {
1391+
mask := uint64(len(fs.fingerprintFields) - 1)
1392+
index := (fingerprint ^ (fingerprint >> 16) ^ (fingerprint >> 32)) & mask
1393+
for {
1394+
entry := fs.fingerprintFields[index]
1395+
if entry.fingerprint == 0 {
1396+
return nil, false
1397+
}
1398+
if entry.fingerprint == fingerprint {
1399+
return entry.field, true
1400+
}
1401+
index = (index + 1) & mask
1402+
}
1403+
}
1404+
1405+
func fieldKeyFingerprint(key []byte) uint64 {
1406+
// Length plus the first and last two bytes distinguish ordinary MMDB
1407+
// field names cheaply. Collisions fall back to the full string map.
1408+
n := len(key)
1409+
fingerprint := uint64(n) << 32
1410+
if n > 0 {
1411+
fingerprint |= uint64(key[0]) << 24
1412+
fingerprint |= uint64(key[n-1]) << 16
1413+
}
1414+
if n > 1 {
1415+
fingerprint |= uint64(key[1]) << 8
1416+
fingerprint |= uint64(key[n-2])
1417+
}
1418+
return fingerprint
1419+
}
1420+
1421+
func makeFingerprintFields(namedFields map[string]*fieldInfo) []fingerprintField {
1422+
tableSize := 1
1423+
for tableSize < len(namedFields)*2 {
1424+
tableSize *= 2
1425+
}
1426+
fingerprintFields := make([]fingerprintField, tableSize)
1427+
mask := uint64(tableSize - 1)
1428+
for _, field := range namedFields {
1429+
fingerprint := fieldKeyFingerprint([]byte(field.name))
1430+
index := (fingerprint ^ (fingerprint >> 16) ^ (fingerprint >> 32)) & mask
1431+
for fingerprintFields[index].fingerprint != 0 &&
1432+
fingerprintFields[index].fingerprint != fingerprint {
1433+
index = (index + 1) & mask
1434+
}
1435+
entry := &fingerprintFields[index]
1436+
if entry.fingerprint != 0 {
1437+
entry.field = nil
1438+
continue
1439+
}
1440+
*entry = fingerprintField{fingerprint: fingerprint, field: field}
1441+
}
1442+
return fingerprintFields
13781443
}
13791444

13801445
type queueEntry struct {
@@ -1648,8 +1713,9 @@ func makeStructFieldsWithStack(
16481713
}
16491714

16501715
fields := &fieldsType{
1651-
namedFields: namedFields,
1652-
validationErr: validationErr,
1716+
namedFields: namedFields,
1717+
fingerprintFields: makeFingerprintFields(namedFields),
1718+
validationErr: validationErr,
16531719
}
16541720

16551721
// Reindex all fields for optimized access

internal/decoder/reflection_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,38 @@ func TestMap(t *testing.T) {
299299
validateDecoding(t, maps)
300300
}
301301

302+
func TestDecodeStructFieldFingerprintCollisions(t *testing.T) {
303+
type resultType struct {
304+
First string `maxminddb:"abXXyz"`
305+
Second string `maxminddb:"abYYyz"`
306+
}
307+
308+
firstKey := []byte("abXXyz")
309+
secondKey := []byte("abYYyz")
310+
unknownKey := []byte("abZZyz")
311+
require.Equal(t, fieldKeyFingerprint(firstKey), fieldKeyFingerprint(secondKey))
312+
require.Equal(t, fieldKeyFingerprint(firstKey), fieldKeyFingerprint(unknownKey))
313+
314+
capacity := 1 + 6 + len(firstKey) + len(secondKey) + len(unknownKey) +
315+
len("one") + len("two") + len("ignored")
316+
data := make([]byte, 1, capacity)
317+
data[0] = 0xe3 // map with three entries
318+
data = append(data, 0x46)
319+
data = append(data, firstKey...)
320+
data = append(data, 0x43, 'o', 'n', 'e')
321+
data = append(data, 0x46)
322+
data = append(data, secondKey...)
323+
data = append(data, 0x43, 't', 'w', 'o')
324+
data = append(data, 0x46)
325+
data = append(data, unknownKey...)
326+
data = append(data, 0x47, 'i', 'g', 'n', 'o', 'r', 'e', 'd')
327+
328+
var result resultType
329+
d := NewWithoutStringCache(data)
330+
require.NoError(t, d.Decode(0, &result))
331+
require.Equal(t, resultType{First: "one", Second: "two"}, result)
332+
}
333+
302334
func TestSlice(t *testing.T) {
303335
slice := map[string]any{
304336
"0004": []any{},

0 commit comments

Comments
 (0)