Skip to content

Surprising performance hit in mi_page_free_list_extend when allowing THP #1373

Description

@timo

TL;DR: This may just be the expected result of working with THP, that you pay for the management and receive benefits over the long run, but I was still rather surprised.

All of this is with v3.4.4 of mimalloc

Benchmark 1: env MVM_SPESH_BLOCKING=0 MIMALLOC_ALLOW_THP=0 rakudo -ne 'BEGIN my %h; if /\s+(\d+)/ { %h{$0}++; }; last if $++ > 1000; END say +%h.keys' gen/moar/CORE.*setting
  Time (mean ± σ):     230.1 ms ±   3.8 ms    [User: 164.3 ms, System: 62.3 ms]
  Range (min … max):   223.8 ms … 237.1 ms    20 runs
 
Benchmark 2: env MVM_SPESH_BLOCKING=0 MIMALLOC_ALLOW_THP=1 rakudo -ne 'BEGIN my %h; if /\s+(\d+)/ { %h{$0}++; }; last if $++ > 1000; END say +%h.keys' gen/moar/CORE.*setting
  Time (mean ± σ):     439.5 ms ±  35.2 ms    [User: 164.9 ms, System: 270.2 ms]
  Range (min … max):   360.0 ms … 490.9 ms    20 runs
 
Summary
  env MVM_SPESH_BLOCKING=0 MIMALLOC_ALLOW_THP=0 rakudo -ne 'BEGIN my %h; if /\s+(\d+)/ { %h{$0}++; }; last if $++ > 1000; END say +%h.keys' gen/moar/CORE.*setting ran
    1.91 ± 0.16 times faster than env MVM_SPESH_BLOCKING=0 MIMALLOC_ALLOW_THP=1 rakudo -ne 'BEGIN my %h; if /\s+(\d+)/ { %h{$0}++; }; last if $++ > 1000; END say +%h.keys' gen/moar/CORE.*setting

You can see the disparity in system time is about 190 milliseconds and the range of wall-clock time is 14ms wide with THP turned off, and 130ms wide with THP turned on.

I made a perf recording of my program once with THP allowed and once with THP disallowed with this command:

env MVM_SPESH_BLOCKING=0 MIMALLOC_ALLOW_THP=1 perf record --user-callchains -g --call-graph fp -F 20000 rakudo -ne 'BEGIN my %h; if /\s+(\d+)/ { %h{$0}++; }; last if $++ > 1000; END say +%h.keys' gen/moar/CORE.*setting

The results in the report are rather large, so I collapsed them. Takeaway is that with THP on it recorded 9k samples for the same workload that recorded 4k samples without THP.

Results of a perf recording with THP disallowed and one with THP allowed:

command for the reports: perf report --stdio --parent "mi_page_queue_find_free_ex" --exclude-other --sort=parent

# Total Lost Samples: 0
#
# Samples: 4K of event 'cpu/cycles/P'
# Event count (approx.): 1047552711
#
# Children      Self  Parent symbol                         
# ........  ........  ......................................
#
     7.54%     7.54%  mi_page_queue_find_free_ex.constprop.0
            |          
            |--5.73%--0x6d2e366c7265702f
            |          main
            |          |          
            |          |--5.08%--MVM_interp_run
            |          |          |          
            |          |          |--1.29%--MVM_6model_get_how
            |          |          |          MVM_6model_get_how
            |          |          |          MVM_gc_write_barrier (inlined)
            |          |          |          MVM_serialization_demand_object
            |          |          |          work_loop
            |          |          |          deserialize_object (inlined)
            |          |          |          |          
            |          |          |           --1.20%--deserialize
            |          |          |                     |          
            |          |          |                      --0.76%--MVM_gc_write_barrier (inlined)
            |          |          |                                |          
            |          |          |                                 --0.67%--MVM_serialization_read_ref
            |          |          |                                           MVM_gc_allocate_object
            |          |          |                                           MVM_gc_gen2_allocate_zeroed
            |          |          |                                           MVM_gc_gen2_allocate
            |          |          |                                           add_page (inlined)
            |          |          |                                           MVM_malloc (inlined)
            |          |          |                                           _mi_malloc_generic
            |          |          |                                           mi_page_queue_find_free_ex (inlined)
            |          |          |                                           |          
            |          |          |                                            --0.53%--mi_page_free_list_extend
            |          |          |                                                      |          
            |          |          |                                                       --0.51%--mi_block_set_next (inlined)
            |          |          |                                                                 mi_block_set_nextx (inlined)
            |          |          |          
            |          |           --1.00%--MVM_serialization_deserialize
            |          |                     |          
            |          |                      --0.87%--repossess
            |          |                                |          
            |          |                                 --0.80%--MVM_serialization_demand_stable
            |          |                                           MVM_gc_root_temp_pop (inlined)
            |          |                                           read_param_intern
            |          |                                           |          
            |          |                                            --0.70%--MVM_gc_allocate_gen2_default_set (inlined)
            |          |                                                      MVM_serialization_demand_object
            |          |                                                      work_loop
            |          |                                                      |          
            |          |                                                       --0.51%--deserialize_object (inlined)
[...]

and here the same setup but with THP allowed

# Total Lost Samples: 0
#
# Samples: 9K of event 'cpu/cycles/P'
# Event count (approx.): 2347591455
#
# Children      Self  Parent symbol                         
# ........  ........  ......................................
#
    47.52%    47.52%  mi_page_queue_find_free_ex.constprop.0
            |          
            |--45.05%--0x6d2e366c7265702f
            |          main
            |          |          
            |          |--43.79%--MVM_interp_run
            |          |          |          
            |          |          |--31.71%--MVM_load_bytecode
            |          |          |          MVM_cu_map_from_file
            |          |          |          MVM_cu_from_bytes
            |          |          |          MVM_bytecode_unpack
            |          |          |          |          
            |          |          |          |--30.13%--deserialize_frames (inlined)
            |          |          |          |          |          
            |          |          |          |          |--29.49%--MVM_repr_alloc_init
            |          |          |          |          |          MVM_gc_allocate_object
            |          |          |          |          |          MVM_gc_gen2_allocate_zeroed
            |          |          |          |          |          MVM_gc_gen2_allocate
            |          |          |          |          |          add_page (inlined)
            |          |          |          |          |          MVM_malloc (inlined)
            |          |          |          |          |          _mi_malloc_generic
            |          |          |          |          |          mi_page_queue_find_free_ex (inlined)
            |          |          |          |          |          |          
            |          |          |          |          |           --29.45%--mi_page_fresh_alloc
            |          |          |          |          |                     mi_arenas_page_regular_alloc
            |          |          |          |          |                     mi_arenas_page_regular_alloc
            |          |          |          |          |                     mi_page_free_list_extend
            |          |          |          |          |                     mi_block_set_next (inlined)
            |          |          |          |          |                     mi_block_set_nextx (inlined)
            |          |          |          |          |          
            |          |          |          |           --0.60%--MVM_malloc (inlined)
            |          |          |          |                     _mi_malloc_generic
            |          |          |          |                     mi_page_queue_find_free_ex (inlined)
            |          |          |          |                     mi_page_fresh_alloc
            |          |          |          |                     mi_arenas_page_regular_alloc
            |          |          |          |                     mi_arenas_page_regular_alloc
            |          |          |          |                     mi_page_free_list_extend
            |          |          |          |                     mi_block_set_next (inlined)
            |          |          |          |                     mi_block_set_nextx (inlined)
[...]

I only noticed this because there was a suspiciously long period on the bytecode specializer thread in MoarVM when it did its first batch of work, and for that period, basically 90% of samples were under mi_theap_realloc_zero going all the way into mi_block_set_nextx which had all of the self samples; mi_block_set_nextx is highlighted in darker yellow in the following two pictures:

Image

and there were similar measurements the next time it worked:

Image

these were recorded with the bytecode specializer thread in "blocking" mode, so that the thread that submitted the work waits for it to complete. that's why the main thread doesn't wake up at all during that time.

The main thread also has a few samples with mi_block_set_nextx but very noticeably less.

It makes sense for the workload in question where the main thread really only works with small arrays and hashes that die young, so memory usage wouldn't grow, whereas the specializer thread would be doing different-sized amounts of work for each bytecode frame it encounters, including potentially quite large temporary arrays.

running the workload with MIMALLOC_VERBOSE=1 MIMALLOC_SHOW_STATS=1 and THP allowed and disallowed. the output looks almost the same, but one has 1 fault to the other's 0, and of course the time taken for the THP allowed one is a bit longer by roughly 200ms system time

allowed:

mimalloc: process init: 0x7FCA03254000
v3.4.4 (built on Aug 24 2026, 14:57:01)
option 'show_errors': 0 
option 'show_stats': 1 
option 'verbose': 1 
option 'deprecated_eager_commit': 1 
option 'arena_eager_commit': 2 
option 'purge_decommits': 1 
option 'allow_large_os_pages': 0 
option 'reserve_huge_os_pages': 0 
option 'reserve_huge_os_pages_at': -1 
option 'reserve_os_memory': 0 KiB
option 'deprecated_segment_cache': 0 
option 'deprecated_page_reset': 0 
option 'deprecated_abandoned_page_purge': 0 
option 'deprecated_segment_reset': 0 
option 'deprecated_eager_commit_delay': 1 
option 'purge_delay': 1000 
option 'use_numa_nodes': 0 
option 'disallow_os_alloc': 0 
option 'os_tag': 100 
option 'max_errors': 32 
option 'max_warnings': 32 
option 'deprecated_max_segment_reclaim': 10 
option 'destroy_on_exit': 0 
option 'arena_reserve': 1048576 KiB
option 'arena_purge_mult': 1 
option 'deprecated_purge_extend_delay': 1 
option 'disallow_arena_alloc': 0 
option 'retry_on_oom': 400 
option 'deprecated_visit_abandoned': 1 
option 'guarded_min': 0 
option 'guarded_max': 1073741824 
option 'guarded_precise': 0 
option 'guarded_sample_rate': 0 
option 'guarded_sample_seed': 0 
option 'generic_collect': 10000 
option 'page_reclaim_on_free': 0 
option 'page_full_retain': 2 
option 'page_max_candidates': 4 
option 'max_vabits': 0 
option 'pagemap_commit': 0 
option 'page_commit_on_demand': 0 
option 'page_max_reclaim': -1 
option 'page_cross_thread_max_reclaim': 32 
option 'allow_thp': 1 
option 'minimal_purge_size': 0 KiB
option 'arena_max_object_size': 2097152 KiB
option 'arena_is_numa_local': 0 
debug level : 0
secure level: 0
mem tracking: none
mimalloc: reserved 1048576 KiB memory
mimalloc: using 5 numa regions
4
subproc 0
 pages           peak       total     current       block      total#   
  touched   :     0           0           0                                ok
  pages     :   717           1.2 K     700      
  abandoned :   550         837         271      
  reclaima  :     0      
  reclaimf  :   560      
  reabandon :     0      
  waits     :     0      
  extended  :     0      
  retire    :     0      
  searches  :     2.4 avg

 arenas          peak       total     current       block      total#   
  reserved  :     1.0 GiB     1.0 GiB     1.0 GiB                          
  committed :   191.4 MiB   191.4 MiB   191.4 MiB                          
  reset     :     0      
  purged    :     2.1 MiB
  arenas    :     1      
  rollback  :     0      
  mmaps     :     3      
  commits   :     0      
  resets    :     0      
  purges    :    29      
  guarded   :     0      
  theaps    :     1           1           1      
  heaps     :     1           1           1      
  heap waits:     0      

 process         peak       total     current       block      total#   
  threads   :     2           2           2      
  numa nodes:     5
  elapsed   :     0.527 s
  process   : user: 0.185 s, system: 0.256 s, faults: 55, peak rss: 193.7 MiB, peak commit: 191.4 MiB

mimalloc: process done: 0x7FCA03254000

disallowed:

mimalloc: process init: 0x7FD95A8E2000
v3.4.4 (built on Aug 24 2026, 14:57:01)
option 'show_errors': 0 
option 'show_stats': 1 
option 'verbose': 1 
option 'deprecated_eager_commit': 1 
option 'arena_eager_commit': 2 
option 'purge_decommits': 1 
option 'allow_large_os_pages': 0 
option 'reserve_huge_os_pages': 0 
option 'reserve_huge_os_pages_at': -1 
option 'reserve_os_memory': 0 KiB
option 'deprecated_segment_cache': 0 
option 'deprecated_page_reset': 0 
option 'deprecated_abandoned_page_purge': 0 
option 'deprecated_segment_reset': 0 
option 'deprecated_eager_commit_delay': 1 
option 'purge_delay': 1000 
option 'use_numa_nodes': 0 
option 'disallow_os_alloc': 0 
option 'os_tag': 100 
option 'max_errors': 32 
option 'max_warnings': 32 
option 'deprecated_max_segment_reclaim': 10 
option 'destroy_on_exit': 0 
option 'arena_reserve': 1048576 KiB
option 'arena_purge_mult': 1 
option 'deprecated_purge_extend_delay': 1 
option 'disallow_arena_alloc': 0 
option 'retry_on_oom': 400 
option 'deprecated_visit_abandoned': 1 
option 'guarded_min': 0 
option 'guarded_max': 1073741824 
option 'guarded_precise': 0 
option 'guarded_sample_rate': 0 
option 'guarded_sample_seed': 0 
option 'generic_collect': 10000 
option 'page_reclaim_on_free': 0 
option 'page_full_retain': 2 
option 'page_max_candidates': 4 
option 'max_vabits': 0 
option 'pagemap_commit': 0 
option 'page_commit_on_demand': 0 
option 'page_max_reclaim': -1 
option 'page_cross_thread_max_reclaim': 32 
option 'allow_thp': 0 
option 'minimal_purge_size': 0 KiB
option 'arena_max_object_size': 2097152 KiB
option 'arena_is_numa_local': 0 
debug level : 0
secure level: 0
mem tracking: none
mimalloc: reserved 1048576 KiB memory
mimalloc: using 5 numa regions
4
subproc 0
 pages           peak       total     current       block      total#   
  touched   :     0           0           0                                ok
  pages     :   717           1.2 K     702      
  abandoned :   550         839         275      
  reclaima  :     0      
  reclaimf  :   558      
  reabandon :     0      
  waits     :     0      
  extended  :     0      
  retire    :     0      
  searches  :     2.4 avg

 arenas          peak       total     current       block      total#   
  reserved  :     1.0 GiB     1.0 GiB     1.0 GiB                          
  committed :   191.5 MiB   191.5 MiB   191.5 MiB                          
  reset     :     0      
  purged    :     2.6 MiB
  arenas    :     1      
  rollback  :     0      
  mmaps     :     4      
  commits   :     0      
  resets    :     0      
  purges    :    37      
  guarded   :     0      
  theaps    :     1           1           1      
  heaps     :     1           1           1      
  heap waits:     0      

 process         peak       total     current       block      total#   
  threads   :     2           2           2      
  numa nodes:     5
  elapsed   :     0.214 s
  process   : user: 0.158 s, system: 0.055 s, faults: 0, peak rss: 186.9 MiB, peak commit: 191.5 MiB

mimalloc: process done: 0x7FD95A8E2000

Again, maybe this is completely normal and expected, and users should turn MIMALLOC_ALLOW_THP off for short-lived processes, or turn THP in their system off if they don't want to deal with this kind of overhead.

Maybe mimalloc can submit some kind of madvise or something when it sets up a freelist of pages since that's what looks to be the cause of a lot of work on linux's MM?

here's the kernel portion of one of the perf recordings btw:

Image Image

Thanks in advance for any advice!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions