Zero Garbage Collector for .NET Core

Zero Garbage Collector on .NET Core

Starting from .NET Core 2.0 coupling between Garbage Collector and the Execution Engine itself have been loosened. Prior to this version, the Garbage Collector code was pretty much tangled with the rest of the CoreCLR code. However, Local GC initiative in version 2.0 is already mature enough to start using it. The purpose of the exercise we are going to do is to prepare Zero Garbage Collector that replaces the default one.

Zero Garbage Collector is the simplest possible implementation that in fact does almost nothing. It only allows you to allocate objects, because this is obviously required by the Execution Engine. Created objects are never automatically deleted and theoretically, no longer needed memory is never reclaimed. Why one would be interested in such a simple GC implementation? There are at least two reasons:

  • it is an excellent basis for the development of your own Garbage Collection mechanism. It provides the necessary functionality to make runtime work properly and you can build on top of that.
  • it may be interesting for special use cases like very short living applications or such that almost no allocate memory (you can come up with those concepts as No-alloc or Zero-alloc programming). In such case providing GC overhead is unnecessary and it may be wise to get rid of it. It is like making huge GC.TryStartNoGCRegion over all you application.

Note: All I’ve done here is currently Windows-related so some steps should be tailored accordingly to Linux environment. I assume also the newest .NET Core 2.0 SDK (not preview) version is installed.

Note: You can clone my GitHub repository and compile CoreCLR.ZeroGC project for reference. All following code samples come from this project.

Building custom CoreCLR

The default CoreCLR build does not have customizable GC enabled (yet?) so we have to compile our custom version with FEATURE_STANDALONE_GC feature enabled. If you are not sure how to begin with CoreCLR compilation, please refer to the documentation or my own earlier post. A custom compilation is easy because the suitable option has been already introduced to build.cmd. I assume also Debug build and skipping all tests (for our experimental usage they just take too long):

In case of -buildstandalonegc option there are two additional FEATURE_STANDALONE_GC and FEATURE_STANDALONE_GC_ONLY preprocessor defines enabled.

After successful build you should have your brand new custom CoreCLR located at .\bin\Product\Windows_NT.x64.Debug folder. Although we could use standard tooling and integrate it with classic dotnet run command, it is not necessary here (and currently a little bit unstable anyway). We can use simpler CLR host called CoreRun.exe located in the mentioned folder (and I will refer to this folder as CLR_DIR).

CoreCLR with standalone Garbage Collector

How custom GC is handled in CoreCLR? Everything starts at .\src\vm\ceemain.cpp:EEStartupHelper method which initializes A LOT of things. Among others, there is .\src\vm\ceemain.cpp:InitializeGarbageCollector method call:

In default case LoadStaticGarbageCollector function is called which fall backs to loading default GC implementation from the runtime itself. But because FEATURE_STANDALONE_GC_ONLY is defined, LoadGarbageCollector is called in our case:

As we can see, the procedure CoreCLR takes is pretty straightforward (for clarity I omitted in the code any conditions and checks):

  1. Get the location of the standalone GC from GCStandalone setting. It can be set both by registry or environment variable in an usual CoreCLR way. We will use COMPlus_GCStandaloneLocation environment variable pointing to our DLL.
  2. Load the specified library and find the function INITIALIZE_GC_FUNCTION_NAME from it (which is “InitializeGarbageCollector”) with signature:
  3. Call InitializeGarbageCollector function from the specified library, providing it IGCToCLR interface and obtaining IGCHeap, IGCHandleManager and GcDacVars pointers.
  4. From now EE will cooperate with our custom GC via mentioned interfaces and no default GC code will be used!

Sounds very interesting and easy! From the user perspective it means we only need to define one environment variable and CoreCLR will replace default GC implementation. Great work has been done to separate the GC from the engine so well.

Standalone Garbage Collector library

Creating a DLL containing our own GC is fairly straightforward, however the separation is not strong yet enough to make sure there are no problems at all. One of the problems is to include the appropriate set of header files. It has become impossible to include them unchanged. This entailed joining the next and subsequent files, until eventually you would have to include almost all CoreCLR sources. Eventually, I’ve just created my own header files, containing only the necessary definitions taken from the CoreCLR code.

We start creating our library from defining required InitializeGarbageCollector as an exported pure C function:

In our simplified case its only responsibility is to return custom interfaces implementations. We should also store somehow the pointer to the IGCToCLR interface allowing us to cooperate with the runtime. Now let’s look at each of these interfaces closely.

IGCToCLR interface

This interface passed as an argument to the function InitializeGarbageCollector is used to communicate with the runtime. It contains quite a lot of available methods and listing them all here is pointless. Let’s just look at some of the most interesting:

With the help of those methods we can create sophisticated GC implementations. However in case of our Zero Garbage Collection, calling only one method will be required as we will see later.

IGCHeap interface

This is the main interface representing core Garbage Collection functionality. Implementing IGCHeap requires implementing 71 methods! It can hardly be called a loose coupling. Among others the most important methods are for allocations (Alloc, AllocLHeap) and initialization (Initialize):

By looking at these methods, we see quite a lot of implementation details of the default .NET GCs, which in theory should not be visible here. There are concepts of generations, SOH and LOH, segments or concurrent GC visible. Does it mean we have to re-implement default GC? Not at all. It turns out that most of this methods may provide just dummy implementation, like:

An important function is the initialization method, which should return NOERROR:

Initialize method should also configure write barriers required by the GC. In case of Zero Garbage Collector no write barriers are needed so we would just want to do nothing. We are doing some magic here to fool the runtime about the managed heap boundaries (by setting ephemeral_low and ephemeral_high). This allows you to bypass the write barrier code because the JITted code ignores the ephemeral segment. It can be seen in CoreCLR sources:

By the way, this shows that some coupling between runtime and GC still exists and we have to use such tricks to overcome it.

Other two important methods are Alloc for allocations on Small Object Heap and AllocLHeap for allocations on Large Object Heap. As our Zero GC does not use those concepts, we are just providing the same implementation for both of them:

This is the simplest implementation I’ve managed to imagine. It uses the standard calloc function, zeroing the allocated memory which is required by the runtime. It allocates size- bytes for the object itself and additional space for the header, defined as:

This is, in fact, all we need to have custom heap working! From now on, all allocations will be made using the standard calloc function. In the true GC, the alloc method would check for memory conditions (space shortage in generations or any other condition that we invent) and invoke garbage collection if needed. This is what makes near-40k lines of code in the original gc.cpp. We have about 400 lines:) Obviously, there is no garbage collection at all and our GarbageCollect (which may be called because of OS or by your application implicitly) does nothing either:

IGCHandleManager interface

IGCHandleManager is a second important interface required when implementing custom GC. It is representing handle manager functionality. Handles are extensively used by the runtime internally so this implementation has to do the minimal viable functionality instead of just dummy implementations, even if our application code does not use handles at all. The interface is again quite wide:

The most important implementations are those related to the underlying IGCHandleStore:

As handle is just a pointer-size memory block storing an address to an object, the simplest implementations of handle manipulation are trivial:

There is also one very simplified method that returns context of the handle in terms of AppDomain index. I’ve take the easy way out here and I just return the first AppDomain index which obviously may be not enough for more sophisticated applications:

My custom handle storage is also oversimplified (since it will cause a entire runtime crash if we create more than 2^16 handles) and obviously requires re-implementing but it just works for Proof Of Concept purposes:

This is in fact all we need to have our custom Garbage Collector working. Obviously there are some parts missing (especially real handles management) but as PoC it works perfectly!

Running custom CoreCLR with custom GC

How can we put it all together? We should have at this moment already compiled our custom CoreCLR and ZeroGC library. As a sample application, I suggest using just a new sample .NET Core 2.0 console application created by dotnet new console command. Then from our application directory, we have to set three environment variables and just run our application:

CORE_LIBRARIES variable is required by the CoreRun itself to point to .NET Core folder. COMPlus_GCStandaloneLocation was mentioned before and it tells the CLR to use our custom GC. Unfortunately for Debug version of runtime currently we also need to set COMPlus_HeapVerify=16 (which sets HEAPVERIFY_NO_RANGE_CHECKS (0x10) for HeapVerifyLevel) as it excludes checking if an OBJECTREF is within the bounds of the managed heap. There is no managed heap in fact so those checks makes no sense.

And that’s all! We have just run the very first .NET Core program with our Zero Garbage Collector enabled.

Note. Due to loose coupling between EE and GC, you can also go opposite way! You can re-use automatic memory management code from CoreCLR in a stand-alone application, without whole .NET runtime. There is even separate GCSample project included in CoreCLR code doing exactly that.

Leave a Reply

Your email address will not be published. Required fields are marked *