syncmap: add a synchronized map implementation.

This is a draft for the sync.Map API proposed in golang/go#18177.

It supports fast-path loads via an atomic variable, falling back to a
Mutex for stores. In order to keep stores amortized to O(1), loads
following a store follow the Mutex path until enough loads have
occurred to offset the cost of a deep copy.

For mostly-read loads, such as the maps in the reflect package in the
standard library, this significantly reduces cache-line contention vs.
a plain RWMutex with a map.

goos: linux
goarch: amd64
pkg: golang.org/x/sync/syncmap
BenchmarkLoadMostlyHits/*syncmap_test.DeepCopyMap               20000000                73.1 ns/op
BenchmarkLoadMostlyHits/*syncmap_test.DeepCopyMap-48            100000000               13.8 ns/op
BenchmarkLoadMostlyHits/*syncmap_test.RWMutexMap                20000000                87.7 ns/op
BenchmarkLoadMostlyHits/*syncmap_test.RWMutexMap-48             10000000               154 ns/op
BenchmarkLoadMostlyHits/*syncmap.Map                            20000000                72.1 ns/op
BenchmarkLoadMostlyHits/*syncmap.Map-48                         100000000               11.2 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.DeepCopyMap             20000000                63.2 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.DeepCopyMap-48          200000000               14.2 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.RWMutexMap              20000000                72.7 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.RWMutexMap-48           10000000               150 ns/op
BenchmarkLoadMostlyMisses/*syncmap.Map                          30000000                56.4 ns/op
BenchmarkLoadMostlyMisses/*syncmap.Map-48                       200000000                9.77 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap_test.RWMutexMap            2000000               683 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap_test.RWMutexMap-48         1000000              1394 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap.Map                        2000000               645 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap.Map-48                     1000000              1253 ns/op
BenchmarkLoadOrStoreUnique/*syncmap_test.RWMutexMap              1000000              1015 ns/op
BenchmarkLoadOrStoreUnique/*syncmap_test.RWMutexMap-48           1000000              1911 ns/op
BenchmarkLoadOrStoreUnique/*syncmap.Map                          1000000              1018 ns/op
BenchmarkLoadOrStoreUnique/*syncmap.Map-48                       1000000              1776 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.DeepCopyMap         50000000                30.2 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.DeepCopyMap-48      2000000000               1.24 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.RWMutexMap          30000000                50.1 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.RWMutexMap-48        5000000               451 ns/op
BenchmarkLoadOrStoreCollision/*syncmap.Map                      30000000                36.8 ns/op
BenchmarkLoadOrStoreCollision/*syncmap.Map-48                   2000000000               1.24 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.DeepCopyMap             10000000               213 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.DeepCopyMap-48           1000000              5012 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.RWMutexMap              20000000                68.8 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.RWMutexMap-48            5000000               429 ns/op
BenchmarkAdversarialAlloc/*syncmap.Map                           5000000               229 ns/op
BenchmarkAdversarialAlloc/*syncmap.Map-48                        2000000               600 ns/op
BenchmarkAdversarialDelete/*syncmap_test.DeepCopyMap             5000000               314 ns/op
BenchmarkAdversarialDelete/*syncmap_test.DeepCopyMap-48          2000000               726 ns/op
BenchmarkAdversarialDelete/*syncmap_test.RWMutexMap             20000000                63.2 ns/op
BenchmarkAdversarialDelete/*syncmap_test.RWMutexMap-48           5000000               469 ns/op
BenchmarkAdversarialDelete/*syncmap.Map                         10000000               203 ns/op
BenchmarkAdversarialDelete/*syncmap.Map-48                      10000000               253 ns/op

goos: linux
goarch: ppc64le
pkg: golang.org/x/sync/syncmap
BenchmarkLoadMostlyHits/*syncmap_test.DeepCopyMap                5000000               253 ns/op
BenchmarkLoadMostlyHits/*syncmap_test.DeepCopyMap-48            50000000                26.2 ns/op
BenchmarkLoadMostlyHits/*syncmap_test.RWMutexMap                 5000000               505 ns/op
BenchmarkLoadMostlyHits/*syncmap_test.RWMutexMap-48              3000000               443 ns/op
BenchmarkLoadMostlyHits/*syncmap.Map                            10000000               200 ns/op
BenchmarkLoadMostlyHits/*syncmap.Map-48                         100000000               18.1 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.DeepCopyMap             10000000               162 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.DeepCopyMap-48          100000000               23.8 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.RWMutexMap              10000000               195 ns/op
BenchmarkLoadMostlyMisses/*syncmap_test.RWMutexMap-48            3000000               531 ns/op
BenchmarkLoadMostlyMisses/*syncmap.Map                          10000000               182 ns/op
BenchmarkLoadMostlyMisses/*syncmap.Map-48                       100000000               15.8 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap_test.RWMutexMap            1000000              1664 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap_test.RWMutexMap-48         1000000              1768 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap.Map                        1000000              2128 ns/op
BenchmarkLoadOrStoreBalanced/*syncmap.Map-48                     1000000              1903 ns/op
BenchmarkLoadOrStoreUnique/*syncmap_test.RWMutexMap              1000000              2657 ns/op
BenchmarkLoadOrStoreUnique/*syncmap_test.RWMutexMap-48           1000000              2577 ns/op
BenchmarkLoadOrStoreUnique/*syncmap.Map                          1000000              1714 ns/op
BenchmarkLoadOrStoreUnique/*syncmap.Map-48                       1000000              2484 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.DeepCopyMap         10000000               130 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.DeepCopyMap-48      100000000               11.3 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.RWMutexMap           3000000               426 ns/op
BenchmarkLoadOrStoreCollision/*syncmap_test.RWMutexMap-48        2000000               930 ns/op
BenchmarkLoadOrStoreCollision/*syncmap.Map                      10000000               131 ns/op
BenchmarkLoadOrStoreCollision/*syncmap.Map-48                   300000000                4.07 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.DeepCopyMap              3000000               447 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.DeepCopyMap-48            300000              4159 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.RWMutexMap              10000000               191 ns/op
BenchmarkAdversarialAlloc/*syncmap_test.RWMutexMap-48            3000000               535 ns/op
BenchmarkAdversarialAlloc/*syncmap.Map                           2000000               525 ns/op
BenchmarkAdversarialAlloc/*syncmap.Map-48                        1000000              1000 ns/op
BenchmarkAdversarialDelete/*syncmap_test.DeepCopyMap             2000000               711 ns/op
BenchmarkAdversarialDelete/*syncmap_test.DeepCopyMap-48          2000000               900 ns/op
BenchmarkAdversarialDelete/*syncmap_test.RWMutexMap              3000000               354 ns/op
BenchmarkAdversarialDelete/*syncmap_test.RWMutexMap-48           3000000               473 ns/op
BenchmarkAdversarialDelete/*syncmap.Map                          2000000              1357 ns/op
BenchmarkAdversarialDelete/*syncmap.Map-48                       5000000               334 ns/op

Updates golang/go#18177

Change-Id: I8d561b617b1cd2ca03a8e68a5d5a28a519a0ce38
Reviewed-on: https://go-review.googlesource.com/33912
Reviewed-by: Russ Cox <rsc@golang.org>
This commit is contained in:
Bryan C. Mills 2016-12-02 19:16:14 -05:00 committed by Bryan Mills
commit 54b13b0b03
4 changed files with 649 additions and 0 deletions

192
syncmap/map.go Normal file
View file

@ -0,0 +1,192 @@
// Copyright 2016 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 syncmap provides a concurrent map implementation.
// It is a prototype for a proposed addition to the sync package
// in the standard library.
// (https://golang.org/issue/18177)
package syncmap
import (
"sync"
"sync/atomic"
)
// Map is a concurrent map with amortized-constant-time operations.
// It is safe for multiple goroutines to call a Map's methods concurrently.
//
// The zero Map is valid and empty.
//
// A Map must not be copied after first use.
type Map struct {
mu sync.Mutex
// clean is a copy of the map's contents that will never be overwritten, and
// is thus safe for concurrent lookups without additional synchronization.
//
// A nil clean map indicates that the current map contents are stored in the
// dirty field instead.
// If clean is non-nil, its contents are up-to-date.
//
// clean is always safe to load, but must only be stored with mu held.
clean atomic.Value // map[interface{}]interface{}
// dirty is a copy of the map to which all writes occur.
//
// A nil dirty map indicates that the current map contents are either empty or
// stored in the clean field.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a deep copy of the clean map, then setting the clean map to nil.
dirty map[interface{}]interface{}
// misses counts the number of Load calls for which the clean map was nil
// since the last write.
//
// Once enough Load misses have occurred to cover the cost of a copy, the
// dirty map will be promoted to clean and any subsequent writes will make
// a new copy.
misses int
}
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
clean, _ := m.clean.Load().(map[interface{}]interface{})
if clean != nil {
value, ok = clean[key]
return value, ok
}
m.mu.Lock()
if m.dirty == nil {
clean, _ := m.clean.Load().(map[interface{}]interface{})
if clean == nil {
// Completely empty — promote to clean immediately.
m.clean.Store(map[interface{}]interface{}{})
} else {
value, ok = clean[key]
}
m.mu.Unlock()
return value, ok
}
value, ok = m.dirty[key]
m.missLocked()
m.mu.Unlock()
return value, ok
}
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
m.mu.Lock()
m.dirtyLocked()
m.dirty[key] = value
m.mu.Unlock()
}
// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// The loaded result is true if the value was loaded, false if stored.
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
// Avoid locking if it's a clean hit.
clean, _ := m.clean.Load().(map[interface{}]interface{})
actual, loaded = clean[key]
if loaded {
return actual, true
}
m.mu.Lock()
if m.dirty == nil {
// Reload clean in case it changed while we were waiting on m.mu.
clean, _ := m.clean.Load().(map[interface{}]interface{})
actual, loaded = clean[key]
if loaded {
m.mu.Unlock()
return actual, true
}
} else {
actual, loaded = m.dirty[key]
if loaded {
m.missLocked()
m.mu.Unlock()
return actual, true
}
}
m.dirtyLocked()
m.dirty[key] = value
actual = value
m.mu.Unlock()
return actual, false
}
// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
m.mu.Lock()
m.dirtyLocked()
delete(m.dirty, key)
m.mu.Unlock()
}
// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
//
// Calls to other Map methods may block until Range returns.
// The function f must not call any other methods on the Map.
//
// Range does not necessarily correspond to any consistent snapshot of the Map's
// contents: no key will be visited more than once, but if the value for any key
// is stored or deleted concurrently, Range may reflect any mapping for that key
// from any point during the Range call.
func (m *Map) Range(f func(key, value interface{}) bool) {
clean, _ := m.clean.Load().(map[interface{}]interface{})
if clean == nil {
m.mu.Lock()
if m.dirty == nil {
clean, _ = m.clean.Load().(map[interface{}]interface{})
if clean == nil {
// Completely empty — add an empty map to bypass m.mu next time.
m.clean.Store(map[interface{}]interface{}{})
}
} else {
// Range is already O(N), so a call to Range amortizes an entire copy of
// the map. If it is dirty, we can promote it to clean immediately!
clean = m.dirty
m.clean.Store(clean)
m.dirty = nil
}
m.mu.Unlock()
}
for k, v := range clean {
if !f(k, v) {
break
}
}
}
func (m *Map) missLocked() {
if m.misses++; m.misses >= len(m.dirty) {
m.clean.Store(m.dirty)
m.dirty = nil
}
}
// dirtyLocked prepares the map for a subsequent write.
// It ensures that the dirty field is non-nil and clean is nil by making a deep
// copy of clean.
func (m *Map) dirtyLocked() {
m.misses = 0
if m.dirty != nil {
return
}
clean, _ := m.clean.Load().(map[interface{}]interface{})
m.dirty = make(map[interface{}]interface{}, len(clean))
for k, v := range clean {
m.dirty[k] = v
}
m.clean.Store(map[interface{}]interface{}(nil))
}