Skip to content

Commit d19fedd

Browse files
committed
runtime: move checkmarks to a separate bitmap
Currently, the GC stores the object marks for checkmarks mode in the heap bitmap using a rather complex encoding: for one word objects, the checkmark is stored in the pointer/scalar bit since one word objects must be pointers; for larger objects, the checkmark is stored in what would be the scan/dead bit for the second word of the object. This encoding made more sense when the runtime used the first scan/dead bit as the regular mark bit, but we moved away from that long ago. This encoding and overloading of the heap bitmap bits causes a great deal of complexity in many parts of the allocator and garbage collector and leads to some subtle bugs like #15903. This CL moves the checkmarks mark bits into their own per-arena bitmap and reclaims the second scan/dead bit as a regular scan/dead bit. I tested this by enabling doubleCheck mode in heapBitsSetType and running in both regular and GODEBUG=gccheckmark=1 mode. Fixes #15903. No performance degradation. (Very slight improvement on a few benchmarks, but it's probably just noise.) name old time/op new time/op delta BiogoIgor 16.6s ± 1% 16.4s ± 1% -0.94% (p=0.000 n=25+24) BiogoKrishna 19.2s ± 3% 19.2s ± 3% ~ (p=0.638 n=23+25) BleveIndexBatch100 6.12s ± 5% 6.17s ± 4% ~ (p=0.170 n=25+25) CompileTemplate 206ms ± 1% 205ms ± 1% -0.43% (p=0.005 n=24+24) CompileUnicode 82.2ms ± 2% 81.5ms ± 2% -0.95% (p=0.001 n=22+22) CompileGoTypes 755ms ± 3% 754ms ± 4% ~ (p=0.715 n=25+25) CompileCompiler 3.73s ± 1% 3.73s ± 1% ~ (p=0.445 n=25+24) CompileSSA 8.67s ± 1% 8.66s ± 1% ~ (p=0.836 n=24+22) CompileFlate 134ms ± 2% 133ms ± 1% -0.66% (p=0.001 n=24+23) CompileGoParser 164ms ± 1% 163ms ± 1% -0.85% (p=0.000 n=24+24) CompileReflect 466ms ± 5% 466ms ± 3% ~ (p=0.863 n=25+25) CompileTar 182ms ± 1% 182ms ± 1% -0.31% (p=0.048 n=24+24) CompileXML 249ms ± 1% 248ms ± 1% -0.32% (p=0.031 n=21+25) CompileStdCmd 10.3s ± 1% 10.3s ± 1% ~ (p=0.459 n=23+23) FoglemanFauxGLRenderRotateBoat 8.66s ± 1% 8.62s ± 1% -0.47% (p=0.000 n=23+24) FoglemanPathTraceRenderGopherIter1 20.3s ± 3% 20.2s ± 2% ~ (p=0.893 n=25+25) GopherLuaKNucleotide 29.7s ± 1% 29.8s ± 2% ~ (p=0.421 n=24+25) MarkdownRenderXHTML 246ms ± 1% 247ms ± 1% ~ (p=0.558 n=25+24) Tile38WithinCircle100kmRequest 779µs ± 4% 779µs ± 3% ~ (p=0.954 n=25+25) Tile38IntersectsCircle100kmRequest 1.02ms ± 3% 1.01ms ± 4% ~ (p=0.658 n=25+25) Tile38KNearestLimit100Request 984µs ± 4% 986µs ± 4% ~ (p=0.627 n=24+25) [Geo mean] 552ms 551ms -0.19% https://perf.golang.org/search?q=upload:20200723.6 Change-Id: Ic703f26a83fb034941dc6f4788fc997d56890dec Reviewed-on: https://go-review.googlesource.com/c/go/+/244539 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Michael Knyszek <[email protected]> Reviewed-by: Martin Möhrmann <[email protected]>
1 parent 7148abc commit d19fedd

File tree

9 files changed

+148
-231
lines changed

9 files changed

+148
-231
lines changed

src/reflect/all_test.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6467,12 +6467,9 @@ func verifyGCBitsSlice(t *testing.T, typ Type, cap int, bits []byte) {
64676467
// Repeat the bitmap for the slice size, trimming scalars in
64686468
// the last element.
64696469
bits = rep(cap, bits)
6470-
for len(bits) > 2 && bits[len(bits)-1] == 0 {
6470+
for len(bits) > 0 && bits[len(bits)-1] == 0 {
64716471
bits = bits[:len(bits)-1]
64726472
}
6473-
if len(bits) == 2 && bits[0] == 0 && bits[1] == 0 {
6474-
bits = bits[:0]
6475-
}
64766473
if !bytes.Equal(heapBits, bits) {
64776474
t.Errorf("heapBits incorrect for make(%v, 0, %v)\nhave %v\nwant %v", typ, cap, heapBits, bits)
64786475
}

src/runtime/cgocall.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,7 @@ func cgoCheckUnknownPointer(p unsafe.Pointer, msg string) (base, i uintptr) {
605605
hbits := heapBitsForAddr(base)
606606
n := span.elemsize
607607
for i = uintptr(0); i < n; i += sys.PtrSize {
608-
if i != 1*sys.PtrSize && !hbits.morePointers() {
608+
if !hbits.morePointers() {
609609
// No more possible pointers.
610610
break
611611
}

src/runtime/gcinfo_test.go

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func TestGCInfo(t *testing.T) {
7777
}
7878

7979
for i := 0; i < 10; i++ {
80-
verifyGCInfo(t, "heap Ptr", escape(new(Ptr)), trimDead(padDead(infoPtr)))
80+
verifyGCInfo(t, "heap Ptr", escape(new(Ptr)), trimDead(infoPtr))
8181
verifyGCInfo(t, "heap PtrSlice", escape(&make([]*byte, 10)[0]), trimDead(infoPtr10))
8282
verifyGCInfo(t, "heap ScalarPtr", escape(new(ScalarPtr)), trimDead(infoScalarPtr))
8383
verifyGCInfo(t, "heap ScalarPtrSlice", escape(&make([]ScalarPtr, 4)[0]), trimDead(infoScalarPtr4))
@@ -97,25 +97,10 @@ func verifyGCInfo(t *testing.T, name string, p interface{}, mask0 []byte) {
9797
}
9898
}
9999

100-
func padDead(mask []byte) []byte {
101-
// Because the dead bit isn't encoded in the second word,
102-
// and because on 32-bit systems a one-word allocation
103-
// uses a two-word block, the pointer info for a one-word
104-
// object needs to be expanded to include an extra scalar
105-
// on 32-bit systems to match the heap bitmap.
106-
if runtime.PtrSize == 4 && len(mask) == 1 {
107-
return []byte{mask[0], 0}
108-
}
109-
return mask
110-
}
111-
112100
func trimDead(mask []byte) []byte {
113-
for len(mask) > 2 && mask[len(mask)-1] == typeScalar {
101+
for len(mask) > 0 && mask[len(mask)-1] == typeScalar {
114102
mask = mask[:len(mask)-1]
115103
}
116-
if len(mask) == 2 && mask[0] == typeScalar && mask[1] == typeScalar {
117-
mask = mask[:0]
118-
}
119104
return mask
120105
}
121106

src/runtime/heapdump.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ func makeheapobjbv(p uintptr, size uintptr) bitvector {
713713
i := uintptr(0)
714714
hbits := heapBitsForAddr(p)
715715
for ; i < nptr; i++ {
716-
if i != 1 && !hbits.morePointers() {
716+
if !hbits.morePointers() {
717717
break // end of object
718718
}
719719
if hbits.isPointer() {

src/runtime/mbitmap.go

Lines changed: 34 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
//
77
// Stack, data, and bss bitmaps
88
//
9-
// Stack frames and global variables in the data and bss sections are described
10-
// by 1-bit bitmaps in which 0 means uninteresting and 1 means live pointer
11-
// to be visited during GC. The bits in each byte are consumed starting with
12-
// the low bit: 1<<0, 1<<1, and so on.
9+
// Stack frames and global variables in the data and bss sections are
10+
// described by bitmaps with 1 bit per pointer-sized word. A "1" bit
11+
// means the word is a live pointer to be visited by the GC (referred to
12+
// as "pointer"). A "0" bit means the word should be ignored by GC
13+
// (referred to as "scalar", though it could be a dead pointer value).
1314
//
1415
// Heap bitmap
1516
//
@@ -20,57 +21,28 @@
2021
// through start+3*ptrSize, ha.bitmap[1] holds the entries for
2122
// start+4*ptrSize through start+7*ptrSize, and so on.
2223
//
23-
// In each 2-bit entry, the lower bit holds the same information as in the 1-bit
24-
// bitmaps: 0 means uninteresting and 1 means live pointer to be visited during GC.
25-
// The meaning of the high bit depends on the position of the word being described
26-
// in its allocated object. In all words *except* the second word, the
27-
// high bit indicates that the object is still being described. In
28-
// these words, if a bit pair with a high bit 0 is encountered, the
29-
// low bit can also be assumed to be 0, and the object description is
30-
// over. This 00 is called the ``dead'' encoding: it signals that the
31-
// rest of the words in the object are uninteresting to the garbage
32-
// collector.
33-
//
34-
// In the second word, the high bit is the GC ``checkmarked'' bit (see below).
24+
// In each 2-bit entry, the lower bit is a pointer/scalar bit, just
25+
// like in the stack/data bitmaps described above. The upper bit
26+
// indicates scan/dead: a "1" value ("scan") indicates that there may
27+
// be pointers in later words of the allocation, and a "0" value
28+
// ("dead") indicates there are no more pointers in the allocation. If
29+
// the upper bit is 0, the lower bit must also be 0, and this
30+
// indicates scanning can ignore the rest of the allocation.
3531
//
3632
// The 2-bit entries are split when written into the byte, so that the top half
3733
// of the byte contains 4 high bits and the bottom half contains 4 low (pointer)
3834
// bits.
3935
// This form allows a copy from the 1-bit to the 4-bit form to keep the
4036
// pointer bits contiguous, instead of having to space them out.
4137
//
42-
// The code makes use of the fact that the zero value for a heap bitmap
43-
// has no live pointer bit set and is (depending on position), not used,
44-
// not checkmarked, and is the dead encoding.
45-
// These properties must be preserved when modifying the encoding.
38+
// The code makes use of the fact that the zero value for a heap
39+
// bitmap means scalar/dead. This property must be preserved when
40+
// modifying the encoding.
4641
//
4742
// The bitmap for noscan spans is not maintained. Code must ensure
4843
// that an object is scannable before consulting its bitmap by
4944
// checking either the noscan bit in the span or by consulting its
5045
// type's information.
51-
//
52-
// Checkmarks
53-
//
54-
// In a concurrent garbage collector, one worries about failing to mark
55-
// a live object due to mutations without write barriers or bugs in the
56-
// collector implementation. As a sanity check, the GC has a 'checkmark'
57-
// mode that retraverses the object graph with the world stopped, to make
58-
// sure that everything that should be marked is marked.
59-
// In checkmark mode, in the heap bitmap, the high bit of the 2-bit entry
60-
// for the second word of the object holds the checkmark bit.
61-
// When not in checkmark mode, this bit is set to 1.
62-
//
63-
// The smallest possible allocation is 8 bytes. On a 32-bit machine, that
64-
// means every allocated object has two words, so there is room for the
65-
// checkmark bit. On a 64-bit machine, however, the 8-byte allocation is
66-
// just one word, so the second bit pair is not available for encoding the
67-
// checkmark. However, because non-pointer allocations are combined
68-
// into larger 16-byte (maxTinySize) allocations, a plain 8-byte allocation
69-
// must be a pointer, so the type bit in the first word is not actually needed.
70-
// It is still used in general, except in checkmark the type bit is repurposed
71-
// as the checkmark bit and then reinitialized (to 1) as the type bit when
72-
// finished.
73-
//
7446

7547
package runtime
7648

@@ -551,33 +523,6 @@ func (h heapBits) isPointer() bool {
551523
return h.bits()&bitPointer != 0
552524
}
553525

554-
// isCheckmarked reports whether the heap bits have the checkmarked bit set.
555-
// It must be told how large the object at h is, because the encoding of the
556-
// checkmark bit varies by size.
557-
// h must describe the initial word of the object.
558-
func (h heapBits) isCheckmarked(size uintptr) bool {
559-
if size == sys.PtrSize {
560-
return (*h.bitp>>h.shift)&bitPointer != 0
561-
}
562-
// All multiword objects are 2-word aligned,
563-
// so we know that the initial word's 2-bit pair
564-
// and the second word's 2-bit pair are in the
565-
// same heap bitmap byte, *h.bitp.
566-
return (*h.bitp>>(heapBitsShift+h.shift))&bitScan != 0
567-
}
568-
569-
// setCheckmarked sets the checkmarked bit.
570-
// It must be told how large the object at h is, because the encoding of the
571-
// checkmark bit varies by size.
572-
// h must describe the initial word of the object.
573-
func (h heapBits) setCheckmarked(size uintptr) {
574-
if size == sys.PtrSize {
575-
atomic.Or8(h.bitp, bitPointer<<h.shift)
576-
return
577-
}
578-
atomic.Or8(h.bitp, bitScan<<(heapBitsShift+h.shift))
579-
}
580-
581526
// bulkBarrierPreWrite executes a write barrier
582527
// for every pointer slot in the memory range [src, src+size),
583528
// using pointer/scalar information from [dst, dst+size).
@@ -795,7 +740,6 @@ func typeBitsBulkBarrier(typ *_type, dst, src, size uintptr) {
795740
// TODO(rsc): Perhaps introduce a different heapBitsSpan type.
796741

797742
// initSpan initializes the heap bitmap for a span.
798-
// It clears all checkmark bits.
799743
// If this is a span of pointer-sized objects, it initializes all
800744
// words to pointer/scan.
801745
// Otherwise, it initializes all words to scalar/dead.
@@ -826,45 +770,6 @@ func (h heapBits) initSpan(s *mspan) {
826770
}
827771
}
828772

829-
// initCheckmarkSpan initializes a span for being checkmarked.
830-
// It clears the checkmark bits, which are set to 1 in normal operation.
831-
func (h heapBits) initCheckmarkSpan(size, n, total uintptr) {
832-
// The ptrSize == 8 is a compile-time constant false on 32-bit and eliminates this code entirely.
833-
if sys.PtrSize == 8 && size == sys.PtrSize {
834-
// Checkmark bit is type bit, bottom bit of every 2-bit entry.
835-
// Only possible on 64-bit system, since minimum size is 8.
836-
// Must clear type bit (checkmark bit) of every word.
837-
// The type bit is the lower of every two-bit pair.
838-
for i := uintptr(0); i < n; i += wordsPerBitmapByte {
839-
*h.bitp &^= bitPointerAll
840-
h = h.forward(wordsPerBitmapByte)
841-
}
842-
return
843-
}
844-
for i := uintptr(0); i < n; i++ {
845-
*h.bitp &^= bitScan << (heapBitsShift + h.shift)
846-
h = h.forward(size / sys.PtrSize)
847-
}
848-
}
849-
850-
// clearCheckmarkSpan undoes all the checkmarking in a span.
851-
// The actual checkmark bits are ignored, so the only work to do
852-
// is to fix the pointer bits. (Pointer bits are ignored by scanobject
853-
// but consulted by typedmemmove.)
854-
func (h heapBits) clearCheckmarkSpan(size, n, total uintptr) {
855-
// The ptrSize == 8 is a compile-time constant false on 32-bit and eliminates this code entirely.
856-
if sys.PtrSize == 8 && size == sys.PtrSize {
857-
// Checkmark bit is type bit, bottom bit of every 2-bit entry.
858-
// Only possible on 64-bit system, since minimum size is 8.
859-
// Must clear type bit (checkmark bit) of every word.
860-
// The type bit is the lower of every two-bit pair.
861-
for i := uintptr(0); i < n; i += wordsPerBitmapByte {
862-
*h.bitp |= bitPointerAll
863-
h = h.forward(wordsPerBitmapByte)
864-
}
865-
}
866-
}
867-
868773
// countAlloc returns the number of objects allocated in span s by
869774
// scanning the allocation bitmap.
870775
func (s *mspan) countAlloc() int {
@@ -957,11 +862,11 @@ func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
957862
if sys.PtrSize == 4 && dataSize == sys.PtrSize {
958863
// 1 pointer object. On 32-bit machines clear the bit for the
959864
// unused second word.
960-
*h.bitp &^= (bitPointer | bitScan | ((bitPointer | bitScan) << heapBitsShift)) << h.shift
865+
*h.bitp &^= (bitPointer | bitScan | (bitPointer|bitScan)<<heapBitsShift) << h.shift
961866
*h.bitp |= (bitPointer | bitScan) << h.shift
962867
} else {
963868
// 2-element slice of pointer.
964-
*h.bitp |= (bitPointer | bitScan | bitPointer<<heapBitsShift) << h.shift
869+
*h.bitp |= (bitPointer | bitScan | (bitPointer|bitScan)<<heapBitsShift) << h.shift
965870
}
966871
return
967872
}
@@ -974,11 +879,10 @@ func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
974879
}
975880
}
976881
b := uint32(*ptrmask)
977-
hb := (b & 3) | bitScan
978-
// bitPointer == 1, bitScan is 1 << 4, heapBitsShift is 1.
979-
// 110011 is shifted h.shift and complemented.
980-
// This clears out the bits that are about to be
981-
// ored into *h.hbitp in the next instructions.
882+
hb := b & 3
883+
hb |= bitScanAll & ((bitScan << (typ.ptrdata / sys.PtrSize)) - 1)
884+
// Clear the bits for this object so we can set the
885+
// appropriate ones.
982886
*h.bitp &^= (bitPointer | bitScan | ((bitPointer | bitScan) << heapBitsShift)) << h.shift
983887
*h.bitp |= uint8(hb << h.shift)
984888
return
@@ -1155,11 +1059,6 @@ func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
11551059
throw("heapBitsSetType: called with non-pointer type")
11561060
return
11571061
}
1158-
if nw < 2 {
1159-
// Must write at least 2 words, because the "no scan"
1160-
// encoding doesn't take effect until the third word.
1161-
nw = 2
1162-
}
11631062

11641063
// Phase 1: Special case for leading byte (shift==0) or half-byte (shift==2).
11651064
// The leading byte is special because it contains the bits for word 1,
@@ -1172,21 +1071,22 @@ func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
11721071

11731072
case h.shift == 0:
11741073
// Ptrmask and heap bitmap are aligned.
1175-
// Handle first byte of bitmap specially.
1074+
//
1075+
// This is a fast path for small objects.
11761076
//
11771077
// The first byte we write out covers the first four
11781078
// words of the object. The scan/dead bit on the first
11791079
// word must be set to scan since there are pointers
1180-
// somewhere in the object. The scan/dead bit on the
1181-
// second word is the checkmark, so we don't set it.
1080+
// somewhere in the object.
11821081
// In all following words, we set the scan/dead
11831082
// appropriately to indicate that the object contains
11841083
// to the next 2-bit entry in the bitmap.
11851084
//
1186-
// TODO: It doesn't matter if we set the checkmark, so
1187-
// maybe this case isn't needed any more.
1085+
// We set four bits at a time here, but if the object
1086+
// is fewer than four words, phase 3 will clear
1087+
// unnecessary bits.
11881088
hb = b & bitPointerAll
1189-
hb |= bitScan | bitScan<<(2*heapBitsShift) | bitScan<<(3*heapBitsShift)
1089+
hb |= bitScanAll
11901090
if w += 4; w >= nw {
11911091
goto Phase3
11921092
}
@@ -1203,14 +1103,13 @@ func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
12031103
// We took care of 1-word and 2-word objects above,
12041104
// so this is at least a 6-word object.
12051105
hb = (b & (bitPointer | bitPointer<<heapBitsShift)) << (2 * heapBitsShift)
1206-
// This is not noscan, so set the scan bit in the
1207-
// first word.
12081106
hb |= bitScan << (2 * heapBitsShift)
1107+
if nw > 1 {
1108+
hb |= bitScan << (3 * heapBitsShift)
1109+
}
12091110
b >>= 2
12101111
nb -= 2
1211-
// Note: no bitScan for second word because that's
1212-
// the checkmark.
1213-
*hbitp &^= uint8((bitPointer | bitScan | (bitPointer << heapBitsShift)) << (2 * heapBitsShift))
1112+
*hbitp &^= uint8((bitPointer | bitScan | ((bitPointer | bitScan) << heapBitsShift)) << (2 * heapBitsShift))
12141113
*hbitp |= uint8(hb)
12151114
hbitp = add1(hbitp)
12161115
if w += 2; w >= nw {
@@ -1449,11 +1348,7 @@ Phase4:
14491348
if j < nptr && (*addb(ptrmask, j/8)>>(j%8))&1 != 0 {
14501349
want |= bitPointer
14511350
}
1452-
if i != 1 {
1453-
want |= bitScan
1454-
} else {
1455-
have &^= bitScan
1456-
}
1351+
want |= bitScan
14571352
}
14581353
if have != want {
14591354
println("mismatch writing bits for", typ.string(), "x", dataSize/typ.size)
@@ -2013,7 +1908,7 @@ func getgcmask(ep interface{}) (mask []byte) {
20131908
if hbits.isPointer() {
20141909
mask[i/sys.PtrSize] = 1
20151910
}
2016-
if i != 1*sys.PtrSize && !hbits.morePointers() {
1911+
if !hbits.morePointers() {
20171912
mask = mask[:i/sys.PtrSize]
20181913
break
20191914
}

0 commit comments

Comments
 (0)