package memclr
import (
"context"
"fmt"
"math"
"runtime"
"runtime/metrics"
"sync"
"testing"
"time"
)
func BenchmarkMemclr(b *testing.B) {
for exp := 4; exp <= 9; exp++ {
size := int(math.Pow10(exp))
b.Run(fmt.Sprintf("bytes=10^%d", exp), testcaseMemclr(size))
}
}
func testcaseMemclr(l int) func(b *testing.B) {
return func(b *testing.B) {
b.SetBytes(int64(l))
v := make([]byte, l)
for range b.N {
clear(v)
}
}
}
func BenchmarkSTW(b *testing.B) {
for exp := 4; exp <= 9; exp++ {
size := int(math.Pow10(exp))
b.Run(fmt.Sprintf("bytes=10^%d", exp), testcaseSTW(size))
}
}
func testcaseSTW(size int) func(*testing.B) {
const name = "/sched/pauses/stopping/other:seconds"
return func(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
clears := 0
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
v := make([]byte, size)
for ctx.Err() == nil {
clear(v)
clears++
}
}()
before := readMetric(name)
var memstats runtime.MemStats
for range b.N {
runtime.ReadMemStats(&memstats)
time.Sleep(10 * time.Microsecond) // allow others to make progress
}
after := readMetric(name)
cancel()
wg.Wait()
ns := float64(time.Second.Nanoseconds())
diff := delta(before.Float64Histogram(), after.Float64Histogram())
b.ReportMetric(worst(diff)*ns, "worst-ns")
b.ReportMetric(avg(diff)*ns, "avg-ns")
b.ReportMetric(float64(clears), "clears")
}
}
func readMetric(name string) metrics.Value {
samples := []metrics.Sample{{Name: name}}
metrics.Read(samples)
return samples[0].Value
}
func delta(a, b *metrics.Float64Histogram) *metrics.Float64Histogram {
v := &metrics.Float64Histogram{
Buckets: a.Buckets,
Counts: append([]uint64(nil), b.Counts...),
}
for i := range a.Counts {
v.Counts[i] -= a.Counts[i]
}
return v
}
func worst(h *metrics.Float64Histogram) float64 {
var v float64
for i, n := range h.Counts {
if n > 0 {
v = h.Buckets[i]
}
}
return v
}
func avg(h *metrics.Float64Histogram) float64 {
var v float64
var nn uint64
for i, n := range h.Counts {
if bv := h.Buckets[i]; !math.IsInf(bv, 0) && !math.IsNaN(bv) {
v += float64(n) * h.Buckets[i]
nn += n
}
}
return v / float64(nn)
}
The
clearandappendbuilt-ins can result in the need to zero an arbitrary amount of memory. For byte slices, the compiler appears to use a call toruntime.memclrNoHeapPointers. That function cannot be preempted, which can lead to arbitrary delays when another goroutine wants to stop the world (such as to start or end a GC cycle).Applications that use
bytes.Buffercan experience this when a call tobytes.(*Buffer).Writeleads to a call tobytes.growSlicewhich usesappend, as seen in one of the execution traces from #68399.The runtime and compiler should collaborate to allow opportunities for preemption when zeroing large amounts of memory.
CC @golang/runtime @mknyszek
Reproducer, using `clear` built-in plus `runtime.ReadMemStats` to provide STWs
Reproducer results, showing average time to stop the world is more than 1 ms (instead of less than 10 µs) when another part of the app is clearing a 100 MB byte slice
`bytes.growSlice` calling `runtime.memclrNoHeapPointers`