runtime/internal/maps: only conditionally clear groups when sparse

We only want to do the work of clearing slots
if they are full. But we also don't want to do too
much work to figure out whether a slot is full or not,
especially if clearing a slot is cheap.
 1) We decide group-by-group instead of slot-by-slot.
    If any slot in a group is full, we zero the whole group.
 2) If groups are unlikely to be empty, don't bother
    testing for it.
 3) If groups are 50%/50% likely to be empty, also don't
    bother testing, as it confuses the branch predictor. See #75097.
 4) But if a group is really large, do the test anyway, as
    clearing is expensive.

Fixes #75097

Change-Id: I9191865dd3e0fe887751cffe6082ac27d8d8439c
Reviewed-on: https://go-review.googlesource.com/c/go/+/697876
Reviewed-by: Keith Randall <khr@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Keith Randall <khr@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Youlin Feng <fengyoulin@live.com>
This commit is contained in:
Keith Randall 2025-08-20 17:33:14 -07:00
parent 8098b99547
commit de5d7eccb9

View file

@ -604,12 +604,37 @@ func (t *table) Clear(typ *abi.MapType) {
if t.used == 0 && t.growthLeft == mgl { // no current entries and no tombstones
return
}
for i := uint64(0); i <= t.groups.lengthMask; i++ {
g := t.groups.group(typ, i)
if g.ctrls().anyFull() {
typedmemclr(typ.Group, g.data)
// We only want to do the work of clearing slots
// if they are full. But we also don't want to do too
// much work to figure out whether a slot is full or not,
// especially if clearing a slot is cheap.
// 1) We decide group-by-group instead of slot-by-slot.
// If any slot in a group is full, we zero the whole group.
// 2) If groups are unlikely to be empty, don't bother
// testing for it.
// 3) If groups are 50%/50% likely to be empty, also don't
// bother testing, as it confuses the branch predictor. See #75097.
// 4) But if a group is really large, do the test anyway, as
// clearing is expensive.
fullTest := uint64(t.used)*4 <= t.groups.lengthMask // less than ~0.25 entries per group -> >3/4 empty groups
if typ.SlotSize > 32 {
// For large slots, it is always worth doing the test first.
fullTest = true
}
if fullTest {
for i := uint64(0); i <= t.groups.lengthMask; i++ {
g := t.groups.group(typ, i)
if g.ctrls().anyFull() {
typedmemclr(typ.Group, g.data)
}
g.ctrls().setEmpty()
}
} else {
for i := uint64(0); i <= t.groups.lengthMask; i++ {
g := t.groups.group(typ, i)
typedmemclr(typ.Group, g.data)
g.ctrls().setEmpty()
}
g.ctrls().setEmpty()
}
t.used = 0
t.growthLeft = mgl