@@ -51,7 +51,7 @@ const (
5151var (
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+
237330func 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
441507func 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.
670779func dumpHeap () {
0 commit comments