Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions syncmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,24 @@ func (m *SyncMap[K, V]) Delete(key K) {
m.syncMap.Delete(key)
}

// Swap replaces the value stored for key with value and returns the
// previous value, if any. The loaded result reports whether the key
// was present. If the key was not present, previous is the zero
// value of V.
func (m *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) {
v, l := m.syncMap.Swap(key, value)
if !l {
var v2 V
return v2, false
}
return v.(V), l
}

// Clear removes all entries from the map, leaving it empty.
func (m *SyncMap[K, V]) Clear() {
m.syncMap.Clear()
}

// CompareAndSwap swaps the old and new values for key if the value
// currently stored in m is equal to old. The swapped result reports
// whether the swap was performed.
Expand Down
72 changes: 68 additions & 4 deletions syncmap_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import (
)

// This file seeds the benchmark suite with coverage for the functions
// landing in issue #14 (CompareAndSwap / CompareAndDelete). A full
// benchmark set covering every public method, plus a raw-sync.Map
// overhead comparison and a committed bench.txt baseline, is owned by
// issue #15.
// landing in issues #13 (Swap, Clear) and #14 (CompareAndSwap,
// CompareAndDelete). A full benchmark set covering every public
// method, plus a raw-sync.Map overhead comparison and a committed
// bench.txt baseline, is owned by issue #15.

func BenchmarkCompareAndSwap(b *testing.B) {
b.ReportAllocs()
Expand Down Expand Up @@ -74,3 +74,67 @@ func BenchmarkCompareAndSwapParallel(b *testing.B) {
}
})
}

func BenchmarkSwap(b *testing.B) {
b.ReportAllocs()
var m syncmap.SyncMap[string, int]
m.Store("k", 0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.Swap("k", i)
}
}

func BenchmarkSwapAbsent(b *testing.B) {
b.ReportAllocs()
// int keys avoid the string-hash cost so the measured overhead
// is dominated by the !loaded guard and sync.Map's fresh-entry path.
var m syncmap.SyncMap[int, int]
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Fresh key each iteration — exercises the !loaded guard.
m.Swap(i, i)
}
}

func BenchmarkSwapParallel(b *testing.B) {
b.ReportAllocs()
var m syncmap.SyncMap[string, int]
m.Store("k", 0)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
m.Swap("k", i)
i++
}
})
}

func BenchmarkClear(b *testing.B) {
b.ReportAllocs()
var m syncmap.SyncMap[string, int]
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Cost includes one Store per iteration — Clear on an empty map
// is meaningless, so this measures the combined cost. Isolate
// via pprof if the Clear fraction needs to be teased apart.
m.Store("k", 0)
m.Clear()
}
}

func BenchmarkClearParallel(b *testing.B) {
b.ReportAllocs()
// Each goroutine owns its own map so Clear can race with Stores
// without invalidating per-iteration semantics. Concurrent Clear
// on a shared map is a legitimate pattern but the result is less
// informative (you can't reason about what any iteration "did").
b.RunParallel(func(pb *testing.PB) {
var m syncmap.SyncMap[string, int]
for pb.Next() {
m.Store("k", 0)
m.Clear()
}
})
}
105 changes: 105 additions & 0 deletions syncmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,3 +678,108 @@ func TestCompareAndSwapContention(t *testing.T) {
require.True(t, ok)
assert.Equal(t, int(winnerID.Load()), finalVal, "stored value must match the winning goroutine's id")
}

func TestSwap(t *testing.T) {
t.Parallel()

t.Run("absent_returns_zero_V_loaded_false_and_stores", func(t *testing.T) {
t.Parallel()
m := &syncmap.SyncMap[string, int]{}
previous, loaded := m.Swap("k", 42)
assert.False(t, loaded)
assert.Zero(t, previous)
v, ok := m.Load("k")
require.True(t, ok)
assert.Equal(t, 42, v)
})

t.Run("present_returns_old_loaded_true_and_overwrites", func(t *testing.T) {
t.Parallel()
m := &syncmap.SyncMap[string, int]{}
m.Store("k", 10)
previous, loaded := m.Swap("k", 20)
assert.True(t, loaded)
assert.Equal(t, 10, previous)
v, ok := m.Load("k")
require.True(t, ok)
assert.Equal(t, 20, v)
})

t.Run("zero_V_distinguished_from_absent", func(t *testing.T) {
t.Parallel()
m := &syncmap.SyncMap[string, int]{}
m.Store("k", 0)
previous, loaded := m.Swap("k", 1)
assert.True(t, loaded)
assert.Equal(t, 0, previous)
})
}

func TestClear(t *testing.T) {
t.Parallel()

t.Run("empty_map_noop", func(t *testing.T) {
t.Parallel()
m := &syncmap.SyncMap[string, int]{}
assert.NotPanics(t, func() { m.Clear() })
assert.Equal(t, 0, m.Len())
})

t.Run("populated_map_becomes_empty", func(t *testing.T) {
t.Parallel()
m := &syncmap.SyncMap[string, int]{}
m.Store("a", 1)
m.Store("b", 2)
m.Store("c", 3)
require.Equal(t, 3, m.Len())
m.Clear()
assert.Equal(t, 0, m.Len())
_, ok := m.Load("a")
assert.False(t, ok)
})

t.Run("subsequent_Store_works", func(t *testing.T) {
t.Parallel()
m := &syncmap.SyncMap[string, int]{}
m.Store("k", 1)
m.Clear()
m.Store("k", 2)
v, ok := m.Load("k")
require.True(t, ok)
assert.Equal(t, 2, v)
assert.Equal(t, 1, m.Len())
})
}

func TestSwapContention(t *testing.T) {
t.Parallel()

const goroutines = 100

m := &syncmap.SyncMap[string, int]{}
var wg sync.WaitGroup
var firstStores atomic.Int32

// Exactly one goroutine should see loaded=false (the first writer);
// the remaining 99 should all see loaded=true on a fresh key.
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
_, loaded := m.Swap("contended", id)
if !loaded {
firstStores.Add(1)
}
}(i)
}

wg.Wait()

assert.Equal(t, int32(1), firstStores.Load(), "exactly one Swap should observe loaded=false on a fresh key")

// All goroutines wrote to the same key, so Len() is 1 and the
// stored value is one of the goroutine ids.
_, ok := m.Load("contended")
assert.True(t, ok)
assert.Equal(t, 1, m.Len())
}