Skip to content

Commit b71d432

Browse files
mknyszekgopherbot
authored andcommitted
runtime: fix race in BenchmarkSetType* benchmarks
Currently the BenchmarkSetType* benchmarks are racy: they call heapBitsSetType on an allocation that might be in a span in-use for allocation on another P. Because heap bits are bits but are written byte-wise non-atomically (because a P assumes it has total ownership of a span's bits), two threads can race writing the same heap bitmap byte creating incorrect metadata. Fix this by forcing every value we're writing heap bits for into a large object. Large object spans will never be written to concurrently unless they're freed first. Also, while we're here, refactor the benchmarks a bit. Use generics to eliminate the reflect nastiness in gc_test.go, and pass b.ResetTimer down into the test to get slightly more accurate results. Fixes #60050. Change-Id: Ib7d6249b321963367c8c8ca88385386c8ae9af1c Reviewed-on: https://go-review.googlesource.com/c/go/+/497215 Reviewed-by: Austin Clements <[email protected]> Run-TryBot: Michael Knyszek <[email protected]> Auto-Submit: Michael Knyszek <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent e73e5d8 commit b71d432

File tree

2 files changed

+144
-55
lines changed

2 files changed

+144
-55
lines changed

src/runtime/export_test.go

Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -230,34 +230,126 @@ func SetEnvs(e []string) { envs = e }
230230

231231
// For benchmarking.
232232

233-
func BenchSetType(n int, x any) {
233+
// blockWrapper is a wrapper type that ensures a T is placed within a
234+
// large object. This is necessary for safely benchmarking things
235+
// that manipulate the heap bitmap, like heapBitsSetType.
236+
//
237+
// More specifically, allocating threads assume they're the sole writers
238+
// to their span's heap bits, which allows those writes to be non-atomic.
239+
// The heap bitmap is written byte-wise, so if one tried to call heapBitsSetType
240+
// on an existing object in a small object span, we might corrupt that
241+
// span's bitmap with a concurrent byte write to the heap bitmap. Large
242+
// object spans contain exactly one object, so we can be sure no other P
243+
// is going to be allocating from it concurrently, hence this wrapper type
244+
// which ensures we have a T in a large object span.
245+
type blockWrapper[T any] struct {
246+
value T
247+
_ [_MaxSmallSize]byte // Ensure we're a large object.
248+
}
249+
250+
func BenchSetType[T any](n int, resetTimer func()) {
251+
x := new(blockWrapper[T])
252+
234253
// Escape x to ensure it is allocated on the heap, as we are
235254
// working on the heap bits here.
236255
Escape(x)
237-
e := *efaceOf(&x)
256+
257+
// Grab the type.
258+
var i any = *new(T)
259+
e := *efaceOf(&i)
260+
t := e._type
261+
262+
// Benchmark setting the type bits for just the internal T of the block.
263+
benchSetType(n, resetTimer, 1, unsafe.Pointer(&x.value), t)
264+
}
265+
266+
const maxArrayBlockWrapperLen = 32
267+
268+
// arrayBlockWrapper is like blockWrapper, but the interior value is intended
269+
// to be used as a backing store for a slice.
270+
type arrayBlockWrapper[T any] struct {
271+
value [maxArrayBlockWrapperLen]T
272+
_ [_MaxSmallSize]byte // Ensure we're a large object.
273+
}
274+
275+
// arrayLargeBlockWrapper is like arrayBlockWrapper, but the interior array
276+
// accommodates many more elements.
277+
type arrayLargeBlockWrapper[T any] struct {
278+
value [1024]T
279+
_ [_MaxSmallSize]byte // Ensure we're a large object.
280+
}
281+
282+
func BenchSetTypeSlice[T any](n int, resetTimer func(), len int) {
283+
// We have two separate cases here because we want to avoid
284+
// tests on big types but relatively small slices to avoid generating
285+
// an allocation that's really big. This will likely force a GC which will
286+
// skew the test results.
287+
var y unsafe.Pointer
288+
if len <= maxArrayBlockWrapperLen {
289+
x := new(arrayBlockWrapper[T])
290+
// Escape x to ensure it is allocated on the heap, as we are
291+
// working on the heap bits here.
292+
Escape(x)
293+
y = unsafe.Pointer(&x.value[0])
294+
} else {
295+
x := new(arrayLargeBlockWrapper[T])
296+
Escape(x)
297+
y = unsafe.Pointer(&x.value[0])
298+
}
299+
300+
// Grab the type.
301+
var i any = *new(T)
302+
e := *efaceOf(&i)
238303
t := e._type
239-
var size uintptr
240-
var p unsafe.Pointer
241-
switch t.Kind_ & kindMask {
242-
case kindPtr:
243-
t = (*ptrtype)(unsafe.Pointer(t)).Elem
244-
size = t.Size_
245-
p = e.data
246-
case kindSlice:
247-
slice := *(*struct {
248-
ptr unsafe.Pointer
249-
len, cap uintptr
250-
})(e.data)
251-
t = (*slicetype)(unsafe.Pointer(t)).Elem
252-
size = t.Size_ * slice.len
253-
p = slice.ptr
304+
305+
// Benchmark setting the type for a slice created from the array
306+
// of T within the arrayBlock.
307+
benchSetType(n, resetTimer, len, y, t)
308+
}
309+
310+
// benchSetType is the implementation of the BenchSetType* functions.
311+
// x must be len consecutive Ts allocated within a large object span (to
312+
// avoid a race on the heap bitmap).
313+
//
314+
// Note: this function cannot be generic. It would get its type from one of
315+
// its callers (BenchSetType or BenchSetTypeSlice) whose type parameters are
316+
// set by a call in the runtime_test package. That means this function and its
317+
// callers will get instantiated in the package that provides the type argument,
318+
// i.e. runtime_test. However, we call a function on the system stack. In race
319+
// mode the runtime package is usually left uninstrumented because e.g. g0 has
320+
// no valid racectx, but if we're instantiated in the runtime_test package,
321+
// we might accidentally cause runtime code to be incorrectly instrumented.
322+
func benchSetType(n int, resetTimer func(), len int, x unsafe.Pointer, t *_type) {
323+
// Compute the input sizes.
324+
size := t.Size() * uintptr(len)
325+
326+
// Validate this function's invariant.
327+
s := spanOfHeap(uintptr(x))
328+
if s == nil {
329+
panic("no heap span for input")
330+
}
331+
if s.spanclass.sizeclass() != 0 {
332+
panic("span is not a large object span")
254333
}
334+
335+
// Round up the size to the size class to make the benchmark a little more
336+
// realistic. However, validate it, to make sure this is safe.
255337
allocSize := roundupsize(size)
338+
if s.npages*pageSize < allocSize {
339+
panic("backing span not large enough for benchmark")
340+
}
341+
342+
// Benchmark heapBitsSetType by calling it in a loop. This is safe because
343+
// x is in a large object span.
344+
resetTimer()
256345
systemstack(func() {
257346
for i := 0; i < n; i++ {
258-
heapBitsSetType(uintptr(p), allocSize, size, t)
347+
heapBitsSetType(uintptr(x), allocSize, size, t)
259348
}
260349
})
350+
351+
// Make sure x doesn't get freed, since we're taking a uintptr.
352+
KeepAlive(x)
261353
}
262354

263355
const PtrSize = goarch.PtrSize

src/runtime/gc_test.go

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -308,35 +308,35 @@ func TestGCTestPointerClass(t *testing.T) {
308308
}
309309

310310
func BenchmarkSetTypePtr(b *testing.B) {
311-
benchSetType(b, new(*byte))
311+
benchSetType[*byte](b)
312312
}
313313

314314
func BenchmarkSetTypePtr8(b *testing.B) {
315-
benchSetType(b, new([8]*byte))
315+
benchSetType[[8]*byte](b)
316316
}
317317

318318
func BenchmarkSetTypePtr16(b *testing.B) {
319-
benchSetType(b, new([16]*byte))
319+
benchSetType[[16]*byte](b)
320320
}
321321

322322
func BenchmarkSetTypePtr32(b *testing.B) {
323-
benchSetType(b, new([32]*byte))
323+
benchSetType[[32]*byte](b)
324324
}
325325

326326
func BenchmarkSetTypePtr64(b *testing.B) {
327-
benchSetType(b, new([64]*byte))
327+
benchSetType[[64]*byte](b)
328328
}
329329

330330
func BenchmarkSetTypePtr126(b *testing.B) {
331-
benchSetType(b, new([126]*byte))
331+
benchSetType[[126]*byte](b)
332332
}
333333

334334
func BenchmarkSetTypePtr128(b *testing.B) {
335-
benchSetType(b, new([128]*byte))
335+
benchSetType[[128]*byte](b)
336336
}
337337

338338
func BenchmarkSetTypePtrSlice(b *testing.B) {
339-
benchSetType(b, make([]*byte, 1<<10))
339+
benchSetTypeSlice[*byte](b, 1<<10)
340340
}
341341

342342
type Node1 struct {
@@ -345,11 +345,11 @@ type Node1 struct {
345345
}
346346

347347
func BenchmarkSetTypeNode1(b *testing.B) {
348-
benchSetType(b, new(Node1))
348+
benchSetType[Node1](b)
349349
}
350350

351351
func BenchmarkSetTypeNode1Slice(b *testing.B) {
352-
benchSetType(b, make([]Node1, 32))
352+
benchSetTypeSlice[Node1](b, 32)
353353
}
354354

355355
type Node8 struct {
@@ -358,11 +358,11 @@ type Node8 struct {
358358
}
359359

360360
func BenchmarkSetTypeNode8(b *testing.B) {
361-
benchSetType(b, new(Node8))
361+
benchSetType[Node8](b)
362362
}
363363

364364
func BenchmarkSetTypeNode8Slice(b *testing.B) {
365-
benchSetType(b, make([]Node8, 32))
365+
benchSetTypeSlice[Node8](b, 32)
366366
}
367367

368368
type Node64 struct {
@@ -371,11 +371,11 @@ type Node64 struct {
371371
}
372372

373373
func BenchmarkSetTypeNode64(b *testing.B) {
374-
benchSetType(b, new(Node64))
374+
benchSetType[Node64](b)
375375
}
376376

377377
func BenchmarkSetTypeNode64Slice(b *testing.B) {
378-
benchSetType(b, make([]Node64, 32))
378+
benchSetTypeSlice[Node64](b, 32)
379379
}
380380

381381
type Node64Dead struct {
@@ -384,11 +384,11 @@ type Node64Dead struct {
384384
}
385385

386386
func BenchmarkSetTypeNode64Dead(b *testing.B) {
387-
benchSetType(b, new(Node64Dead))
387+
benchSetType[Node64Dead](b)
388388
}
389389

390390
func BenchmarkSetTypeNode64DeadSlice(b *testing.B) {
391-
benchSetType(b, make([]Node64Dead, 32))
391+
benchSetTypeSlice[Node64Dead](b, 32)
392392
}
393393

394394
type Node124 struct {
@@ -397,11 +397,11 @@ type Node124 struct {
397397
}
398398

399399
func BenchmarkSetTypeNode124(b *testing.B) {
400-
benchSetType(b, new(Node124))
400+
benchSetType[Node124](b)
401401
}
402402

403403
func BenchmarkSetTypeNode124Slice(b *testing.B) {
404-
benchSetType(b, make([]Node124, 32))
404+
benchSetTypeSlice[Node124](b, 32)
405405
}
406406

407407
type Node126 struct {
@@ -410,11 +410,11 @@ type Node126 struct {
410410
}
411411

412412
func BenchmarkSetTypeNode126(b *testing.B) {
413-
benchSetType(b, new(Node126))
413+
benchSetType[Node126](b)
414414
}
415415

416416
func BenchmarkSetTypeNode126Slice(b *testing.B) {
417-
benchSetType(b, make([]Node126, 32))
417+
benchSetTypeSlice[Node126](b, 32)
418418
}
419419

420420
type Node128 struct {
@@ -423,11 +423,11 @@ type Node128 struct {
423423
}
424424

425425
func BenchmarkSetTypeNode128(b *testing.B) {
426-
benchSetType(b, new(Node128))
426+
benchSetType[Node128](b)
427427
}
428428

429429
func BenchmarkSetTypeNode128Slice(b *testing.B) {
430-
benchSetType(b, make([]Node128, 32))
430+
benchSetTypeSlice[Node128](b, 32)
431431
}
432432

433433
type Node130 struct {
@@ -436,11 +436,11 @@ type Node130 struct {
436436
}
437437

438438
func BenchmarkSetTypeNode130(b *testing.B) {
439-
benchSetType(b, new(Node130))
439+
benchSetType[Node130](b)
440440
}
441441

442442
func BenchmarkSetTypeNode130Slice(b *testing.B) {
443-
benchSetType(b, make([]Node130, 32))
443+
benchSetTypeSlice[Node130](b, 32)
444444
}
445445

446446
type Node1024 struct {
@@ -449,24 +449,21 @@ type Node1024 struct {
449449
}
450450

451451
func BenchmarkSetTypeNode1024(b *testing.B) {
452-
benchSetType(b, new(Node1024))
452+
benchSetType[Node1024](b)
453453
}
454454

455455
func BenchmarkSetTypeNode1024Slice(b *testing.B) {
456-
benchSetType(b, make([]Node1024, 32))
456+
benchSetTypeSlice[Node1024](b, 32)
457457
}
458458

459-
func benchSetType(b *testing.B, x any) {
460-
v := reflect.ValueOf(x)
461-
t := v.Type()
462-
switch t.Kind() {
463-
case reflect.Pointer:
464-
b.SetBytes(int64(t.Elem().Size()))
465-
case reflect.Slice:
466-
b.SetBytes(int64(t.Elem().Size()) * int64(v.Len()))
467-
}
468-
b.ResetTimer()
469-
runtime.BenchSetType(b.N, x)
459+
func benchSetType[T any](b *testing.B) {
460+
b.SetBytes(int64(unsafe.Sizeof(*new(T))))
461+
runtime.BenchSetType[T](b.N, b.ResetTimer)
462+
}
463+
464+
func benchSetTypeSlice[T any](b *testing.B, len int) {
465+
b.SetBytes(int64(unsafe.Sizeof(*new(T)) * uintptr(len)))
466+
runtime.BenchSetTypeSlice[T](b.N, b.ResetTimer, len)
470467
}
471468

472469
func BenchmarkAllocation(b *testing.B) {

0 commit comments

Comments
 (0)