Memory Allocation Without the GC
Unity’s garbage collector is super slow and the bane of our programming life. It’s the reason we can’t use foreach
, have to make pools of objects, and go to great lengths to avoid boxing. It’s also seemingly mandatory, but that’s not quite true. Today’s article shows you a way that you can skip the GC and still allocate memory!
To recap, C# has reference types and value types. Reference types are class instances and value types are primitives like int
and structs. Reference types are created on the heap, reference counted, and garbage collected. Value types are created on the stack unless they’re fields of classes.
So it’s good practice in Unity to avoid using a lot of reference types because they ultimately create garbage that the garbage collector has to collect, which fragments memory and creates frame spikes. So we turn to structs instead of classes because they don’t create garbage when we use them as local variables. Think of Vector3
, Quaternion
, or Color
.
We can use these structs as local variables and pass them as parameters with or without the ref
keyword. But those are temporary operations. Sometimes we’d like to keep them longer, so we add a struct field to a class or we create an array or List
of structs. Unfortunately, that class or array or List
is itself going to become garbage someday. And so it seems we’re painted into a corner and must accept the inevitability of the GC.
Thankfully, we have a way out! There is a way to allocate memory without using the new
operator and without using any classes, including arrays. The way to do it is to call System.Runtime.InteropServices.Marshal.AllocHGlobal
. You pass it the number of bytes you’d like to allocate and it allocates them on the heap and returns you an IntPtr
struct. When you’re done, call Marshal.FreeHGlobal
and pass it the IntPtr
you got back from AllocHGlobal
.
In the meantime, you can do whatever you want with that IntPtr
. You’ll probably want to turn on “unsafe” code by putting -unsafe
in your Asset’s directory’s gmcs.rsp
, smcs.rsp
, and mcs.rsp
. Then you can cast the IntPtr
to a pointer and access the memory however you want. Here’s a little example where we store a struct on the heap:
using System; using System.Runtime.InteropServices; using UnityEngine; struct MyStruct { public int Int; public bool Bool; public long Long; } unsafe void Foo() { // Allocate enough room for one MyStruct instance // Cast the IntPtr to MyStruct* so we can treat the memory as a MyStruct var pStruct = (MyStruct*)Marshal.AllocHGlobal(sizeof(MyStruct)); // Store a struct on the heap! *pStruct = new MyStruct { Int = 123, Bool = true, Long = 456 }; // Read the struct from the heap Debug.Log(pStruct->Int + ", " + pStruct->Bool + ", " + pStruct->Long); // Free the heap memory when we're done with it Marshal.FreeHGlobal((IntPtr)pStruct); }
Other than the Debug.Log
, this code doesn’t create any garbage. We can store the MyStruct*
long-term just like a reference to a class. Copying a pointer is cheap, too. A MyStruct*
is just 4 or 8 bytes regardless of how big MyStruct
gets.
Of course we can do whatever else we want with the heap memory we get back from AllocHGlobal
. Want to replace arrays? Easy!
using System; using System.Runtime.InteropServices; using UnityEngine; /// <summary> /// An array stored in the unmanaged heap /// http://JacksonDunstan.com/articles/3740 /// </summary> unsafe struct UnmanagedArray { /// <summary> /// Number of elements in the array /// </summary> public int Length; /// <summary> /// The size of one element of the array in bytes /// </summary> public int ElementSize; /// <summary> /// Pointer to the unmanaged heap memory the array is stored in /// </summary> public void* Memory; /// <summary> /// Create the array. Its elements are initially undefined. /// </summary> /// <param name="length">Number of elements in the array</param> /// <param name="elementSize">The size of one element of the array in bytes</param> public UnmanagedArray(int length, int elementSize) { Memory = (void*)Marshal.AllocHGlobal(length * elementSize); Length = length; ElementSize = elementSize; } /// <summary> /// Get a pointer to an element in the array /// </summary> /// <param name="index">Index of the element to get a pointer to</param> public void* this[int index] { get { return ((byte*)Memory) + ElementSize * index; } } /// <summary> /// Free the unmanaged heap memory where the array is stored, set <see cref="Memory"/> to null, /// and <see cref="Length"/> to zero. /// </summary> public void Destroy() { Marshal.FreeHGlobal((IntPtr)Memory); Memory = null; Length = 0; } } unsafe void Foo() { // Create an array of 5 MyStruct instances var array = new UnmanagedArray(5, sizeof(MyStruct)); // Fill the array for (var i = 0; i < array.Length; ++i) { *((MyStruct*)array[i]) = new MyStruct { Int = i, Bool = i%2==0, Long = i*10 }; } // Read from the array for (var i = 0; i < array.Length; ++i) { var pStruct = (MyStruct*)array[i]; Debug.Log(pStruct->Int + ", " + pStruct->Bool + ", " + pStruct->Long); } // Free the array's memory when we're done with it array.Destroy();
One downside of this approach is that we need to make sure to call FreeHGlobal
or the memory will never be released. The OS will free it all for you when the app exits. One issue crops up when running in the editor because the app is the editor, not your game. So clicking the Play button to stop the game means you might leave 100 MB of memory un-freed. Do that ten times and the editor will be using an extra gig of RAM! You could just reboot the editor, but there’s a cleaner way so you don’t even need to do that.
Instead of calling AllocHGlobal
and FreeHGlobal
directly, you can insert a middle-man who remembers all of the allocations you’ve done. Then you can tell this middle-man to free them all when your app exits. Again, this is only necessary in the editor so it’s good to use #if
and [Conditional]
to strip out as much of the middle-man as possible from your game builds.
using System; using System.Diagnostics; using System.Runtime.InteropServices; #if UNITY_EDITOR using System.Collections.Generic; #endif /// <summary> /// Allocates and frees blocks of unmanaged memory. Tracks allocations in the Unity editor so they /// can be freed in bulk via <see cref="Cleanup"/>. /// http://JacksonDunstan.com/articles/3740 /// </summary> public static class UnmanagedMemory { /// <summary> /// Keep track of all the allocations that haven't been freed /// </summary> #if UNITY_EDITOR private static readonly HashSet<IntPtr> allocations = new HashSet<IntPtr>(); #endif /// <summary> /// Allocate unmanaged heap memory and track it /// </summary> /// <param name="size">Number of bytes of unmanaged heap memory to allocate</param> public static IntPtr Alloc(int size) { var ptr = Marshal.AllocHGlobal(size); #if UNITY_EDITOR allocations.Add(ptr); #endif return ptr; } /// <summary> /// Free unmanaged heap memory and stop tracking it /// </summary> /// <param name="ptr">Pointer to the unmanaged heap memory to free</param> public static void Free(IntPtr ptr) { Marshal.FreeHGlobal(ptr); #if UNITY_EDITOR allocations.Remove(ptr); #endif } /// <summary> /// Free all unmanaged heap memory allocated with <see cref="Alloc"/> /// </summary> [Conditional("UNITY_EDITOR")] public static void Cleanup() { foreach (var ptr in allocations) { Marshal.FreeHGlobal(ptr); } allocations.Clear(); } }
Now just replace your calls to Marshal.AllocHGlobal
and Marshal.FreeHGlobal
with calls to UnmanagedMemory.Alloc
and UnmanagedMemory.Free
. If you want to use the UnmanagedArray
struct above, you might want to do this replacement in there as well. The final step is to put this one-liner in any MonoBehaviour.OnApplicationQuit
:
UnmanagedMemory.Cleanup();
Hopefully this technique will be a good addition to your toolbox as a Unity programmer. It can certainly come in handy when you need to avoid the GC!
#1 by Ed Earl on February 6th, 2017 ·
Maybe you could ensure allocations are freed with a little RAII class?
#2 by jackson on February 6th, 2017 ·
Can you provide a little example of how that might work in C#?
#3 by Ed Earl on February 8th, 2017 ·
The idea is to tie the lifecycle of a resource, in this case a block of unmanaged memory, to the lifecycle of an object. In C++ that’s done with RAII; in C#, the Dispose pattern:
(untested – apologies for typos!)
Of course, each UnmanagedMemHolder object allocation will then add to GC pressure.
Hopefully there’s a final GC pass when you exit the Unity Editor/runtime – otherwise this doesn’t help much! (Should probably test that…)
#4 by jackson on February 8th, 2017 ·
The GC won’t ever call your
Dispose
function. Users have to call it manually, but they do get access to theusing
block syntax sugar:Instead of this:
So
Dispose
gets called for them. That’s a handy convenience and the code looks a lot cleaner, but it’s functionally equivalent to theDestroy
andCleanup
methods in the article. On the downside, as you point out, it adds garbage for theUnmanagedMemHolder
class instance. So you’d never want to use it on types that come and go often throughout the app, likeUnmanagedArray
.One change I could see would be for the
UnmanagedMemory
class to be made non-static and implement a destructor:The destructor would be called when the
UnmanagedMemory
instance is garbage-collected, which would hopefully only be when the application exits. Therefore, it seems like the usage would be to create one in aMonoBehaviour
that’s active in the first scene, store it as a field, never destroy or deactivate thatMonoBehaviour
, then set the field tonull
inOnApplicationQuit
. You’d then either need to makeUnmanagedMemory
a singleton or pass instances of it around to anyone who needs to use it. It’s hard to see how either way provides any benefits over the static class. Perhaps without the singleton it’s easier to unit test.You’re definitely right about C++ RAII. It’s destructors are totally different from the destructors in C#. They run at well-defined times and the classes you put them in don’t create any garbage because C++ isn’t a garbage-collected language. We don’t have anything like that for C# though. We don’t even have access to destructors in structs. So we just remember to call our
Dispose
functions.#5 by Ed Earl on February 8th, 2017 ·
You’re right, Dispose isn’t called automatically. Thanks for clearing that up. Finalize is, though, so it might be possible to free unmanaged memory in an implementation of Finalize.
The thing I like about the idea of tying the lifecycle of unmanaged memory to the lifecycle of a managed object is that it confers some of the advantages of a managed memory approach, such as not having to remember to make an explicit call in order to free memory. Have to write a working implementation, though, which I failed to do ;)
#6 by jackson on February 8th, 2017 ·
C# destructors are essentially finalizers. You could use them to free the unmanaged memory. Unfortunately that would mean that you need to use a class to get garbage collected, which kind of defeats the point in the case of types like
UnmanagedArray
.If you like the C++ RAII approach there are two ways you can get it. First, you could use C++/CLI to produce .NET DLLs. I’m not so sure how well that would work with Unity and IL2CPP, but it might. Also, “pure” (no native) DLL support is deprecated in the C++/CLI compiler so you’ll probably run out of support soon. Second, you could call into native code via P/Invoke (e.g.
[DllImport]
) and then write your own real, native C++ there. There’s potentially a lot of work to the “glue” layer between C# and C++, but it’s an option if you really want to escape .NET.#7 by Ed Earl on February 9th, 2017 ·
Yep, current projects are using a lot of native plugins.
#8 by ms on February 22nd, 2017 ·
damn this some heavy stuff (including the comments by ed), super dope — thanks for the schoolin’
#9 by ms on February 22nd, 2017 ·
How would one go about tracking (I assume) managed allocations? Would this be anytime we use the ‘new’ keyword? Does it even make sense to? Can we track the lifetime of a resource? I apologize if any of these questions are naive or redundant.
Thanks,
m
#10 by jackson on February 22nd, 2017 ·
Questions of all levels are welcome! :-D
Managed allocations (e.g.
new MyClass()
) are tracked by the garbage collector. It keeps track of how many references there are to an instance of an object. Sometime after there are no more references to an object the garbage collector will reclaim the memory those objects were using. Currently, Unity’s garbage collector is very slow, runs on the main thread, collects all the garbage at once, and causes memory fragmentation. This is the reason why I have written so many articles with strategies to avoid creating any garbage.On the contrary,
AllocHGlobal
allocates unmanaged memory. The garbage collector doesn’t know about this memory. It doesn’t keep a reference count for it and it never reclaims it. It’s entirely your responsibility to callFreeHGlobal
when you’re done with the memory you asked for withAllocHGlobal
. That can be very error prone but also very efficient, so the article is there to inform you of that option.#11 by m on March 3rd, 2017 ·
Thanks for the response!
My follow up question, albeit perhaps naive, is: can we get a count of the GC managed references at will? For example, say simply for printing the count to the console on demand.
A second question, semi-related to this article and topic: do we need to work in unmanaged territory to reap the benefits of SoA (struct of array) design? Or are we guaranteed contiguous (parallel?) blocks of memory when defining several arrays as fields in a c# class? Or is that only a guarantee in a c# struct? Or no guarantee at all?
For reference see: ‘managing data relationships’ by noel llopis. His article (as well as this talk at gdc 2017 by tim ford) has inspired me as of late and all this talk of memory management I feel is related. I am curious because traditionally we only read about DOD from a c++ perspective, not from a c# perspective.
Have you explored DOD in the context of unity?
Cheers!
#12 by jackson on March 3rd, 2017 ·
I don’t know of a way to get the number of managed objects, but you can get the total managed memory size with
System.GC.GetTotalMemory
. TheGC
class has other useful functionality, such asCollect
to force garbage collection (e.g. on a loading screen).For SoA, you can easily make a class or struct containing arrays. These arrays are managed references to
Array
objects that contain the pointer to the actual memory (i.e. array of values) as well as other data such as theLength
of the array. It’s the C++ equivalent of astruct
containing multiplestd::shared_ptr<std::vector<T>>
, except theshared_ptr
is garbage-collected instead of reference counted.If you want to contain the actual array in your class or struct, you have two options. First, you can use a fixed array of a compile-time known number of primitives (e.g.
int
) in a struct. That’s pretty restrictive, but it may work in some use cases. Second, you can use unmanaged memory as described in this article to store pointers directly in your class or struct. You can then treat those pointers as arrays as you choose. If you need them to be contiguous, simply allocate (i.e. withAllocHGlobal
) a block of memory large enough for all your arrays and then set the pointers to offset into the memory you get back. For example, if you need three arrays of contiguous floats you can do this:Unmanaged memory is a lot more flexible, but has the downsides of needing to explicitly free it, double-frees, using freed memory, etc.
As for DOD, it’s really only discussed among C/C++/Assembly programmers because they tend to be the ones who care most about squeezing out the last bits of CPU performance. If that’s your concern, C# is a terrible option. It’s just not designed for that purpose. That said, there are certain “escape hatches” you can use to get around a lot of its overhead. This article discusses one of them: use
AllocHGlobal
/FreeHGlobal
to avoid the GC entirely. Structs, “unsafe” code, and pointers are more such “escape hatches”. If you avoid most of the language and .NET library then you can pull off some semblance of DOD in C#, especially with IL2CPP.Thanks for the reference to the article and the talk. :)
#13 by m on March 5th, 2017 ·
Thanks for the response man, very insightful and interesting. Looking forward to the next article.
#14 by benjamin guihaire on September 10th, 2017 ·
with Unity games, often at loading time, while for example deserializing protobuf , the need for memory heap gets really, really high… so internally Unity bump the heap size, but unfortunately never shrink it again, so we get memory allocated for nothing, for ever, and less memory for system memory (textures, audio …)
So .. using your technique, I am wondering if its going to also grow the same heap used by the Managed Memory .. in that case, maybe we could call Marshal.CoTaskMemAlloc() instead of AllocHGlobal ?
#15 by jackson on September 11th, 2017 ·
“Deserializing protobuf” makes me think you’re talking about the managed heap since Unity doesn’t provide any Protocol Buffers functionality as far as I’m aware. In that case,
AllocHGlobal
should be fine since it allocates unmanaged memory. I assume it’s a pass-through to a native function likemalloc
, but it might be implemented by a native memory manager. Given that the doc forAllocCoTaskMem
says it’s for COM purposes, I’m not sure how this is implemented on non-Windows platforms like iOS and Android. It may just be a synonym forAllocHGlobal
.#16 by benjamin guihaire on September 12th, 2017 ·
it looks like allocCoTaskMem and AllocHGlobal both call malloc.
#17 by Kailang Fu on September 21st, 2018 ·
This IS the solution that I’ve been looking for.
I’ll rewrite all my music synthesis stuffs in unsafe code.
Writing C in C# is so beautiful.
#18 by Stephan on October 21st, 2018 ·
Great stuff!
If I’m using Unity is there a way to make sure the memory I allocate is low in the heap so I don’t badly fragment the heap? All I can think is to allocate that memory as early as possible.
#19 by jackson on October 21st, 2018 ·
No, both .NET’s
Marshal.AllocHGlobal
and Unity’sUnsafeUtility.Malloc
don’t give you any control over the location of the allocated memory. In the case ofUnsafeUtility.Malloc
, you can at least choose theAllocator
so perhapsTemp
orTempJob
will be useful to you.#20 by Matteo James on August 24th, 2020 ·
hey so this is amazing! your series on c++/c# really helped me understand a lot about how communicating between these two languages works!
How would you go about a gc-less approach for a struct that contains 3 strings?
all guaranteed to be 65 bytes each?
I can’t seem to finagle a way that works similar to what you’re doing here!
any help would be much appreciated!
#21 by jackson on August 24th, 2020 ·
Fixed-size buffers should do the trick because your strings don’t change their lengths. Of course you won’t be able to use any of the managed
string
functionality on them, but that can be replaced. For example, here’s a struct with three 65-byte strings and a replacement forstring.IndexOf
:#22 by Matteo James on August 24th, 2020 ·
Wow thats way simpler than I thought it would be! thanks so much, you’re a true wizard!
I’m making a Vector3 type thing(arb3) in c++ that is called in c#. But its arbitrary precision, so I can really only pass strings back and forth between the languages to represent these extremely large numbers.
I was originally using a struct with three strings that used sequential layout. but when testing i saw that calling my add function 100,000 times took like 5 seconds and the GC was allocating like 55 mb and ruining any performance gains i was hoping to receive. I compared this against calling a c++ function that did the 100,000 additions internally and it was so fast and only allocated a few kB, so i know its not the arbitrary precision code taking a while.
Hopefully this solution will speed things up as i can avoid the garbage collector and pass it as an intptr :)
here’s my old code for reference lol
c#
c++
#23 by Matteo James on August 24th, 2020 ·
so I’m hella dumb and I’ve been trying all day to find out how to receive this in c++ and I’m currently trying :
is this even close to right?
I also tried this and had no luck:
the problem is they both allow me to assign the value, but I get nothing but blankness when I attempt to turn the result into a string. (I’m passing in 0.00 as x y and z and I’ve made sure to null terminate as you have, I confirmed their format and was able to convert them to strings in c# and debug them, I just can’t seem to replicate the data in c++).
any help is much appreciated. do you have a patreon or something?
#24 by jackson on August 24th, 2020 ·
The first version is a struct with three pointers, so it’s just 12 or 24 bytes and therefore not a good match for the C# struct I posted since it’s 195 bytes.
The second version is more correct since it has three 65-byte arrays. The compiler may add padding though, so you might want to turn that off with something like
#pragma pack(1)
or__attribute__((packed));
depending on your compiler. You can usesizeof(HasStrings)
to check the size.This line has a memory leak:
That’s because
new
allocates aHasStrings
on the free store (a.k.a. heap) and is never freed with a correspondingdelete
. The pointer is simply dereferenced to aHasStrings
and then copied tolocal
. Instead, you can just put a semicolon or use another kind of initialization:As for
StoreInCPP
, P/Invoke bindings are notoriously complicated, especially when it comes to cross-platform support, arrays, and pointers. You’ll need to see the C# side and likely spend some time in a debugger to figure out exactly what’s going on. Once you’ve confirmed that your structs have the same memory layout in C# and C++, you might want to avoid the complexity of automatic struct marshaling by passing aHasStrings*
from C# to C++ and then either assigning withlocal = *value
or usingmemcpy
to copy the bytes.Best of luck!
PS: I don’t have a Patreon.