Skip to content

Commit 02f061c

Browse files
committed
runtime (gc.blocks): move objHeader to the end
This moves the objHeader from before an object body to after it. On 32-bit systems with 16-byte alignment requirements (x86, ARM, RISC-V), we previously padded the header to a whole block. This wastes up to 12 bytes, as on -gc=conservative the header is a single pointer. With this change, no padding is required (beyond that from rounding the size up). The "head" block in the metadata was moved to the end of the range to match the header location. This changed the block loop directions throughout the GC logic. The bit hacks used by sweep no longer work because there is no equivalent of addition that carries downwards. However it is now possible to merge the sweep and free range list rebuild passes because their loop directions match. There are two other places where we rebuilt the free ranges list: when initializing or growing the heap. The former can be easily replaced with a single hardcoded range containing the entire heap. In the latter case, I opted to only add the new space to the existing list. These replacements allowed me to fully remove the buildFreeRanges function.
1 parent 469e243 commit 02f061c

1 file changed

Lines changed: 114 additions & 117 deletions

File tree

src/runtime/gc_blocks.go

Lines changed: 114 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ package runtime
88
// The memory manager internally uses blocks of 4 pointers big (see
99
// bytesPerBlock). Every allocation first rounds up to this size to align every
1010
// block. It will first try to find a chain of blocks that is big enough to
11-
// satisfy the allocation. If it finds one, it marks the first one as the "head"
12-
// and the following ones (if any) as the "tail" (see below). If it cannot find
11+
// satisfy the allocation. If it finds one, it marks the last one as the "head"
12+
// and the preceding ones (if any) as the "tail" (see below). If it cannot find
1313
// any free space, it will perform a garbage collection cycle and try again. If
1414
// it still cannot find any free space, it gives up.
1515
//
1616
// Every block has some metadata, which is stored at the end of the heap.
1717
// The four states are "free", "head", "tail", and "mark". During normal
18-
// operation, there are no marked blocks. Every allocated object starts with a
19-
// "head" and is followed by "tail" blocks. The reason for this distinction is
18+
// operation, there are no marked blocks. Every allocated object ends with a
19+
// "head" and is preceded by "tail" blocks. The reason for this distinction is
2020
// that this way, the start and end of every object can be found easily.
2121
//
2222
// Metadata is stored in a special area at the end of the heap, in the area
@@ -129,7 +129,7 @@ func (b gcBlock) address() uintptr {
129129
return addr
130130
}
131131

132-
// findHead returns the head (first block) of an object, assuming the block
132+
// findHead returns the head (last block) of an object, assuming the block
133133
// points to an allocated object. It returns the same block if this block
134134
// already points to the head.
135135
func (b gcBlock) findHead() gcBlock {
@@ -142,7 +142,7 @@ func (b gcBlock) findHead() gcBlock {
142142
// large allocation.
143143
stateByte := b.stateByte()
144144
if stateByte == blockStateByteAllTails {
145-
b -= (b % blocksPerStateByte) + 1
145+
b += blocksPerStateByte - (b % blocksPerStateByte)
146146
continue
147147
}
148148

@@ -152,7 +152,7 @@ func (b gcBlock) findHead() gcBlock {
152152
if state != blockStateTail {
153153
break
154154
}
155-
b--
155+
b++
156156
}
157157
if gcAsserts {
158158
if b.state() != blockStateHead && b.state() != blockStateMark {
@@ -162,18 +162,6 @@ func (b gcBlock) findHead() gcBlock {
162162
return b
163163
}
164164

165-
// findNext returns the first block just past the end of the tail. This may or
166-
// may not be the head of an object.
167-
func (b gcBlock) findNext() gcBlock {
168-
if b.state() == blockStateHead || b.state() == blockStateMark {
169-
b++
170-
}
171-
for b.address() < uintptr(metadataStart) && b.state() == blockStateTail {
172-
b++
173-
}
174-
return b
175-
}
176-
177165
func (b gcBlock) stateByte() byte {
178166
return *(*uint8)(unsafe.Add(metadataStart, b/blocksPerStateByte))
179167
}
@@ -200,7 +188,22 @@ func (b gcBlock) setState(newState blockState) {
200188
}
201189
}
202190

203-
// objHeader is a structure prepended to every heap object to hold metadata.
191+
// unmark changes the state of b from blockStateMark to blockStateHead.
192+
func (b gcBlock) unmark() {
193+
if gcAsserts && b.state() != blockStateMark {
194+
runtimePanic("gc: block not marked")
195+
}
196+
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, b/blocksPerStateByte))
197+
*stateBytePtr ^= uint8(blockStateMark^blockStateHead) << (b % blocksPerStateByte)
198+
}
199+
200+
// free changes the state of b to blockStateFree.
201+
func (b gcBlock) free() {
202+
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, b/blocksPerStateByte))
203+
*stateBytePtr &^= uint8(blockStateMask) << (b % blocksPerStateByte)
204+
}
205+
206+
// objHeader is a structure appended to every heap object to hold metadata.
204207
type objHeader struct {
205208
// next is the next object to scan after this.
206209
next *objHeader
@@ -317,8 +320,12 @@ func initHeap() {
317320
metadataSize := heapEnd - uintptr(metadataStart)
318321
memzero(unsafe.Pointer(metadataStart), metadataSize)
319322

320-
// Rebuild the free ranges list.
321-
buildFreeRanges()
323+
// Create the initial free range.
324+
if endBlock > 0 {
325+
r := (*freeRange)(unsafe.Pointer(heapStart))
326+
*r = freeRange{len: uintptr(endBlock)}
327+
freeRanges = r
328+
}
322329
}
323330

324331
// setHeapEnd is called to expand the heap. The heap can only grow, not shrink.
@@ -340,6 +347,7 @@ func setHeapEnd(newHeapEnd uintptr) {
340347
// memcpy is fine as it only copies the old metadata and the new memory will
341348
// have been zero initialized.
342349
heapEnd = newHeapEnd
350+
oldEndBlock := endBlock
343351
calculateHeapAddresses()
344352
memcpy(metadataStart, oldMetadataStart, oldMetadataSize)
345353

@@ -351,8 +359,14 @@ func setHeapEnd(newHeapEnd uintptr) {
351359
runtimePanic("gc: heap did not grow enough at once")
352360
}
353361

354-
// Rebuild the free ranges list.
355-
buildFreeRanges()
362+
// Insert the new free range. This range will be separate from any previous
363+
// free space at the end of the heap. This may result in more heap growth
364+
// than strictly necessary when an allocation requests more memory than the
365+
// previous heap size. Otherwise this will only result in slightly more
366+
// memory fragmentation than necessary. We cannot easily remove the old
367+
// range and adding a special free-list rebuild function for this edge case
368+
// would not be worthwhile in terms of binary size or code maintenance.
369+
insertFreeRange(oldEndBlock.pointer(), uintptr(endBlock-oldEndBlock))
356370
}
357371

358372
// calculateHeapAddresses initializes variables such as metadataStart and
@@ -400,7 +414,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
400414

401415
// Round the size up to a multiple of blocks, adding space for the header.
402416
rawSize := size
403-
size += align(unsafe.Sizeof(objHeader{}))
417+
size += unsafe.Sizeof(objHeader{})
404418
size += bytesPerBlock - 1
405419
if size < rawSize {
406420
// The size overflowed.
@@ -456,25 +470,27 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
456470
runtimePanicAt(returnAddress(0), "out of memory")
457471
}
458472

459-
// Set the backing blocks as being allocated.
473+
// Set the block states.
460474
block := blockFromAddr(uintptr(pointer))
461-
block.setState(blockStateHead)
462-
for i := block + 1; i != block+gcBlock(neededBlocks); i++ {
475+
i := block + gcBlock(neededBlocks) - 1
476+
i.setState(blockStateHead)
477+
for i != block {
478+
i--
463479
i.setState(blockStateTail)
464480
}
465481

466482
// Create the object header.
467-
header := (*objHeader)(pointer)
483+
size -= unsafe.Sizeof(objHeader{})
484+
header := (*objHeader)(unsafe.Add(pointer, size))
468485
header.layout = parseGCLayout(layout)
469486

470487
// We've claimed this allocation, now we can unlock the heap.
471488
gcLock.Unlock()
472489

473-
// Return a pointer to this allocation.
474-
add := align(unsafe.Sizeof(objHeader{}))
475-
pointer = unsafe.Add(pointer, add)
476-
size -= add
490+
// Clear the allocation body.
477491
memzero(pointer, size)
492+
493+
// Return a pointer to this allocation.
478494
return pointer
479495
}
480496

@@ -483,16 +499,34 @@ func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
483499
return alloc(size, nil)
484500
}
485501

486-
ptrAddress := uintptr(ptr)
487-
endOfTailAddress := blockFromAddr(ptrAddress).findNext().address()
502+
// Find the first block of the original allocation.
503+
firstBlock := blockFromAddr(uintptr(ptr))
504+
505+
// Find the last block of the original allocation.
506+
lastBlock := firstBlock
507+
for lastBlock.state() == blockStateTail {
508+
lastBlock++
509+
}
510+
if gcAsserts && lastBlock.state() != blockStateHead {
511+
runtimePanic("gc: realloc of free or corrupted allocation")
512+
}
513+
514+
// Calculate the size of the original allocation body.
515+
oldSize := uintptr(lastBlock-firstBlock)*blocksPerStateByte + (bytesPerBlock - unsafe.Sizeof(objHeader{}))
488516

489-
// this might be a few bytes longer than the original size of
490-
// ptr, because we align to full blocks of size bytesPerBlock
491-
oldSize := endOfTailAddress - ptrAddress
492517
if size <= oldSize {
518+
// The requested size is less than the old size.
519+
// There are likely scenarios for this:
520+
// - The caller intended to grow the allocation, but the original size
521+
// was rounded up by alloc to a multiple of the block size.
522+
// The rounded size is already sufficient.
523+
// - The caller intended to shrink the allocation.
524+
// We currently ignore this case.
525+
// Either way, the current allocation can be left alone.
493526
return ptr
494527
}
495528

529+
// Create a new allocation and copy the old data.
496530
newAlloc := alloc(size, nil)
497531
memcpy(newAlloc, ptr, oldSize)
498532
free(ptr)
@@ -559,11 +593,8 @@ func runGC() (freeBytes uintptr) {
559593
gcResumeWorld()
560594

561595
// Sweep phase: free all non-marked objects and unmark marked objects for
562-
// the next collection cycle.
563-
sweep()
564-
565-
// Rebuild the free ranges list.
566-
freeBytes = buildFreeRanges()
596+
// the next collection cycle. This also rebuilds the free ranges list.
597+
freeBytes = sweep()
567598

568599
// Show how much has been sweeped, for debugging.
569600
if gcDebug {
@@ -629,13 +660,21 @@ func finishMark() {
629660
continue
630661
}
631662

632-
// Compute the scan bounds.
633-
objAddr := uintptr(unsafe.Pointer(obj))
634-
start := objAddr + align(unsafe.Sizeof(objHeader{}))
635-
end := blockFromAddr(objAddr).findNext().address()
663+
// Find the last block in the object.
664+
// This block contains the header.
665+
lastBlock := blockFromAddr(uintptr(unsafe.Pointer(obj)))
666+
667+
// Find the first block in the allocation.
668+
firstBlock := lastBlock
669+
for firstBlock > 0 && (firstBlock-1).state() == blockStateTail {
670+
firstBlock--
671+
}
672+
673+
// Compute the size of the allocation.
674+
bodySize := uintptr(lastBlock-firstBlock)*bytesPerBlock + (bytesPerBlock - unsafe.Sizeof(objHeader{}))
636675

637676
// Scan the object.
638-
obj.layout.scan(start, end-start)
677+
obj.layout.scan(firstBlock.address(), bodySize)
639678
}
640679
}
641680

@@ -668,97 +707,55 @@ func markRoot(addr, root uintptr) {
668707
head.setState(blockStateMark)
669708

670709
// Add the object to the scan list.
671-
header := (*objHeader)(head.pointer())
710+
header := (*objHeader)(unsafe.Add(head.pointer(), bytesPerBlock-unsafe.Sizeof(objHeader{})))
672711
header.next = scanList
673712
scanList = header
674713
}
675714

676715
// Sweep goes through all memory and frees unmarked memory.
677-
func sweep() {
678-
metadataEnd := unsafe.Add(metadataStart, (endBlock+(blocksPerStateByte-1))/blocksPerStateByte)
679-
var carry byte
680-
for meta := metadataStart; meta != metadataEnd; meta = unsafe.Add(meta, 1) {
681-
// Fetch the state byte.
682-
stateBytePtr := (*byte)(unsafe.Pointer(meta))
683-
stateByte := *stateBytePtr
684-
685-
// Separate blocks by type.
686-
// Split the nibbles.
687-
// Each nibble is a mask of blocks.
688-
high := stateByte >> blocksPerStateByte
689-
low := stateByte & blockStateEach
690-
// Marked heads are in both nibbles.
691-
markedHeads := low & high
692-
// Unmarked heads are in the low nibble but not the high nibble.
693-
unmarkedHeads := low &^ high
694-
// Tails are in the high nibble but not the low nibble.
695-
tails := high &^ low
696-
697-
// Clear all tail runs after unmarked (freed) heads.
698-
//
699-
// Adding 1 to the start of a bit run will clear the run and set the next bit:
700-
// (2^k - 1) + 1 = 2^k
701-
// e.g. 0b0011 + 1 = 0b0100
702-
// Bitwise-and with the original mask to clear the newly set bit.
703-
// e.g. (0b0011 + 1) & 0b0011 = 0b0100 & 0b0011 = 0b0000
704-
// This will not clear bits after the run because the gap stops the carry:
705-
// e.g. (0b1011 + 1) & 0b1011 = 0b1100 & 0b1011 = 0b1000
706-
// This can clear multiple runs in a single addition:
707-
// e.g. (0b1101 + 0b0101) & 0b1101 = 0b10010 & 0b1101 = 0b0000
708-
//
709-
// In order to find tail run starts after unmarked heads we could use tails & (unmarkedHeads << 1).
710-
// It is possible omit the bitwise-and because the clear still works if the next block is not a tail.
711-
// A head is not a tail, so corresponding missing tail bit will stop the carry from a previous tail run.
712-
// As such it will set the next bit which will be cleared back away later.
713-
// e.g. HHTH: (0b0010 + (0b1101 << 1)) & 0b0010 = 0b11100 & 0b0010 = 0b0000
714-
//
715-
// Treat the whole heap as a single pair of integer masks.
716-
// This is accomplished for addition by carrying the overflow to the next state byte.
717-
// The unmarkedHeads << 1 is equivalent to unmarkedHeads + unmarkedHeads, so it can be merged with the sum.
718-
// This does not require any special work for the bitwise-and because it operates bitwise.
719-
tailClear := tails + (unmarkedHeads << 1) + carry
720-
carry = tailClear >> blocksPerStateByte
721-
tails &= tailClear
722-
723-
// Construct the new state byte.
724-
*stateBytePtr = markedHeads | (tails << blocksPerStateByte)
725-
}
726-
}
727-
728-
// buildFreeRanges rebuilds the freeRanges list.
729-
// This must be called after a GC sweep or heap grow.
730-
// It returns how many bytes are free in the heap.
731-
func buildFreeRanges() uintptr {
716+
func sweep() uintptr {
717+
// Discard the old free ranges list.
732718
freeRanges = nil
719+
720+
// Scan backwards through the block metadata.
733721
block := endBlock
734-
var totalBlocks uintptr
722+
var freeBlocks uintptr
735723
for {
736-
// Skip backwards over occupied blocks.
737-
for block > 0 && (block-1).state() != blockStateFree {
724+
// Scan backwards until we find a marked head.
725+
// Free the blocks as we go.
726+
freeEnd := block
727+
for block > 0 && (block-1).state() != blockStateMark {
738728
block--
729+
block.free()
730+
}
731+
732+
if freeLen := uintptr(freeEnd - block); freeLen > 0 {
733+
// Insert the freed blocks.
734+
freeBlocks += freeLen
735+
insertFreeRange(block.pointer(), freeLen)
739736
}
737+
740738
if block == 0 {
739+
// There are no more blocks to sweep.
741740
break
742741
}
743742

744-
// Find the start of the free range.
745-
end := block
746-
for block > 0 && (block-1).state() == blockStateFree {
743+
// Unmark the next head.
744+
block--
745+
block.unmark()
746+
747+
// Skip the tail.
748+
for block > 0 && (block-1).state() == blockStateTail {
747749
block--
748750
}
749-
750-
// Insert the free range.
751-
len := uintptr(end - block)
752-
totalBlocks += len
753-
insertFreeRange(block.pointer(), len)
754751
}
755752

756753
if gcDebug {
757-
println("free ranges after rebuild:")
754+
println("free ranges after sweep:")
758755
dumpFreeRangeCounts()
759756
}
760757

761-
return totalBlocks * bytesPerBlock
758+
return freeBlocks * bytesPerBlock
762759
}
763760

764761
func dumpFreeRangeCounts() {

0 commit comments

Comments
 (0)