Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

How to disable the garbage collector (on Windows)

Discussion in 'Scripting' started by Sundersoft, Nov 20, 2015.

  1. Sundersoft

    Sundersoft

    Joined:
    Sep 15, 2012
    Posts:
    4
    The default settings for Mono's GC implementation will cause frequent lag spikes if there is more than 100MB of data allocated (including internal allocations by Mono). Dynamic methods seem to allocate a large amount of memory which needs to be garbage collected.

    There are functions called "mono_gc_disable" and "mono_gc_enable" in Mono, but they are not exported in the DLL. However, there are debugging symbols for mono.dll in the "mono.pdb" file, and the addresses of the functions can be extracted using the "dbh.exe" utility in WinDbg, and they can then be called from C#. There are 64 bit and 32 bit versions of mono.dll depending on which version of the editor or what type of standalone build is being used (the dlls are the same for the editor and the standalone build).

    Once the mono_gc_disable function is called, the GC can be invoked manually by calling mono_gc_enable, then GC.Collect, then mono_gc_disable. This can be done between level transitions or based on the game's memory usage. The GC performance depends mostly on how much live memory there is (since the prune phase just does a linear memory scan and is faster than the mark phase), so GC performance can be increased by waiting longer between GC iterations.

    Here is a script which disables GC and then manually invokes it during Update whenever certain thresholds are reached. Right now it only works for the 64 bit version of the editor and hasn't been tested with the standalone build (although it ought to work with the 64 bit version). It has also only been tested on Unity 5.2.2f1; different versions of unity might use different builds of mono.dll.

    If the script does not work, you may have to use WinDbg to figure out what the addresses of the mono_gc_disable and mono_gc_enable functions are and then update the script with the proper addresses. The mono_gc_collect address is also used to make sure the right version of mono.dll has been loaded.

    The script needs to be added to an active gameobject.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3. using System;
    4. using System.Runtime.InteropServices;
    5.  
    6. public class gc_manager : MonoBehaviour {
    7.  
    8.  
    9. //set this to true to have the GC be manually invoked by this script when certain thresholds are reached
    10. public bool turn_off_mono_gc=true;
    11.  
    12. //gc is invoked if the number of bytes allocated exceeds this value (in megabytes)
    13. public int manual_gc_bytes_threshold_mb=1000;
    14.  
    15. //however, gc will not be invoked if the number of live bytes after the most recent gc iteration multiplied by manual_gc_factor_threshold is
    16. //less than the current number of bytes allocated
    17. public float manual_gc_factor_threshold=2;
    18.  
    19. //if set to true, generate log messages about gc performance whenever gc is run
    20. public bool manual_gc_profile=true;
    21.  
    22. //minimum sampling time for calculating expected_time_until_gc
    23. public float manual_gc_min_time_delta_seconds=10;
    24.  
    25. //set by this script every update. this is the number of bytes currently allocated
    26. public float allocated_mb;
    27.  
    28. //set by this script every update. this is the average rate of memory allocation since the last gc iteration
    29. public float average_allocation_rate_mbps=-1;
    30.  
    31. //set by this script every update. this is the expected number of seconds until gc runs, or -1 if unknown
    32. //this can be used to run gc early e.g. if the game is paused
    33. public float expected_time_until_gc=-1;
    34.  
    35. //
    36. //
    37.  
    38. [DllImport("kernel32.dll", CharSet=CharSet.Auto)]
    39. static extern IntPtr GetModuleHandle(string lpModuleName);
    40.  
    41. [DllImport("kernel32.dll", CharSet=CharSet.Ansi, ExactSpelling=true, SetLastError=true)]
    42. static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
    43.  
    44. static Action mono_gc_disable;
    45. static Action mono_gc_enable;
    46.  
    47. static bool mono_gc_loaded=false;
    48.  
    49. static bool load_mono_gc() {
    50.     if (mono_gc_loaded) {
    51.         return true;
    52.     }
    53.    
    54.     unsafe {
    55.         //extracted from mono.pdb using dbh.exe (using the "enum *!*mono_gc_*" command)
    56.         //note: for the 64 bit editor, there is only a 64 bit version of mono.pdb, so you need to also download the 32 bit editor to update this for 32 bit standalone builds
    57.         // (you also need to decide which version of the dll to use; this can be done by comparing the mono_gc_collect offset with the two offsets for the 32 bit and 64 bit dlls)
    58.         int offset_mono_gc_disable=0x1b100;
    59.         int offset_mono_gc_enable=0x1b108;
    60.         int offset_mono_gc_collect=0x1b0b4; //this is used to verify that mono.dll hasn't changed
    61.        
    62.         IntPtr mono_module=GetModuleHandle("mono.dll");
    63.         IntPtr func_ptr_mono_gc_collect=new IntPtr(mono_module.ToInt64()+offset_mono_gc_collect);
    64.         IntPtr expected_func_ptr_mono_gc_collect=GetProcAddress(mono_module, "mono_gc_collect");
    65.         if (func_ptr_mono_gc_collect!=expected_func_ptr_mono_gc_collect) {
    66.             //if you see this error, you need to update the "offset_mono_gc_" variables above
    67.             Debug.Log("Cannot load gc functions. Expected collect at "+ func_ptr_mono_gc_collect.ToInt64() +" Actual at "+ func_ptr_mono_gc_collect.ToInt64() +" Module root "+ mono_module.ToInt64());
    68.             return false;
    69.         }
    70.        
    71.         mono_gc_enable=(Action)Marshal.GetDelegateForFunctionPointer(new IntPtr(mono_module.ToInt64()+offset_mono_gc_enable), typeof(Action));
    72.         mono_gc_disable=(Action)Marshal.GetDelegateForFunctionPointer(new IntPtr(mono_module.ToInt64()+offset_mono_gc_disable), typeof(Action));
    73.     }
    74.    
    75.     mono_gc_loaded=true;
    76.     return true;
    77. }
    78.  
    79. //
    80. //
    81.  
    82. //if you have a method that allocates large amounts of memory, call this at the start of it to let gc run
    83. public object force_enable_gc() {
    84.     if (force_enable_gc_count==0 && turn_off_mono_gc) {
    85.         assert._(d_gc_disabled);
    86.         d_gc_disabled=false;
    87.         mono_gc_enable();
    88.     }
    89.    
    90.     ++force_enable_gc_count;
    91.     var token=new force_enable_gc_token();
    92.     token.count=force_enable_gc_count;
    93.     return token;
    94. }
    95.  
    96. //this has to be called for each call to force_enable_gc, and the object returned by force_enable_gc must be passed
    97. public void force_enable_gc_done(object token) {
    98.     var t=(force_enable_gc_token)token;
    99.     assert._(t.count==force_enable_gc_count);
    100.     --force_enable_gc_count;
    101.     assert._(force_enable_gc_count>=0);
    102.    
    103.     t.count=-1;
    104.    
    105.     if (force_enable_gc_count==0 && turn_off_mono_gc) {
    106.         assert._(!d_gc_disabled);
    107.         d_gc_disabled=true;
    108.         mono_gc_disable();
    109.        
    110.         enabled=true;
    111.     }
    112. }
    113.  
    114. //alternate version of System.GC.Collect which works if gc is disabled
    115. public void gc_collect() {
    116.     if (d_gc_disabled) {
    117.         manual_gc();
    118.     } else {
    119.         GC.Collect();
    120.     }
    121. }
    122.  
    123. //alternate version of System.GC.GetTotalMemory
    124. public long gc_get_total_memory(bool do_gc) {
    125.     if (do_gc) {
    126.         gc_collect();
    127.     }
    128.     return GC.GetTotalMemory(false);
    129. }
    130.  
    131. //
    132. //
    133.  
    134. static bool d_gc_disabled=false;
    135.  
    136. long manual_gc_most_recent_in_use_bytes=-1;
    137.  
    138. System.Collections.IEnumerator run_manual_gc_after(float time) {
    139.     yield return new WaitForSeconds(time);
    140.     manual_gc();
    141. }
    142.  
    143. protected void Start() {
    144.     if (!load_mono_gc()) {
    145.         turn_off_mono_gc=false;
    146.     }
    147.    
    148.     if (turn_off_mono_gc) {
    149.         mono_gc_disable();
    150.         d_gc_disabled=true;
    151.     }
    152.    
    153.     StartCoroutine(run_manual_gc_after(0.1f)); //to get average_allocation_rate_mbps to work
    154. }
    155.  
    156. protected void OnApplicationQuit() {
    157.     StopAllCoroutines();
    158.    
    159.     if (d_gc_disabled) {
    160.         manual_gc();
    161.         mono_gc_enable();
    162.         d_gc_disabled=false;
    163.     }
    164.    
    165.     assert._(force_enable_gc_count==0);
    166. }
    167.  
    168. int[] dummy_object;
    169.  
    170. void manual_gc() {
    171.     assert._(d_gc_disabled);
    172.    
    173.     float start_time=(manual_gc_profile)? Time.realtimeSinceStartup : 0;
    174.     float bytes_allocated_initially=(manual_gc_profile)? GC.GetTotalMemory(false) : 0;
    175.    
    176.     int collection_count=GC.CollectionCount(0);
    177.     mono_gc_enable();
    178.    
    179.     //see if gc will run on its own after being enabled
    180.     for (int x=0;x<100;++x) {
    181.         dummy_object=new int[1];
    182.         dummy_object[0]=0;
    183.     }
    184.     int new_collection_count=GC.CollectionCount(0);
    185.     if (new_collection_count==collection_count) {
    186.         GC.Collect(); //if not, run it manually
    187.     }
    188.    
    189.     mono_gc_disable();
    190.    
    191.     manual_gc_most_recent_in_use_bytes=GC.GetTotalMemory(false);
    192.    
    193.     if (manual_gc_profile) {
    194.         float end_time=Time.realtimeSinceStartup;
    195.         Debug.Log(
    196.             "Ran GC iteration.\n"+
    197.             "Time: "+ (end_time-start_time)*1000 +" ms\n"+
    198.             "Initial alloc: "+ bytes_allocated_initially/1024/1024 +" MB\n"+
    199.             "Final alloc: "+ ((float)manual_gc_most_recent_in_use_bytes)/1024/1024 +" MB\n"+
    200.             "Util: "+ (manual_gc_most_recent_in_use_bytes/bytes_allocated_initially*100)+ " %\n"
    201.         );
    202.     }
    203.    
    204.     allocated_mb=((float)manual_gc_most_recent_in_use_bytes)/1024/1024;
    205.     last_gc_time=Time.realtimeSinceStartup;
    206.     last_gc_allocated_mb=allocated_mb;
    207. }
    208.  
    209. float last_gc_time=-1;
    210. float last_gc_allocated_mb=-1;
    211.  
    212. int force_enable_gc_count=0;
    213.  
    214. class force_enable_gc_token {
    215.     public int count;
    216. };
    217.  
    218. void monitor_gc() {
    219.     if (!d_gc_disabled) {
    220.         enabled=false;
    221.         return;
    222.     }
    223.    
    224.     long allocated_bytes=GC.GetTotalMemory(false);
    225.     allocated_mb=((float)allocated_bytes)/1024/1024;
    226.    
    227.     float allocated_mb_limit=manual_gc_bytes_threshold_mb;
    228.     if (manual_gc_most_recent_in_use_bytes!=-1) {
    229.         allocated_mb_limit=Mathf.Max(allocated_mb_limit, ((float)manual_gc_most_recent_in_use_bytes)/1024/1024*manual_gc_factor_threshold);
    230.     }
    231.    
    232.     if (allocated_mb>=allocated_mb_limit) {
    233.         manual_gc();
    234.     }
    235.    
    236.     {
    237.         float time=Time.realtimeSinceStartup;
    238.         if (last_gc_time!=-1) {
    239.             float delta=time-last_gc_time;
    240.             if (delta>=manual_gc_min_time_delta_seconds) {
    241.                 average_allocation_rate_mbps=(allocated_mb-last_gc_allocated_mb)/delta;
    242.             }
    243.         }
    244.        
    245.         if (average_allocation_rate_mbps!=-1) {
    246.             expected_time_until_gc=(allocated_mb_limit-allocated_mb)/average_allocation_rate_mbps;
    247.         }
    248.     }
    249. }
    250.  
    251. protected void FixedUpdate() {
    252.     monitor_gc();
    253. }
    254.  
    255. protected void Update() {
    256.     monitor_gc();
    257. }
    258.  
    259.  
    260. };
     
    Kiwasi likes this.
  2. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    You probably want to test it in at least one build before you start bouncing it around.

    Its a cool idea. But switching off the GC is a potentially dangerous practice. You'd really want to know what you are doing before you take this path. I can see new users crashing their systems trying to run it.
     
  3. Patico

    Patico

    Joined:
    May 21, 2013
    Posts:
    886
    The manhood spent the YEARS to invent the automatic collection of garbage, the ways to avoid a memory leaks and so on... But you, you just TURN IT OFF... :confused:
    :)
     
    LiberLogic969 and Kiwasi like this.
  4. Sundersoft

    Sundersoft

    Joined:
    Sep 15, 2012
    Posts:
    4
    Unity cannot crash the operating system since it is a user-mode program. The unity editor or game may deadlock or crash if a single frame allocates enough memory to use up all of the system memory (which would require several gigabytes of allocations in a single frame). The operating system will kill Unity if it uses up all of the system memory.

    The full script will decide when to run the GC to avoid running out of memory; using it should improve performance at the expense of memory usage (since the memory utilization will go down), but it will not improve GC latency. The system should not run out of memory as long as each frame does not allocate too much memory (although there are functions to re-enable the GC temporarily).

    The script does not take the system's total amount of ram or total memory usage into account. It is also possible for some of the memory allocated by Mono to get swapped out to disk, which will make GC very slow.
     
  5. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Awful lot of should's there. I'm not saying its a bad thing. Plenty of languages out there and game engines built in them that take care of their own memory allocation. Its just you'll need to be uber careful. Your operating system might be nominally protected. But a leak can still crash your game.
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,405
    It should be pointed out, the script uses 'kernel32.dll', so would only work with Windows only.

    Also, the memory being used up would create an alternative issue. Sure, when Windows memory got completely full... it Windows would just force the application out of service. BUT, it'd have to reach that point first. This is the nature of memory leaks... they leak out slowly, filling up memory, and slowing the system down in general.

    Especially on low memory systems.

    For example, the script hard codes its memory cap, rather than base it off the amount of available system memory. If a naive user were to utilize it, set the memory cap at a few gigs... because on their 8 or 16 gig dev system (heck, mine has 32 gigs), everything works fine. But built and deployed to a small consumer computer of 2 or 4 gigs (may sound ridiculously low... but if you think that, you'd be surprised by what is in the average system out there)... and things go south real fast.



    I so dearly wish for a better garbage collector in Unity, but we're using a very outdated version of it (and it probably won't be changed any time soon, so your concerns about that breaking your script actually shouldn't be heavy). It doesn't even have generational gc, as you mentioned, it does a linear scan.

    But this solution targets a very specific platform, win 64-bit. And has potential to really mess the newb up.



    But hey, with all that said... awesome that you shared it! Could definitely help out some people, and as long as we get the caveats understood, they can deal with it accordingly.
     
    Kiwasi likes this.
  7. yanivng

    yanivng

    Joined:
    May 21, 2017
    Posts:
    43
    Hi
    This is a rather old thread, but still very interesting. Is there a more "clean" way to locate the entry point for the relevant methods rather than using:
    1. int offset_mono_gc_disable=0x1b100;
    2. int offset_mono_gc_enable=0x1b108;
    Thanks,
    Yaniv
     
  8. ApacheThunder

    ApacheThunder

    Joined:
    Sep 24, 2021
    Posts:
    1
    This thread a bit on the old side but I will add this worked like a charm for Enter the Gungeon (via mods). This game is using a newer version of Unity then the original thread poster had back when he first made this. Here's the updated offsets for Unity 2017.4.40f1 (64-bit)'s version of mono which may be applical for similar versions close to this one as long as the version of mono they used didn't change:


    int offset_mono_gc_disable = 0x1b310;
    int offset_mono_gc_enable = 0x1b4a0;
    int offset_mono_gc_collect = 0x1b2c4;

    Enter the Gungeon had it's own class specifically for displaying GC stats which I used to confirm that this was indeed working. That game is notoriously laggy once you have a lot of mods installed. I will be adjusting this to still allow GC on floor loads and when certain ram limits are hit. Should reduce the frame drops in this game a lot. :D

    The game doesn't leak ram that bad. I think GC is maybe just a tad too aggressive for this game.

    Note that newer modern versions of Unity I think have given support for more control over the garbage collector so this probably isn't relevant for 2018 and newer versions of Unity. However if you got 2017 or older this is still relevant.