github.com/fufuok/utils@v1.0.10/xsync/util_hash_mapof_test.go (about)

     1  //go:build go1.18
     2  // +build go1.18
     3  
     4  package xsync_test
     5  
     6  //lint:file-ignore U1000 unused fields are necessary to access the hasher
     7  //lint:file-ignore SA4000 hash code comparisons use identical expressions
     8  
     9  import (
    10  	"fmt"
    11  	"testing"
    12  	"unsafe"
    13  
    14  	. "github.com/fufuok/utils/xsync"
    15  )
    16  
    17  func TestMakeHashFunc(t *testing.T) {
    18  	type User struct {
    19  		Name string
    20  		City string
    21  	}
    22  
    23  	seed := MakeSeed()
    24  
    25  	hashString := MakeHasher[string]()
    26  	hashUser := MakeHasher[User]()
    27  
    28  	hashUserMap := makeMapHasher[User]()
    29  
    30  	// Not that much to test TBH.
    31  
    32  	// check that hash is not always the same
    33  	for i := 0; ; i++ {
    34  		if hashString("foo", seed) != hashString("bar", seed) {
    35  			break
    36  		}
    37  		if i >= 100 {
    38  			t.Error("hashString is always the same")
    39  			break
    40  		}
    41  
    42  		seed = MakeSeed() // try with a new seed
    43  	}
    44  
    45  	if hashString("foo", seed) != hashString("foo", seed) {
    46  		t.Error("hashString is not deterministic")
    47  	}
    48  
    49  	if hashUser(User{Name: "Ivan", City: "Sofia"}, seed) != hashUser(User{Name: "Ivan", City: "Sofia"}, seed) {
    50  		t.Error("hashUser is not deterministic")
    51  	}
    52  
    53  	// just for fun, compare with native hash function
    54  	if hashUser(User{Name: "Ivan", City: "Sofia"}, seed) != hashUserMap(User{Name: "Ivan", City: "Sofia"}, seed) {
    55  		t.Error("hashUser and hashUserNative return different values")
    56  	}
    57  }
    58  
    59  func makeMapHasher[T comparable]() func(T, uint64) uint64 {
    60  	hasher := makeMapHasherInternal(make(map[T]struct{}))
    61  
    62  	is64Bit := unsafe.Sizeof(uintptr(0)) == 8
    63  
    64  	if is64Bit {
    65  		return func(value T, seed uint64) uint64 {
    66  			seed64 := *(*uint64)(unsafe.Pointer(&seed))
    67  			return uint64(hasher(runtime_noescape(unsafe.Pointer(&value)), uintptr(seed64)))
    68  		}
    69  	} else {
    70  		return func(value T, seed uint64) uint64 {
    71  			seed64 := *(*uint64)(unsafe.Pointer(&seed))
    72  			lo := hasher(runtime_noescape(unsafe.Pointer(&value)), uintptr(seed64))
    73  			hi := hasher(runtime_noescape(unsafe.Pointer(&value)), uintptr(seed64>>32))
    74  			return uint64(hi)<<32 | uint64(lo)
    75  		}
    76  	}
    77  }
    78  
    79  //go:noescape
    80  //go:linkname runtime_noescape runtime.noescape
    81  func runtime_noescape(p unsafe.Pointer) unsafe.Pointer
    82  
    83  type nativeHasher func(unsafe.Pointer, uintptr) uintptr
    84  
    85  func makeMapHasherInternal(mapValue any) nativeHasher {
    86  	// go/src/runtime/type.go
    87  	type tflag uint8
    88  	type nameOff int32
    89  	type typeOff int32
    90  
    91  	// go/src/runtime/type.go
    92  	type _type struct {
    93  		size       uintptr
    94  		ptrdata    uintptr
    95  		hash       uint32
    96  		tflag      tflag
    97  		align      uint8
    98  		fieldAlign uint8
    99  		kind       uint8
   100  		equal      func(unsafe.Pointer, unsafe.Pointer) bool
   101  		gcdata     *byte
   102  		str        nameOff
   103  		ptrToThis  typeOff
   104  	}
   105  
   106  	// go/src/runtime/type.go
   107  	type maptype struct {
   108  		typ    _type
   109  		key    *_type
   110  		elem   *_type
   111  		bucket *_type
   112  		// function for hashing keys (ptr to key, seed) -> hash
   113  		hasher     nativeHasher
   114  		keysize    uint8
   115  		elemsize   uint8
   116  		bucketsize uint16
   117  		flags      uint32
   118  	}
   119  
   120  	type mapiface struct {
   121  		typ *maptype
   122  		val uintptr
   123  	}
   124  
   125  	i := (*mapiface)(unsafe.Pointer(&mapValue))
   126  	return i.typ.hasher
   127  }
   128  
   129  func BenchmarkMakeHashFunc(b *testing.B) {
   130  	type Point struct {
   131  		X, Y, Z int
   132  	}
   133  
   134  	type User struct {
   135  		ID        int
   136  		FirstName string
   137  		LastName  string
   138  		IsActive  bool
   139  		City      string
   140  	}
   141  
   142  	type PadInside struct {
   143  		A int
   144  		B byte
   145  		C int
   146  	}
   147  
   148  	type PadTrailing struct {
   149  		A int
   150  		B byte
   151  	}
   152  
   153  	doBenchmarkMakeHashFunc(b, int64(116))
   154  	doBenchmarkMakeHashFunc(b, int32(116))
   155  	doBenchmarkMakeHashFunc(b, 3.14)
   156  	doBenchmarkMakeHashFunc(b, "test key test key test key test key test key test key test key test key test key ")
   157  	doBenchmarkMakeHashFunc(b, Point{1, 2, 3})
   158  	doBenchmarkMakeHashFunc(b, User{ID: 1, FirstName: "Ivan", LastName: "Ivanov", IsActive: true, City: "Sofia"})
   159  	doBenchmarkMakeHashFunc(b, PadInside{})
   160  	doBenchmarkMakeHashFunc(b, PadTrailing{})
   161  	doBenchmarkMakeHashFunc(b, [1024]byte{})
   162  	doBenchmarkMakeHashFunc(b, [128]Point{})
   163  	doBenchmarkMakeHashFunc(b, [128]User{})
   164  	doBenchmarkMakeHashFunc(b, [128]PadInside{})
   165  	doBenchmarkMakeHashFunc(b, [128]PadTrailing{})
   166  }
   167  
   168  func doBenchmarkMakeHashFunc[T comparable](b *testing.B, val T) {
   169  	hash := MakeHasher[T]()
   170  	hashNativeMap := makeMapHasher[T]()
   171  	seed := MakeSeed()
   172  
   173  	b.Run(fmt.Sprintf("%T normal", val), func(b *testing.B) {
   174  		b.ReportAllocs()
   175  		for i := 0; i < b.N; i++ {
   176  			_ = hash(val, seed)
   177  		}
   178  	})
   179  
   180  	b.Run(fmt.Sprintf("%T map native", val), func(b *testing.B) {
   181  		b.ReportAllocs()
   182  		for i := 0; i < b.N; i++ {
   183  			_ = hashNativeMap(val, seed)
   184  		}
   185  	})
   186  }