mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-12 23:13:40 +00:00
runtime (gc_blocks.go): make sweep branchless
Instead of looping over each block, we can use bit hacks to operate on an entire state byte. I deinterleaved the state bits in order to enable these tricks. Sweep used to count free/freed allocations/blocks. I managed to move/remove all of these counters: - The free space is now calculated in buildFreeRanges by adding the range lengths. - ReadMemStats counts freed objects by subtracting live objects from allocated objects. - gcFreedBlocks was never necessary because MemStats.HeapAlloc is the same as MemStats.HeapInUse.
This commit is contained in:
@@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
|
|||||||
// This is a small number of very diverse targets that we want to test.
|
// This is a small number of very diverse targets that we want to test.
|
||||||
tests := []sizeTest{
|
tests := []sizeTest{
|
||||||
// microcontrollers
|
// microcontrollers
|
||||||
{"hifive1b", "examples/echo", 3808, 280, 0, 2268},
|
{"hifive1b", "examples/echo", 3668, 280, 0, 2244},
|
||||||
{"microbit", "examples/serial", 2790, 342, 8, 2272},
|
{"microbit", "examples/serial", 2694, 342, 8, 2248},
|
||||||
{"wioterminal", "examples/pininterrupt", 7327, 1493, 116, 6912},
|
{"wioterminal", "examples/pininterrupt", 7187, 1489, 116, 6888},
|
||||||
|
|
||||||
// TODO: also check wasm. Right now this is difficult, because
|
// TODO: also check wasm. Right now this is difficult, because
|
||||||
// wasm binaries are run through wasm-opt and therefore the
|
// wasm binaries are run through wasm-opt and therefore the
|
||||||
|
|||||||
+141
-97
@@ -54,10 +54,7 @@ var (
|
|||||||
freeRanges *freeRange // freeRanges is a linked list of free block ranges
|
freeRanges *freeRange // freeRanges is a linked list of free block ranges
|
||||||
endBlock gcBlock // the block just past the end of the available space
|
endBlock gcBlock // the block just past the end of the available space
|
||||||
gcTotalAlloc uint64 // total number of bytes allocated
|
gcTotalAlloc uint64 // total number of bytes allocated
|
||||||
gcTotalBlocks uint64 // total number of allocated blocks
|
|
||||||
gcMallocs uint64 // total number of allocations
|
gcMallocs uint64 // total number of allocations
|
||||||
gcFrees uint64 // total number of objects freed
|
|
||||||
gcFreedBlocks uint64 // total number of freed blocks
|
|
||||||
gcLock task.PMutex // lock to avoid race conditions on multicore systems
|
gcLock task.PMutex // lock to avoid race conditions on multicore systems
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,24 +63,28 @@ var zeroSizedAlloc uint8
|
|||||||
|
|
||||||
// Provide some abstraction over heap blocks.
|
// Provide some abstraction over heap blocks.
|
||||||
|
|
||||||
// blockState stores the four states in which a block can be. It is two bits in
|
// blockState stores the four states in which a block can be.
|
||||||
// size.
|
// It holds 1 bit in each nibble.
|
||||||
|
// When stored into a state byte, each bit in a nibble corresponds to a different block.
|
||||||
|
// For blocks A-D, a state byte would be laid out as 0bDCBA_DCBA.
|
||||||
type blockState uint8
|
type blockState uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
blockStateFree blockState = 0 // 00
|
blockStateLow blockState = 1
|
||||||
blockStateHead blockState = 1 // 01
|
blockStateHigh blockState = 1 << blocksPerStateByte
|
||||||
blockStateTail blockState = 2 // 10
|
|
||||||
blockStateMark blockState = 3 // 11
|
blockStateFree blockState = 0
|
||||||
blockStateMask blockState = 3 // 11
|
blockStateHead blockState = blockStateLow
|
||||||
|
blockStateTail blockState = blockStateHigh
|
||||||
|
blockStateMark blockState = blockStateLow | blockStateHigh
|
||||||
|
blockStateMask blockState = blockStateLow | blockStateHigh
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// blockStateEach is a mask that can be used to extract a nibble from the block state.
|
||||||
|
const blockStateEach = 1<<blocksPerStateByte - 1
|
||||||
|
|
||||||
// The byte value of a block where every block is a 'tail' block.
|
// The byte value of a block where every block is a 'tail' block.
|
||||||
const blockStateByteAllTails = 0 |
|
const blockStateByteAllTails = byte(blockStateTail) * blockStateEach
|
||||||
uint8(blockStateTail<<(stateBits*3)) |
|
|
||||||
uint8(blockStateTail<<(stateBits*2)) |
|
|
||||||
uint8(blockStateTail<<(stateBits*1)) |
|
|
||||||
uint8(blockStateTail<<(stateBits*0))
|
|
||||||
|
|
||||||
// String returns a human-readable version of the block state, for debugging.
|
// String returns a human-readable version of the block state, for debugging.
|
||||||
func (s blockState) String() string {
|
func (s blockState) String() string {
|
||||||
@@ -180,7 +181,7 @@ func (b gcBlock) stateByte() byte {
|
|||||||
// Return the block state given a state byte. The state byte must have been
|
// Return the block state given a state byte. The state byte must have been
|
||||||
// obtained using b.stateByte(), otherwise the result is incorrect.
|
// obtained using b.stateByte(), otherwise the result is incorrect.
|
||||||
func (b gcBlock) stateFromByte(stateByte byte) blockState {
|
func (b gcBlock) stateFromByte(stateByte byte) blockState {
|
||||||
return blockState(stateByte>>((b%blocksPerStateByte)*stateBits)) & blockStateMask
|
return blockState(stateByte>>(b%blocksPerStateByte)) & blockStateMask
|
||||||
}
|
}
|
||||||
|
|
||||||
// State returns the current block state.
|
// State returns the current block state.
|
||||||
@@ -193,38 +194,12 @@ func (b gcBlock) state() blockState {
|
|||||||
// from head to mark.
|
// from head to mark.
|
||||||
func (b gcBlock) setState(newState blockState) {
|
func (b gcBlock) setState(newState blockState) {
|
||||||
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, b/blocksPerStateByte))
|
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, b/blocksPerStateByte))
|
||||||
*stateBytePtr |= uint8(newState << ((b % blocksPerStateByte) * stateBits))
|
*stateBytePtr |= uint8(newState << (b % blocksPerStateByte))
|
||||||
if gcAsserts && b.state() != newState {
|
if gcAsserts && b.state() != newState {
|
||||||
runtimePanic("gc: setState() was not successful")
|
runtimePanic("gc: setState() was not successful")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// markFree sets the block state to free, no matter what state it was in before.
|
|
||||||
func (b gcBlock) markFree() {
|
|
||||||
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, b/blocksPerStateByte))
|
|
||||||
*stateBytePtr &^= uint8(blockStateMask << ((b % blocksPerStateByte) * stateBits))
|
|
||||||
if gcAsserts && b.state() != blockStateFree {
|
|
||||||
runtimePanic("gc: markFree() was not successful")
|
|
||||||
}
|
|
||||||
if gcAsserts {
|
|
||||||
*(*[wordsPerBlock]uintptr)(unsafe.Pointer(b.address())) = [wordsPerBlock]uintptr{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// unmark changes the state of the block from mark to head. It must be marked
|
|
||||||
// before calling this function.
|
|
||||||
func (b gcBlock) unmark() {
|
|
||||||
if gcAsserts && b.state() != blockStateMark {
|
|
||||||
runtimePanic("gc: unmark() on a block that is not marked")
|
|
||||||
}
|
|
||||||
clearMask := blockStateMask ^ blockStateHead // the bits to clear from the state
|
|
||||||
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, b/blocksPerStateByte))
|
|
||||||
*stateBytePtr &^= uint8(clearMask << ((b % blocksPerStateByte) * stateBits))
|
|
||||||
if gcAsserts && b.state() != blockStateHead {
|
|
||||||
runtimePanic("gc: unmark() was not successful")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// objHeader is a structure prepended to every heap object to hold metadata.
|
// objHeader is a structure prepended to every heap object to hold metadata.
|
||||||
type objHeader struct {
|
type objHeader struct {
|
||||||
// next is the next object to scan after this.
|
// next is the next object to scan after this.
|
||||||
@@ -441,7 +416,6 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
|
|||||||
// Update the total allocation counters.
|
// Update the total allocation counters.
|
||||||
gcTotalAlloc += uint64(rawSize)
|
gcTotalAlloc += uint64(rawSize)
|
||||||
gcMallocs++
|
gcMallocs++
|
||||||
gcTotalBlocks += uint64(neededBlocks)
|
|
||||||
|
|
||||||
// Acquire a range of free blocks.
|
// Acquire a range of free blocks.
|
||||||
var ranGC bool
|
var ranGC bool
|
||||||
@@ -586,10 +560,10 @@ func runGC() (freeBytes uintptr) {
|
|||||||
|
|
||||||
// Sweep phase: free all non-marked objects and unmark marked objects for
|
// Sweep phase: free all non-marked objects and unmark marked objects for
|
||||||
// the next collection cycle.
|
// the next collection cycle.
|
||||||
freeBytes = sweep()
|
sweep()
|
||||||
|
|
||||||
// Rebuild the free ranges list.
|
// Rebuild the free ranges list.
|
||||||
buildFreeRanges()
|
freeBytes = buildFreeRanges()
|
||||||
|
|
||||||
// Show how much has been sweeped, for debugging.
|
// Show how much has been sweeped, for debugging.
|
||||||
if gcDebug {
|
if gcDebug {
|
||||||
@@ -700,45 +674,64 @@ func markRoot(addr, root uintptr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sweep goes through all memory and frees unmarked memory.
|
// Sweep goes through all memory and frees unmarked memory.
|
||||||
// It returns how many bytes are free in the heap after the sweep.
|
func sweep() {
|
||||||
func sweep() (freeBytes uintptr) {
|
metadataEnd := unsafe.Add(metadataStart, (endBlock+(blocksPerStateByte-1))/blocksPerStateByte)
|
||||||
freeCurrentObject := false
|
var carry byte
|
||||||
var freed uint64
|
for meta := metadataStart; meta != metadataEnd; meta = unsafe.Add(meta, 1) {
|
||||||
for block := gcBlock(0); block < endBlock; block++ {
|
// Fetch the state byte.
|
||||||
switch block.state() {
|
stateBytePtr := (*byte)(unsafe.Pointer(meta))
|
||||||
case blockStateHead:
|
stateByte := *stateBytePtr
|
||||||
// Unmarked head. Free it, including all tail blocks following it.
|
|
||||||
block.markFree()
|
// Separate blocks by type.
|
||||||
freeCurrentObject = true
|
// Split the nibbles.
|
||||||
gcFrees++
|
// Each nibble is a mask of blocks.
|
||||||
freed++
|
high := stateByte >> blocksPerStateByte
|
||||||
case blockStateTail:
|
low := stateByte & blockStateEach
|
||||||
if freeCurrentObject {
|
// Marked heads are in both nibbles.
|
||||||
// This is a tail object following an unmarked head.
|
markedHeads := low & high
|
||||||
// Free it now.
|
// Unmarked heads are in the low nibble but not the high nibble.
|
||||||
block.markFree()
|
unmarkedHeads := low &^ high
|
||||||
freed++
|
// Tails are in the high nibble but not the low nibble.
|
||||||
}
|
tails := high &^ low
|
||||||
case blockStateMark:
|
|
||||||
// This is a marked object. The next tail blocks must not be freed,
|
// Clear all tail runs after unmarked (freed) heads.
|
||||||
// but the mark bit must be removed so the next GC cycle will
|
//
|
||||||
// collect this object if it is unreferenced then.
|
// Adding 1 to the start of a bit run will clear the run and set the next bit:
|
||||||
block.unmark()
|
// (2^k - 1) + 1 = 2^k
|
||||||
freeCurrentObject = false
|
// e.g. 0b0011 + 1 = 0b0100
|
||||||
case blockStateFree:
|
// Bitwise-and with the original mask to clear the newly set bit.
|
||||||
freeBytes += bytesPerBlock
|
// e.g. (0b0011 + 1) & 0b0011 = 0b0100 & 0b0011 = 0b0000
|
||||||
}
|
// This will not clear bits after the run because the gap stops the carry:
|
||||||
|
// e.g. (0b1011 + 1) & 0b1011 = 0b1100 & 0b1011 = 0b1000
|
||||||
|
// This can clear multiple runs in a single addition:
|
||||||
|
// e.g. (0b1101 + 0b0101) & 0b1101 = 0b10010 & 0b1101 = 0b0000
|
||||||
|
//
|
||||||
|
// In order to find tail run starts after unmarked heads we could use tails & (unmarkedHeads << 1).
|
||||||
|
// It is possible omit the bitwise-and because the clear still works if the next block is not a tail.
|
||||||
|
// A head is not a tail, so corresponding missing tail bit will stop the carry from a previous tail run.
|
||||||
|
// As such it will set the next bit which will be cleared back away later.
|
||||||
|
// e.g. HHTH: (0b0010 + (0b1101 << 1)) & 0b0010 = 0b11100 & 0b0010 = 0b0000
|
||||||
|
//
|
||||||
|
// Treat the whole heap as a single pair of integer masks.
|
||||||
|
// This is accomplished for addition by carrying the overflow to the next state byte.
|
||||||
|
// The unmarkedHeads << 1 is equivalent to unmarkedHeads + unmarkedHeads, so it can be merged with the sum.
|
||||||
|
// This does not require any special work for the bitwise-and because it operates bitwise.
|
||||||
|
tailClear := tails + (unmarkedHeads << 1) + carry
|
||||||
|
carry = tailClear >> blocksPerStateByte
|
||||||
|
tails &= tailClear
|
||||||
|
|
||||||
|
// Construct the new state byte.
|
||||||
|
*stateBytePtr = markedHeads | (tails << blocksPerStateByte)
|
||||||
}
|
}
|
||||||
gcFreedBlocks += freed
|
|
||||||
freeBytes += uintptr(freed) * bytesPerBlock
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildFreeRanges rebuilds the freeRanges list.
|
// buildFreeRanges rebuilds the freeRanges list.
|
||||||
// This must be called after a GC sweep or heap grow.
|
// This must be called after a GC sweep or heap grow.
|
||||||
func buildFreeRanges() {
|
// It returns how many bytes are free in the heap.
|
||||||
|
func buildFreeRanges() uintptr {
|
||||||
freeRanges = nil
|
freeRanges = nil
|
||||||
block := endBlock
|
block := endBlock
|
||||||
|
var totalBlocks uintptr
|
||||||
for {
|
for {
|
||||||
// Skip backwards over occupied blocks.
|
// Skip backwards over occupied blocks.
|
||||||
for block > 0 && (block-1).state() != blockStateFree {
|
for block > 0 && (block-1).state() != blockStateFree {
|
||||||
@@ -755,13 +748,17 @@ func buildFreeRanges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert the free range.
|
// Insert the free range.
|
||||||
insertFreeRange(block.pointer(), uintptr(end-block))
|
len := uintptr(end - block)
|
||||||
|
totalBlocks += len
|
||||||
|
insertFreeRange(block.pointer(), len)
|
||||||
}
|
}
|
||||||
|
|
||||||
if gcDebug {
|
if gcDebug {
|
||||||
println("free ranges after rebuild:")
|
println("free ranges after rebuild:")
|
||||||
dumpFreeRangeCounts()
|
dumpFreeRangeCounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return totalBlocks * bytesPerBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpFreeRangeCounts() {
|
func dumpFreeRangeCounts() {
|
||||||
@@ -801,28 +798,75 @@ func dumpHeap() {
|
|||||||
// call to ReadMemStats. This would not do GC implicitly for you.
|
// call to ReadMemStats. This would not do GC implicitly for you.
|
||||||
func ReadMemStats(m *MemStats) {
|
func ReadMemStats(m *MemStats) {
|
||||||
gcLock.Lock()
|
gcLock.Lock()
|
||||||
m.HeapIdle = 0
|
|
||||||
m.HeapInuse = 0
|
// Calculate the raw size of the heap.
|
||||||
for block := gcBlock(0); block < endBlock; block++ {
|
heapEnd := heapEnd
|
||||||
bstate := block.state()
|
heapStart := heapStart
|
||||||
if bstate == blockStateFree {
|
|
||||||
m.HeapIdle += uint64(bytesPerBlock)
|
|
||||||
} else {
|
|
||||||
m.HeapInuse += uint64(bytesPerBlock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.HeapReleased = 0 // always 0, we don't currently release memory back to the OS.
|
|
||||||
m.HeapSys = m.HeapInuse + m.HeapIdle
|
|
||||||
m.GCSys = uint64(heapEnd - uintptr(metadataStart))
|
|
||||||
m.TotalAlloc = gcTotalAlloc
|
|
||||||
m.Mallocs = gcMallocs
|
|
||||||
m.Frees = gcFrees
|
|
||||||
m.Sys = uint64(heapEnd - heapStart)
|
m.Sys = uint64(heapEnd - heapStart)
|
||||||
m.HeapAlloc = (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock)
|
m.HeapSys = uint64(uintptr(metadataStart) - heapStart)
|
||||||
m.Alloc = m.HeapAlloc
|
metadataStart := metadataStart
|
||||||
|
// TODO: should GCSys include objHeaders?
|
||||||
|
m.GCSys = uint64(heapEnd - uintptr(metadataStart))
|
||||||
|
m.HeapReleased = 0 // always 0, we don't currently release memory back to the OS.
|
||||||
|
|
||||||
|
// Count live heads and tails.
|
||||||
|
var liveHeads, liveTails uintptr
|
||||||
|
endBlock := endBlock
|
||||||
|
metadataEnd := unsafe.Add(metadataStart, (endBlock+(blocksPerStateByte-1))/blocksPerStateByte)
|
||||||
|
for meta := metadataStart; meta != metadataEnd; meta = unsafe.Add(meta, 1) {
|
||||||
|
// Since we are outside of a GC, nothing is marked.
|
||||||
|
// A bit in the low nibble implies a head.
|
||||||
|
// A bit in the high nibble implies a tail.
|
||||||
|
stateByte := *(*byte)(unsafe.Pointer(meta))
|
||||||
|
liveHeads += uintptr(count4LUT[stateByte&blockStateEach])
|
||||||
|
liveTails += uintptr(count4LUT[stateByte>>blocksPerStateByte])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add heads and tails to count live blocks.
|
||||||
|
liveBlocks := liveHeads + liveTails
|
||||||
|
liveBytes := uint64(liveBlocks * bytesPerBlock)
|
||||||
|
m.HeapInuse = liveBytes
|
||||||
|
m.HeapAlloc = liveBytes
|
||||||
|
m.Alloc = liveBytes
|
||||||
|
|
||||||
|
// Subtract live blocks from total blocks to count free blocks.
|
||||||
|
freeBlocks := uintptr(endBlock) - liveBlocks
|
||||||
|
m.HeapIdle = uint64(freeBlocks * bytesPerBlock)
|
||||||
|
|
||||||
|
// Record the number of allocated objects.
|
||||||
|
gcMallocs := gcMallocs
|
||||||
|
m.Mallocs = gcMallocs
|
||||||
|
|
||||||
|
// Subtract live objects from allocated objects to count freed objects.
|
||||||
|
m.Frees = gcMallocs - uint64(liveHeads)
|
||||||
|
|
||||||
|
// Record the total allocated bytes.
|
||||||
|
m.TotalAlloc = gcTotalAlloc
|
||||||
|
|
||||||
gcLock.Unlock()
|
gcLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// count4LUT is a lookup table used to count set bits in a 4-bit mask.
|
||||||
|
// TODO: replace with popcnt when available
|
||||||
|
var count4LUT = [16]uint8{
|
||||||
|
0b0000: 0,
|
||||||
|
0b0001: 1,
|
||||||
|
0b0010: 1,
|
||||||
|
0b0011: 2,
|
||||||
|
0b0100: 1,
|
||||||
|
0b0101: 2,
|
||||||
|
0b0110: 2,
|
||||||
|
0b0111: 3,
|
||||||
|
0b1000: 1,
|
||||||
|
0b1001: 2,
|
||||||
|
0b1010: 2,
|
||||||
|
0b1011: 3,
|
||||||
|
0b1100: 2,
|
||||||
|
0b1101: 3,
|
||||||
|
0b1110: 3,
|
||||||
|
0b1111: 4,
|
||||||
|
}
|
||||||
|
|
||||||
func SetFinalizer(obj interface{}, finalizer interface{}) {
|
func SetFinalizer(obj interface{}, finalizer interface{}) {
|
||||||
// Unimplemented.
|
// Unimplemented.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user