From c3b502018e15a1ef54a4e2ced12a1c392746a5bc Mon Sep 17 00:00:00 2001 From: Andrei Pechkurov <37772591+puzpuzpuz@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:44:06 +0300 Subject: [PATCH] Speed up built-in string hash function (#106) --- .github/workflows/build-32-bit.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/coverage.yml | 2 +- export_test.go | 6 ++++++ util.go | 16 ++++++++++++---- util_test.go | 25 ++++++++++++++++++++++++- 6 files changed, 45 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-32-bit.yml b/.github/workflows/build-32-bit.yml index 3b99130..bc24367 100644 --- a/.github/workflows/build-32-bit.yml +++ b/.github/workflows/build-32-bit.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [1.20.x] + go-version: [1.21.x] name: Build with Go ${{ matrix.go-version }} 32-bit steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f8ad21e..c5ad8bb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [1.20.x] + go-version: [1.21.x] name: Build with Go ${{ matrix.go-version }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index da65143..30f8f55 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [1.20.x] + go-version: [1.21.x] name: Build with Go ${{ matrix.go-version }} steps: - uses: actions/checkout@v3 diff --git a/export_test.go b/export_test.go index 7d82818..16d373c 100644 --- a/export_test.go +++ b/export_test.go @@ -1,5 +1,7 @@ package xsync +import "hash/maphash" + const ( EntriesPerMapBucket = entriesPerMapBucket MapLoadFactor = mapLoadFactor @@ -48,6 +50,10 @@ func DisableAssertions() { assertionsEnabled = false } +func HashString(seed maphash.Seed, s string) uint64 { + return hashString(seed, s) +} + func Fastrand() uint32 { return fastrand() } diff --git a/util.go b/util.go index 81a596a..d8a64f8 100644 --- a/util.go +++ b/util.go @@ -2,7 +2,9 @@ package xsync import ( "hash/maphash" + "reflect" "runtime" + "unsafe" _ "unsafe" ) @@ -44,12 +46,18 @@ func parallelism() uint32 { // hashString calculates a hash of s with the given seed. func hashString(seed maphash.Seed, s string) uint64 { - var h maphash.Hash - h.SetSeed(seed) - h.WriteString(s) - return h.Sum64() + seed64 := *(*uint64)(unsafe.Pointer(&seed)) + if s == "" { + return seed64 + } + strh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + return uint64(memhash(unsafe.Pointer(strh.Data), uintptr(seed64), uintptr(strh.Len))) } +//go:noescape +//go:linkname memhash runtime.memhash +func memhash(p unsafe.Pointer, h, s uintptr) uintptr + //go:noescape //go:linkname fastrand runtime.fastrand func fastrand() uint32 diff --git a/util_test.go b/util_test.go index c4bbf22..32b3a3d 100644 --- a/util_test.go +++ b/util_test.go @@ -1,6 +1,7 @@ package xsync_test import ( + "hash/maphash" "math/rand" "testing" @@ -25,7 +26,7 @@ func TestNextPowOf2(t *testing.T) { // This test is here to catch potential problems // with fastrand-related changes. func TestFastrand(t *testing.T) { - count := 10000 + count := 100 set := make(map[uint32]struct{}, count) for i := 0; i < count; i++ { @@ -51,3 +52,25 @@ func BenchmarkRand(b *testing.B) { } // about 12 ns/op on x86-64 } + +func BenchmarkMapHashString(b *testing.B) { + fn := func(seed maphash.Seed, s string) uint64 { + var h maphash.Hash + h.SetSeed(seed) + h.WriteString(s) + return h.Sum64() + } + seed := maphash.MakeSeed() + for i := 0; i < b.N; i++ { + _ = fn(seed, benchmarkKeyPrefix) + } + // about 13ns/op on x86-64 +} + +func BenchmarkHashString(b *testing.B) { + seed := maphash.MakeSeed() + for i := 0; i < b.N; i++ { + _ = HashString(seed, benchmarkKeyPrefix) + } + // about 4ns/op on x86-64 +}