Skip to content

Commit 6c5f7df

Browse files
committed
runtime (gc_blocks.go): use best-fit allocation
The allocator originally just looped through the blocks until it found a sufficiently-long range. This is simple, but it fragments very easily and can degrade to a full heap scan for long requests. Instead, we now maintain a sorted nested list of free ranges by size. The allocator will select the shortest sufficient-length range, generally reducing fragmentation. This data structure can find a range in time directly proportional to the requested length.
1 parent b37535b commit 6c5f7df

File tree

2 files changed

+193
-84
lines changed

2 files changed

+193
-84
lines changed

builder/sizes_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ func TestBinarySize(t *testing.T) {
4242
// This is a small number of very diverse targets that we want to test.
4343
tests := []sizeTest{
4444
// microcontrollers
45-
{"hifive1b", "examples/echo", 3568, 280, 0, 2268},
46-
{"microbit", "examples/serial", 2630, 342, 8, 2272},
47-
{"wioterminal", "examples/pininterrupt", 7175, 1493, 116, 6912},
45+
{"hifive1b", "examples/echo", 3808, 280, 0, 2268},
46+
{"microbit", "examples/serial", 2790, 342, 8, 2272},
47+
{"wioterminal", "examples/pininterrupt", 7327, 1493, 116, 6912},
4848

4949
// TODO: also check wasm. Right now this is difficult, because
5050
// wasm binaries are run through wasm-opt and therefore the

src/runtime/gc_blocks.go

Lines changed: 190 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const (
5151
var (
5252
metadataStart unsafe.Pointer // pointer to the start of the heap metadata
5353
scanList *objHeader // scanList is a singly linked list of heap objects that have been marked but not scanned
54-
nextAlloc gcBlock // the next block that should be tried by the allocator
54+
freeRanges *freeRange // freeRanges is a linked list of free block ranges
5555
endBlock gcBlock // the block just past the end of the available space
5656
gcTotalAlloc uint64 // total number of bytes allocated
5757
gcTotalBlocks uint64 // total number of allocated blocks
@@ -234,6 +234,99 @@ type objHeader struct {
234234
layout gcLayout
235235
}
236236

237+
// freeRange is a node on the outer list of range lengths.
238+
// The free ranges are structured as two nested singly-linked lists:
239+
// - The outer level (freeRange) has one entry for each unique range length.
240+
// - The inner level (freeRangeMore) has one entry for each additional range of the same length.
241+
// This two-level structure ensures that insertion/removal times are proportional to the requested length.
242+
type freeRange struct {
243+
// len is the length of this free range.
244+
len uintptr
245+
246+
// nextLen is the next longer free range.
247+
nextLen *freeRange
248+
249+
// nextWithLen is the next free range with this length.
250+
nextWithLen *freeRangeMore
251+
}
252+
253+
// freeRangeMore is a node on the inner list of equal-length ranges.
254+
type freeRangeMore struct {
255+
next *freeRangeMore
256+
}
257+
258+
// insertFreeRange inserts a range of len blocks starting at ptr into the free list.
259+
func insertFreeRange(ptr unsafe.Pointer, len uintptr) {
260+
if gcAsserts && len == 0 {
261+
runtimePanic("gc: insert 0-length free range")
262+
}
263+
264+
// Find the insertion point by length.
265+
// Skip until the next range is at least the target length.
266+
insDst := &freeRanges
267+
for *insDst != nil && (*insDst).len < len {
268+
insDst = &(*insDst).nextLen
269+
}
270+
271+
// Create the new free range.
272+
next := *insDst
273+
if next != nil && next.len == len {
274+
// Insert into the list with this length.
275+
newRange := (*freeRangeMore)(ptr)
276+
newRange.next = next.nextWithLen
277+
next.nextWithLen = newRange
278+
} else {
279+
// Insert into the list of lengths.
280+
newRange := (*freeRange)(ptr)
281+
*newRange = freeRange{
282+
len: len,
283+
nextLen: next,
284+
nextWithLen: nil,
285+
}
286+
*insDst = newRange
287+
}
288+
}
289+
290+
// popFreeRange removes a range of len blocks from the freeRanges list.
291+
// It returns nil if there are no sufficiently long ranges.
292+
func popFreeRange(len uintptr) unsafe.Pointer {
293+
if gcAsserts && len == 0 {
294+
runtimePanic("gc: pop 0-length free range")
295+
}
296+
297+
// Find the removal point by length.
298+
// Skip until the next range is at least the target length.
299+
remDst := &freeRanges
300+
for *remDst != nil && (*remDst).len < len {
301+
remDst = &(*remDst).nextLen
302+
}
303+
304+
rangeWithLength := *remDst
305+
if rangeWithLength == nil {
306+
// No ranges are long enough.
307+
return nil
308+
}
309+
removedLen := rangeWithLength.len
310+
311+
// Remove the range.
312+
var ptr unsafe.Pointer
313+
if nextWithLen := rangeWithLength.nextWithLen; nextWithLen != nil {
314+
// Remove from the list with this length.
315+
rangeWithLength.nextWithLen = nextWithLen.next
316+
ptr = unsafe.Pointer(nextWithLen)
317+
} else {
318+
// Remove from the list of lengths.
319+
*remDst = rangeWithLength.nextLen
320+
ptr = unsafe.Pointer(rangeWithLength)
321+
}
322+
323+
if removedLen > len {
324+
// Insert the leftover range.
325+
insertFreeRange(unsafe.Add(ptr, len*bytesPerBlock), removedLen-len)
326+
}
327+
return ptr
328+
}
329+
237330
func isOnHeap(ptr uintptr) bool {
238331
return ptr >= heapStart && ptr < uintptr(metadataStart)
239332
}
@@ -248,6 +341,9 @@ func initHeap() {
248341
// Set all block states to 'free'.
249342
metadataSize := heapEnd - uintptr(metadataStart)
250343
memzero(unsafe.Pointer(metadataStart), metadataSize)
344+
345+
// Rebuild the free ranges list.
346+
buildFreeRanges()
251347
}
252348

253349
// setHeapEnd is called to expand the heap. The heap can only grow, not shrink.
@@ -279,6 +375,9 @@ func setHeapEnd(newHeapEnd uintptr) {
279375
if gcAsserts && uintptr(metadataStart) < uintptr(oldMetadataStart)+oldMetadataSize {
280376
runtimePanic("gc: heap did not grow enough at once")
281377
}
378+
379+
// Rebuild the free ranges list.
380+
buildFreeRanges()
282381
}
283382

284383
// calculateHeapAddresses initializes variables such as metadataStart and
@@ -344,98 +443,65 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
344443
gcMallocs++
345444
gcTotalBlocks += uint64(neededBlocks)
346445

347-
// Continue looping until a run of free blocks has been found that fits the
348-
// requested size.
349-
index := nextAlloc
350-
numFreeBlocks := uintptr(0)
351-
heapScanCount := uint8(0)
446+
// Acquire a range of free blocks.
447+
var ranGC bool
448+
var grewHeap bool
449+
var pointer unsafe.Pointer
352450
for {
353-
if index == nextAlloc {
354-
if heapScanCount == 0 {
355-
heapScanCount = 1
356-
} else if heapScanCount == 1 {
357-
// The entire heap has been searched for free memory, but none
358-
// could be found. Run a garbage collection cycle to reclaim
359-
// free memory and try again.
360-
heapScanCount = 2
361-
freeBytes := runGC()
362-
heapSize := uintptr(metadataStart) - heapStart
363-
if freeBytes < heapSize/3 {
364-
// Ensure there is at least 33% headroom.
365-
// This percentage was arbitrarily chosen, and may need to
366-
// be tuned in the future.
367-
growHeap()
368-
}
369-
} else {
370-
// Even after garbage collection, no free memory could be found.
371-
// Try to increase heap size.
372-
if growHeap() {
373-
// Success, the heap was increased in size. Try again with a
374-
// larger heap.
375-
} else {
376-
// Unfortunately the heap could not be increased. This
377-
// happens on baremetal systems for example (where all
378-
// available RAM has already been dedicated to the heap).
379-
runtimePanicAt(returnAddress(0), "out of memory")
380-
}
381-
}
451+
pointer = popFreeRange(neededBlocks)
452+
if pointer != nil {
453+
break
382454
}
383455

384-
// Wrap around the end of the heap.
385-
if index == endBlock {
386-
index = 0
387-
// Reset numFreeBlocks as allocations cannot wrap.
388-
numFreeBlocks = 0
389-
// In rare cases, the initial heap might be so small that there are
390-
// no blocks at all. In this case, it's better to jump back to the
391-
// start of the loop and try again, until the GC realizes there is
392-
// no memory and grows the heap.
393-
// This can sometimes happen on WebAssembly, where the initial heap
394-
// is created by whatever is left on the last memory page.
456+
if !ranGC {
457+
// Run the collector and try again.
458+
freeBytes := runGC()
459+
ranGC = true
460+
heapSize := uintptr(metadataStart) - heapStart
461+
if freeBytes < heapSize/3 {
462+
// Ensure there is at least 33% headroom.
463+
// This percentage was arbitrarily chosen, and may need to
464+
// be tuned in the future.
465+
growHeap()
466+
}
395467
continue
396468
}
397469

398-
// Is the block we're looking at free?
399-
if index.state() != blockStateFree {
400-
// This block is in use. Try again from this point.
401-
numFreeBlocks = 0
402-
index++
470+
if gcDebug && !grewHeap {
471+
println("grow heap for request:", uint(neededBlocks))
472+
dumpFreeRangeCounts()
473+
}
474+
if growHeap() {
475+
grewHeap = true
403476
continue
404477
}
405-
numFreeBlocks++
406-
index++
407-
408-
// Are we finished?
409-
if numFreeBlocks == neededBlocks {
410-
// Found a big enough range of free blocks!
411-
nextAlloc = index
412-
thisAlloc := index - gcBlock(neededBlocks)
413-
if gcDebug {
414-
println("found memory:", thisAlloc.pointer(), int(size))
415-
}
416478

417-
// Set the following blocks as being allocated.
418-
thisAlloc.setState(blockStateHead)
419-
for i := thisAlloc + 1; i != nextAlloc; i++ {
420-
i.setState(blockStateTail)
421-
}
479+
// Unfortunately the heap could not be increased. This
480+
// happens on baremetal systems for example (where all
481+
// available RAM has already been dedicated to the heap).
482+
runtimePanicAt(returnAddress(0), "out of memory")
483+
}
422484

423-
// Create the object header.
424-
pointer := thisAlloc.pointer()
425-
header := (*objHeader)(pointer)
426-
header.layout = parseGCLayout(layout)
485+
// Set the backing blocks as being allocated.
486+
block := blockFromAddr(uintptr(pointer))
487+
block.setState(blockStateHead)
488+
for i := block + 1; i != block+gcBlock(neededBlocks); i++ {
489+
i.setState(blockStateTail)
490+
}
427491

428-
// We've claimed this allocation, now we can unlock the heap.
429-
gcLock.Unlock()
492+
// Create the object header.
493+
header := (*objHeader)(pointer)
494+
header.layout = parseGCLayout(layout)
430495

431-
// Return a pointer to this allocation.
432-
add := align(unsafe.Sizeof(objHeader{}))
433-
pointer = unsafe.Add(pointer, add)
434-
size -= add
435-
memzero(pointer, size)
436-
return pointer
437-
}
438-
}
496+
// We've claimed this allocation, now we can unlock the heap.
497+
gcLock.Unlock()
498+
499+
// Return a pointer to this allocation.
500+
add := align(unsafe.Sizeof(objHeader{}))
501+
pointer = unsafe.Add(pointer, add)
502+
size -= add
503+
memzero(pointer, size)
504+
return pointer
439505
}
440506

441507
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
@@ -522,6 +588,9 @@ func runGC() (freeBytes uintptr) {
522588
// the next collection cycle.
523589
freeBytes = sweep()
524590

591+
// Rebuild the free ranges list.
592+
buildFreeRanges()
593+
525594
// Show how much has been sweeped, for debugging.
526595
if gcDebug {
527596
dumpHeap()
@@ -665,6 +734,46 @@ func sweep() (freeBytes uintptr) {
665734
return
666735
}
667736

737+
// buildFreeRanges rebuilds the freeRanges list.
738+
// This must be called after a GC sweep or heap grow.
739+
func buildFreeRanges() {
740+
freeRanges = nil
741+
block := endBlock
742+
for {
743+
// Skip backwards over occupied blocks.
744+
for block > 0 && (block-1).state() != blockStateFree {
745+
block--
746+
}
747+
if block == 0 {
748+
break
749+
}
750+
751+
// Find the start of the free range.
752+
end := block
753+
for block > 0 && (block-1).state() == blockStateFree {
754+
block--
755+
}
756+
757+
// Insert the free range.
758+
insertFreeRange(block.pointer(), uintptr(end-block))
759+
}
760+
761+
if gcDebug {
762+
println("free ranges after rebuild:")
763+
dumpFreeRangeCounts()
764+
}
765+
}
766+
767+
func dumpFreeRangeCounts() {
768+
for rangeWithLength := freeRanges; rangeWithLength != nil; rangeWithLength = rangeWithLength.nextLen {
769+
totalRanges := uintptr(1)
770+
for nextWithLen := rangeWithLength.nextWithLen; nextWithLen != nil; nextWithLen = nextWithLen.next {
771+
totalRanges++
772+
}
773+
println("-", uint(rangeWithLength.len), "x", uint(totalRanges))
774+
}
775+
}
776+
668777
// dumpHeap can be used for debugging purposes. It dumps the state of each heap
669778
// block to standard output.
670779
func dumpHeap() {

0 commit comments

Comments
 (0)