41
41
#define DEBUG_printf (...) (void)0
42
42
#endif
43
43
44
+ // Uncomment this if you want to use a debugger to capture state at every allocation and free.
45
+ // #define LOG_HEAP_ACTIVITY 1
46
+
44
47
// make this 1 to dump the heap each time it changes
45
48
#define EXTENSIVE_HEAP_PROFILING (0)
46
49
59
62
#define AT_MARK (3)
60
63
61
64
#define BLOCKS_PER_ATB (4)
62
- #define ATB_MASK_0 (0x03)
63
- #define ATB_MASK_1 (0x0c)
64
- #define ATB_MASK_2 (0x30)
65
- #define ATB_MASK_3 (0xc0)
66
-
67
- #define ATB_0_IS_FREE (a ) (((a) & ATB_MASK_0) == 0)
68
- #define ATB_1_IS_FREE (a ) (((a) & ATB_MASK_1) == 0)
69
- #define ATB_2_IS_FREE (a ) (((a) & ATB_MASK_2) == 0)
70
- #define ATB_3_IS_FREE (a ) (((a) & ATB_MASK_3) == 0)
71
65
72
66
#define BLOCK_SHIFT (block ) (2 * ((block) & (BLOCKS_PER_ATB - 1)))
73
67
#define ATB_GET_KIND (block ) ((MP_STATE_MEM(gc_alloc_table_start)[(block) / BLOCKS_PER_ATB] >> BLOCK_SHIFT(block)) & 3)
@@ -152,14 +146,19 @@ void gc_init(void *start, void *end) {
152
146
memset (MP_STATE_MEM (gc_finaliser_table_start ), 0 , gc_finaliser_table_byte_len );
153
147
#endif
154
148
155
- // set last free ATB index to start of heap
149
+ // Set first free ATB index to the start of the heap.
156
150
MP_STATE_MEM (gc_last_free_atb_index ) = 0 ;
151
+ // Set last free ATB index to the end of the heap.
152
+ MP_STATE_MEM (gc_last_free_atb_index ) = MP_STATE_MEM (gc_alloc_table_byte_len ) - 1 ;
153
+ // Set the lowest long lived ptr to the end of the heap to start. This will be lowered as long
154
+ // lived objects are allocated.
155
+ MP_STATE_MEM (gc_lowest_long_lived_ptr ) = (void * ) PTR_FROM_BLOCK (MP_STATE_MEM (gc_alloc_table_byte_len * BLOCKS_PER_ATB ));
157
156
158
157
// unlock the GC
159
158
MP_STATE_MEM (gc_lock_depth ) = 0 ;
160
159
161
160
// allow auto collection
162
- MP_STATE_MEM (gc_auto_collect_enabled ) = 1 ;
161
+ MP_STATE_MEM (gc_auto_collect_enabled ) = true ;
163
162
164
163
#if MICROPY_GC_ALLOC_THRESHOLD
165
164
// by default, maxuint for gc threshold, effectively turning gc-by-threshold off
@@ -288,6 +287,7 @@ STATIC void gc_sweep(void) {
288
287
}
289
288
#endif
290
289
free_tail = 1 ;
290
+ ATB_ANY_TO_FREE (block );
291
291
DEBUG_printf ("gc_sweep(%x)\n" , PTR_FROM_BLOCK (block ));
292
292
293
293
#ifdef LOG_HEAP_ACTIVITY
@@ -296,7 +296,7 @@ STATIC void gc_sweep(void) {
296
296
#if MICROPY_PY_GC_COLLECT_RETVAL
297
297
MP_STATE_MEM (gc_collected )++ ;
298
298
#endif
299
- // fall through to free the head
299
+ break ;
300
300
301
301
case AT_TAIL :
302
302
if (free_tail ) {
@@ -338,7 +338,8 @@ void gc_collect_root(void **ptrs, size_t len) {
338
338
void gc_collect_end (void ) {
339
339
gc_deal_with_stack_overflow ();
340
340
gc_sweep ();
341
- MP_STATE_MEM (gc_last_free_atb_index ) = 0 ;
341
+ MP_STATE_MEM (gc_first_free_atb_index ) = 0 ;
342
+ MP_STATE_MEM (gc_last_free_atb_index ) = MP_STATE_MEM (gc_alloc_table_byte_len ) - 1 ;
342
343
MP_STATE_MEM (gc_lock_depth )-- ;
343
344
GC_EXIT ();
344
345
}
@@ -407,7 +408,9 @@ void gc_info(gc_info_t *info) {
407
408
GC_EXIT ();
408
409
}
409
410
410
- void * gc_alloc (size_t n_bytes , bool has_finaliser ) {
411
+ // We place long lived objects at the end of the heap rather than the start. This reduces
412
+ // fragmentation by localizing the heap churn to one portion of memory (the start of the heap.)
413
+ void * gc_alloc (size_t n_bytes , bool has_finaliser , bool long_lived ) {
411
414
size_t n_blocks = ((n_bytes + BYTES_PER_BLOCK - 1 ) & (~(BYTES_PER_BLOCK - 1 ))) / BYTES_PER_BLOCK ;
412
415
DEBUG_printf ("gc_alloc(" UINT_FMT " bytes -> " UINT_FMT " blocks)\n" , n_bytes , n_blocks );
413
416
@@ -424,29 +427,62 @@ void *gc_alloc(size_t n_bytes, bool has_finaliser) {
424
427
return NULL ;
425
428
}
426
429
427
- size_t i ;
430
+ size_t found_block = 0xffffffff ;
428
431
size_t end_block ;
429
432
size_t start_block ;
430
- size_t n_free = 0 ;
431
- int collected = !MP_STATE_MEM (gc_auto_collect_enabled );
433
+ size_t n_free ;
434
+ bool collected = !MP_STATE_MEM (gc_auto_collect_enabled );
432
435
433
436
#if MICROPY_GC_ALLOC_THRESHOLD
434
437
if (!collected && MP_STATE_MEM (gc_alloc_amount ) >= MP_STATE_MEM (gc_alloc_threshold )) {
435
438
GC_EXIT ();
436
439
gc_collect ();
437
440
GC_ENTER ();
441
+ collected = true;
438
442
}
439
443
#endif
440
444
441
- for (;;) {
442
-
445
+ bool keep_looking = true;
446
+
447
+ // When we start searching on the other side of the crossover block we make sure to
448
+ // perform a collect. That way we'll get the closest free block in our section.
449
+ size_t crossover_block = BLOCK_FROM_PTR (MP_STATE_MEM (gc_lowest_long_lived_ptr ));
450
+ while (keep_looking ) {
451
+ int8_t direction = 1 ;
452
+ size_t start = MP_STATE_MEM (gc_first_free_atb_index );
453
+ if (long_lived ) {
454
+ direction = -1 ;
455
+ start = MP_STATE_MEM (gc_last_free_atb_index );
456
+ }
457
+ n_free = 0 ;
443
458
// look for a run of n_blocks available blocks
444
- for (i = MP_STATE_MEM (gc_last_free_atb_index ); i < MP_STATE_MEM (gc_alloc_table_byte_len ); i ++ ) {
459
+ for (size_t i = start ; keep_looking && MP_STATE_MEM (gc_first_free_atb_index ) <= i && i <= MP_STATE_MEM (gc_last_free_atb_index ); i += direction ) {
445
460
byte a = MP_STATE_MEM (gc_alloc_table_start )[i ];
446
- if (ATB_0_IS_FREE (a )) { if (++ n_free >= n_blocks ) { i = i * BLOCKS_PER_ATB + 0 ; goto found ; } } else { n_free = 0 ; }
447
- if (ATB_1_IS_FREE (a )) { if (++ n_free >= n_blocks ) { i = i * BLOCKS_PER_ATB + 1 ; goto found ; } } else { n_free = 0 ; }
448
- if (ATB_2_IS_FREE (a )) { if (++ n_free >= n_blocks ) { i = i * BLOCKS_PER_ATB + 2 ; goto found ; } } else { n_free = 0 ; }
449
- if (ATB_3_IS_FREE (a )) { if (++ n_free >= n_blocks ) { i = i * BLOCKS_PER_ATB + 3 ; goto found ; } } else { n_free = 0 ; }
461
+ // Four ATB states are packed into a single byte.
462
+ int j = 0 ;
463
+ if (direction == -1 ) {
464
+ j = 3 ;
465
+ }
466
+ for (; keep_looking && 0 <= j && j <= 3 ; j += direction ) {
467
+ if ((a & (0x3 << (j * 2 ))) == 0 ) {
468
+ if (++ n_free >= n_blocks ) {
469
+ found_block = i * BLOCKS_PER_ATB + j ;
470
+ keep_looking = false;
471
+ }
472
+ } else {
473
+ if (!collected ) {
474
+ size_t block = i * BLOCKS_PER_ATB + j ;
475
+ if ((direction == 1 && block >= crossover_block ) ||
476
+ (direction == -1 && block < crossover_block )) {
477
+ keep_looking = false;
478
+ }
479
+ }
480
+ n_free = 0 ;
481
+ }
482
+ }
483
+ }
484
+ if (n_free >= n_blocks ) {
485
+ break ;
450
486
}
451
487
452
488
GC_EXIT ();
@@ -456,23 +492,31 @@ void *gc_alloc(size_t n_bytes, bool has_finaliser) {
456
492
}
457
493
DEBUG_printf ("gc_alloc(" UINT_FMT "): no free mem, triggering GC\n" , n_bytes );
458
494
gc_collect ();
459
- collected = 1 ;
495
+ collected = true;
496
+ // Try again since we've hopefully freed up space.
497
+ keep_looking = true;
460
498
GC_ENTER ();
461
499
}
500
+ assert (found_block != 0xffffffff );
462
501
463
- // found, ending at block i inclusive
464
- found :
465
- // get starting and end blocks, both inclusive
466
- end_block = i ;
467
- start_block = i - n_free + 1 ;
468
-
469
- // Set last free ATB index to block after last block we found, for start of
502
+ // Found free space ending at found_block inclusive.
503
+ // Also, set last free ATB index to block after last block we found, for start of
470
504
// next scan. To reduce fragmentation, we only do this if we were looking
471
505
// for a single free block, which guarantees that there are no free blocks
472
- // before this one. Also, whenever we free or shink a block we must check
506
+ // before this one. Also, whenever we free or shrink a block we must check
473
507
// if this index needs adjusting (see gc_realloc and gc_free).
474
- if (n_free == 1 ) {
475
- MP_STATE_MEM (gc_last_free_atb_index ) = (i + 1 ) / BLOCKS_PER_ATB ;
508
+ if (!long_lived ) {
509
+ end_block = found_block ;
510
+ start_block = found_block - n_free + 1 ;
511
+ if (n_blocks == 1 ) {
512
+ MP_STATE_MEM (gc_first_free_atb_index ) = (found_block + 1 ) / BLOCKS_PER_ATB ;
513
+ }
514
+ } else {
515
+ start_block = found_block ;
516
+ end_block = found_block + n_free - 1 ;
517
+ if (n_blocks == 1 ) {
518
+ MP_STATE_MEM (gc_last_free_atb_index ) = (found_block - 1 ) / BLOCKS_PER_ATB ;
519
+ }
476
520
}
477
521
478
522
#ifdef LOG_HEAP_ACTIVITY
@@ -493,6 +537,13 @@ void *gc_alloc(size_t n_bytes, bool has_finaliser) {
493
537
void * ret_ptr = (void * )(MP_STATE_MEM (gc_pool_start ) + start_block * BYTES_PER_BLOCK );
494
538
DEBUG_printf ("gc_alloc(%p)\n" , ret_ptr );
495
539
540
+ // If the allocation was long live then update the lowest value. Its used to trigger early
541
+ // collects when allocations fail in their respective section. Its also used to ignore calls to
542
+ // gc_make_long_lived where the pointer is already in the long lived section.
543
+ if (long_lived && ret_ptr < MP_STATE_MEM (gc_lowest_long_lived_ptr )) {
544
+ MP_STATE_MEM (gc_lowest_long_lived_ptr ) = ret_ptr ;
545
+ }
546
+
496
547
#if MICROPY_GC_ALLOC_THRESHOLD
497
548
MP_STATE_MEM (gc_alloc_amount ) += n_blocks ;
498
549
#endif
@@ -566,7 +617,10 @@ void gc_free(void *ptr) {
566
617
#endif
567
618
568
619
// set the last_free pointer to this block if it's earlier in the heap
569
- if (block / BLOCKS_PER_ATB < MP_STATE_MEM (gc_last_free_atb_index )) {
620
+ if (block / BLOCKS_PER_ATB < MP_STATE_MEM (gc_first_free_atb_index )) {
621
+ MP_STATE_MEM (gc_first_free_atb_index ) = block / BLOCKS_PER_ATB ;
622
+ }
623
+ if (block / BLOCKS_PER_ATB > MP_STATE_MEM (gc_last_free_atb_index )) {
570
624
MP_STATE_MEM (gc_last_free_atb_index ) = block / BLOCKS_PER_ATB ;
571
625
}
572
626
@@ -607,6 +661,50 @@ size_t gc_nbytes(const void *ptr) {
607
661
return 0 ;
608
662
}
609
663
664
+ bool gc_has_finaliser (const void * ptr ) {
665
+ #if MICROPY_ENABLE_FINALISER
666
+ GC_ENTER ();
667
+ if (VERIFY_PTR (ptr )) {
668
+ bool has_finaliser = FTB_GET (BLOCK_FROM_PTR (ptr ));
669
+ GC_EXIT ();
670
+ return has_finaliser ;
671
+ }
672
+
673
+ // invalid pointer
674
+ GC_EXIT ();
675
+ #else
676
+ (void ) ptr ;
677
+ #endif
678
+ return false;
679
+ }
680
+
681
+ void * gc_make_long_lived (void * old_ptr ) {
682
+ // If its already in the long lived section then don't bother moving it.
683
+ if (old_ptr >= MP_STATE_MEM (gc_lowest_long_lived_ptr )) {
684
+ return old_ptr ;
685
+ }
686
+ size_t n_bytes = gc_nbytes (old_ptr );
687
+ if (n_bytes == 0 ) {
688
+ return old_ptr ;
689
+ }
690
+ bool has_finaliser = gc_has_finaliser (old_ptr );
691
+
692
+ // Try and find a new area in the long lived section to copy the memory to.
693
+ void * new_ptr = gc_alloc (n_bytes , has_finaliser , true);
694
+ if (new_ptr == NULL ) {
695
+ return old_ptr ;
696
+ } else if (old_ptr > new_ptr ) {
697
+ // Return the old pointer if the new one is lower in the heap and free the new space.
698
+ gc_free (new_ptr );
699
+ return old_ptr ;
700
+ }
701
+ // We copy everything over and let the garbage collection process delete the old copy. That way
702
+ // we ensure we don't delete memory that has a second reference. (Though if there is we may
703
+ // confuse things when its mutable.)
704
+ memcpy (new_ptr , old_ptr , n_bytes );
705
+ return new_ptr ;
706
+ }
707
+
610
708
#if 0
611
709
// old, simple realloc that didn't expand memory in place
612
710
void * gc_realloc (void * ptr , mp_uint_t n_bytes ) {
@@ -639,7 +737,7 @@ void *gc_realloc(void *ptr, mp_uint_t n_bytes) {
639
737
void * gc_realloc (void * ptr_in , size_t n_bytes , bool allow_move ) {
640
738
// check for pure allocation
641
739
if (ptr_in == NULL ) {
642
- return gc_alloc (n_bytes , false);
740
+ return gc_alloc (n_bytes , false, false );
643
741
}
644
742
645
743
// check for pure free
@@ -714,7 +812,10 @@ void *gc_realloc(void *ptr_in, size_t n_bytes, bool allow_move) {
714
812
}
715
813
716
814
// set the last_free pointer to end of this block if it's earlier in the heap
717
- if ((block + new_blocks ) / BLOCKS_PER_ATB < MP_STATE_MEM (gc_last_free_atb_index )) {
815
+ if ((block + new_blocks ) / BLOCKS_PER_ATB < MP_STATE_MEM (gc_first_free_atb_index )) {
816
+ MP_STATE_MEM (gc_first_free_atb_index ) = (block + new_blocks ) / BLOCKS_PER_ATB ;
817
+ }
818
+ if ((block + new_blocks ) / BLOCKS_PER_ATB > MP_STATE_MEM (gc_last_free_atb_index )) {
718
819
MP_STATE_MEM (gc_last_free_atb_index ) = (block + new_blocks ) / BLOCKS_PER_ATB ;
719
820
}
720
821
@@ -774,7 +875,7 @@ void *gc_realloc(void *ptr_in, size_t n_bytes, bool allow_move) {
774
875
}
775
876
776
877
// can't resize inplace; try to find a new contiguous chain
777
- void * ptr_out = gc_alloc (n_bytes , ftb_state );
878
+ void * ptr_out = gc_alloc (n_bytes , ftb_state , false );
778
879
779
880
// check that the alloc succeeded
780
881
if (ptr_out == NULL ) {
0 commit comments