Skip to content

Commit

Permalink
Speed up MapOf lookups (#134)
Browse files Browse the repository at this point in the history
Introduces meta memory and SWAR-based lookups similar to C++'s
`absl::flat_hash_map` hash table (https://abseil.io/docs/cpp/guides/container).

Also, reduces `MapOf`'s memory overhead: each bucket now holds up
to 5 entries instead of 3.
  • Loading branch information
puzpuzpuz authored Jul 6, 2024
1 parent 6092349 commit 9d42f97
Show file tree
Hide file tree
Showing 8 changed files with 424 additions and 148 deletions.
2 changes: 2 additions & 0 deletions BENCHMARKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ $ benchstat bench.txt | tee benchstat.txt

The below sections contain some of the results. Refer to [this gist](https://gist.github.com/puzpuzpuz/e62e38e06feadecfdc823c0f941ece0b) for the complete output.

Please note that `MapOf` got a number of optimizations since v2.3.1, so the current result is likely to be different.

### Counter vs. atomic int64

```
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ m.Store("foo", "bar")
v, ok := m.Load("foo")
```

One important difference with `Map` is that `MapOf` supports arbitrary `comparable` key types:
Apart from CLHT, `MapOf` borrows ideas from Java's `j.u.c.ConcurrentHashMap` (immutable K/V pair structs instead of atomic snapshots) and C++'s `absl::flat_hash_map` (meta memory and SWAR-based lookups). It also has more dense memory layout when compared with `Map`. Long story short, `MapOf` should be preferred over `Map` when possible.

An important difference with `Map` is that `MapOf` supports arbitrary `comparable` key types:

```go
type Point struct {
Expand Down
28 changes: 23 additions & 5 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package xsync

const (
EntriesPerMapBucket = entriesPerMapBucket
MapLoadFactor = mapLoadFactor
DefaultMinMapTableLen = defaultMinMapTableLen
DefaultMinMapTableCap = defaultMinMapTableLen * entriesPerMapBucket
MaxMapCounterLen = maxMapCounterLen
EntriesPerMapBucket = entriesPerMapBucket
EntriesPerMapOfBucket = entriesPerMapOfBucket
MapLoadFactor = mapLoadFactor
DefaultMinMapTableLen = defaultMinMapTableLen
DefaultMinMapTableCap = defaultMinMapTableLen * entriesPerMapBucket
DefaultMinMapOfTableCap = defaultMinMapTableLen * entriesPerMapOfBucket
MaxMapCounterLen = maxMapCounterLen
)

type (
Expand Down Expand Up @@ -45,6 +47,22 @@ func Fastrand() uint32 {
return runtime_fastrand()
}

func Broadcast(b uint8) uint64 {
return broadcast(b)
}

func FirstMarkedByteIndex(w uint64) int {
return firstMarkedByteIndex(w)
}

func MarkZeroBytes(w uint64) uint64 {
return markZeroBytes(w)
}

func SetByte(w uint64, b uint8, idx int) uint64 {
return setByte(w, b, idx)
}

func NextPowOf2(v uint32) uint32 {
return nextPowOf2(v)
}
Expand Down
4 changes: 2 additions & 2 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const (
)

const (
// number of entries per bucket; 3 entries lead to size of 64B
// number of Map entries per bucket; 3 entries lead to size of 64B
// (one cache line) on 64-bit machines
entriesPerMapBucket = 3
// threshold fraction of table occupation to start a table shrinking
Expand Down Expand Up @@ -477,7 +477,7 @@ func (m *Map) doCompute(
unlockBucket(&rootb.topHashMutex)
return newValue, false
}
// Create and append the bucket.
// Create and append a bucket.
newb := new(bucketPadded)
newb.keys[0] = unsafe.Pointer(&key)
newb.values[0] = unsafe.Pointer(&newValue)
Expand Down
Loading

0 comments on commit 9d42f97

Please sign in to comment.