Skip to content

Commit ce7ffc0

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 20e22d4 commit ce7ffc0

File tree

2 files changed

+196
-87
lines changed

2 files changed

+196
-87
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", 3896, 280, 0, 2268},
46-
{"microbit", "examples/serial", 2860, 360, 8, 2272},
47-
{"wioterminal", "examples/pininterrupt", 7361, 1491, 116, 6912},
45+
{"hifive1b", "examples/echo", 4132, 280, 0, 2268},
46+
{"microbit", "examples/serial", 3024, 360, 8, 2272},
47+
{"wioterminal", "examples/pininterrupt", 7537, 1491, 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: 193 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const (
5151

5252
var (
5353
metadataStart unsafe.Pointer // pointer to the start of the heap metadata
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
@@ -225,6 +225,99 @@ func (b gcBlock) unmark() {
225225
}
226226
}
227227

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

244340
// setHeapEnd is called to expand the heap. The heap can only grow, not shrink.
@@ -270,6 +366,9 @@ func setHeapEnd(newHeapEnd uintptr) {
270366
if gcAsserts && uintptr(metadataStart) < uintptr(oldMetadataStart)+oldMetadataSize {
271367
runtimePanic("gc: heap did not grow enough at once")
272368
}
369+
370+
// Rebuild the free ranges list.
371+
buildFreeRanges()
273372
}
274373

275374
// calculateHeapAddresses initializes variables such as metadataStart and
@@ -338,100 +437,67 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
338437
gcMallocs++
339438
gcTotalBlocks += uint64(neededBlocks)
340439

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

378-
// Wrap around the end of the heap.
379-
if index == endBlock {
380-
index = 0
381-
// Reset numFreeBlocks as allocations cannot wrap.
382-
numFreeBlocks = 0
383-
// In rare cases, the initial heap might be so small that there are
384-
// no blocks at all. In this case, it's better to jump back to the
385-
// start of the loop and try again, until the GC realizes there is
386-
// no memory and grows the heap.
387-
// This can sometimes happen on WebAssembly, where the initial heap
388-
// is created by whatever is left on the last memory page.
450+
if !ranGC {
451+
// Run the collector and try again.
452+
freeBytes := runGC()
453+
ranGC = true
454+
heapSize := uintptr(metadataStart) - heapStart
455+
if freeBytes < heapSize/3 {
456+
// Ensure there is at least 33% headroom.
457+
// This percentage was arbitrarily chosen, and may need to
458+
// be tuned in the future.
459+
growHeap()
460+
}
389461
continue
390462
}
391463

392-
// Is the block we're looking at free?
393-
if index.state() != blockStateFree {
394-
// This block is in use. Try again from this point.
395-
numFreeBlocks = 0
396-
index++
464+
if gcDebug && !grewHeap {
465+
println("grow heap for request:", uint(neededBlocks))
466+
dumpFreeRangeCounts()
467+
}
468+
if growHeap() {
469+
grewHeap = true
397470
continue
398471
}
399-
numFreeBlocks++
400-
index++
401-
402-
// Are we finished?
403-
if numFreeBlocks == neededBlocks {
404-
// Found a big enough range of free blocks!
405-
nextAlloc = index
406-
thisAlloc := index - gcBlock(neededBlocks)
407-
if gcDebug {
408-
println("found memory:", thisAlloc.pointer(), int(size))
409-
}
410472

411-
// Set the following blocks as being allocated.
412-
thisAlloc.setState(blockStateHead)
413-
for i := thisAlloc + 1; i != nextAlloc; i++ {
414-
i.setState(blockStateTail)
415-
}
473+
// Unfortunately the heap could not be increased. This
474+
// happens on baremetal systems for example (where all
475+
// available RAM has already been dedicated to the heap).
476+
runtimePanicAt(returnAddress(0), "out of memory")
477+
}
416478

417-
// We've claimed this allocation, now we can unlock the heap.
418-
gcLock.Unlock()
419-
420-
// Return a pointer to this allocation.
421-
pointer := thisAlloc.pointer()
422-
if preciseHeap {
423-
// Store the object layout at the start of the object.
424-
// TODO: this wastes a little bit of space on systems with
425-
// larger-than-pointer alignment requirements.
426-
*(*unsafe.Pointer)(pointer) = layout
427-
add := align(unsafe.Sizeof(layout))
428-
pointer = unsafe.Add(pointer, add)
429-
size -= add
430-
}
431-
memzero(pointer, size)
432-
return pointer
433-
}
479+
// Set the backing blocks as being allocated.
480+
block := blockFromAddr(uintptr(pointer))
481+
block.setState(blockStateHead)
482+
for i := block + 1; i != block+gcBlock(neededBlocks); i++ {
483+
i.setState(blockStateTail)
434484
}
485+
486+
// We've claimed this allocation, now we can unlock the heap.
487+
gcLock.Unlock()
488+
489+
// Return a pointer to this allocation.
490+
if preciseHeap {
491+
// Store the object layout at the start of the object.
492+
// TODO: this wastes a little bit of space on systems with
493+
// larger-than-pointer alignment requirements.
494+
*(*unsafe.Pointer)(pointer) = layout
495+
add := align(unsafe.Sizeof(layout))
496+
pointer = unsafe.Add(pointer, add)
497+
size -= add
498+
}
499+
memzero(pointer, size)
500+
return pointer
435501
}
436502

437503
func realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
@@ -518,6 +584,9 @@ func runGC() (freeBytes uintptr) {
518584
// the next collection cycle.
519585
freeBytes = sweep()
520586

587+
// Rebuild the free ranges list.
588+
buildFreeRanges()
589+
521590
// Show how much has been sweeped, for debugging.
522591
if gcDebug {
523592
dumpHeap()
@@ -717,6 +786,46 @@ func sweep() (freeBytes uintptr) {
717786
return
718787
}
719788

789+
// buildFreeRanges rebuilds the freeRanges list.
790+
// This must be called after a GC sweep or heap grow.
791+
func buildFreeRanges() {
792+
freeRanges = nil
793+
block := endBlock
794+
for {
795+
// Skip backwards over occupied blocks.
796+
for block > 0 && (block-1).state() != blockStateFree {
797+
block--
798+
}
799+
if block == 0 {
800+
break
801+
}
802+
803+
// Find the start of the free range.
804+
end := block
805+
for block > 0 && (block-1).state() == blockStateFree {
806+
block--
807+
}
808+
809+
// Insert the free range.
810+
insertFreeRange(block.pointer(), uintptr(end-block))
811+
}
812+
813+
if gcDebug {
814+
println("free ranges after rebuild:")
815+
dumpFreeRangeCounts()
816+
}
817+
}
818+
819+
func dumpFreeRangeCounts() {
820+
for rangeWithLength := freeRanges; rangeWithLength != nil; rangeWithLength = rangeWithLength.nextLen {
821+
totalRanges := uintptr(1)
822+
for nextWithLen := rangeWithLength.nextWithLen; nextWithLen != nil; nextWithLen = nextWithLen.next {
823+
totalRanges++
824+
}
825+
println("-", uint(rangeWithLength.len), "x", uint(totalRanges))
826+
}
827+
}
828+
720829
// dumpHeap can be used for debugging purposes. It dumps the state of each heap
721830
// block to standard output.
722831
func dumpHeap() {

0 commit comments

Comments
 (0)