Version: 2020.2
Unity architecture
.NET profile support

Overview of .NET in Unity

Unity uses the open-source .NET platform in order to ensure that applications you make with Unity can run on a wide variety of different hardware configurations. .NET supports a range of languages and API libraries.

Scripting backends

Unity has two scripting backends, Mono and IL2CPP (Intermediate Language To C++), each of which uses a different compilation technique:

  • Mono uses just-in-time (JIT) compilation and compiles code on demand at runtime.
  • IL2CPP uses ahead-of-time (AOT) compilation and compiles your entire application before it is run.

The benefit of using a JIT-based scripting backend is that the compilation time is typically much faster than AOT and it’s platform-independent.

The Unity Editor is JIT-based and uses Mono as the scripting backend. When you build a player for your application, you can choose which scripting backend to use. To do this through the Editor, go to Edit > Project Settings > Player, open the Other Settings panel, then click on the Scripting Backend dropdown and select which backend you want.

Managed code stripping

When you build your application, Unity scans the compiled assemblies (.DLLs) to detect and remove unused code. This process reduces the final binary size of your build, but increases build time.

Code stripping is disabled by default when you use Mono but code stripping cannot be disabled for IL2CPP. You can control how aggressive Unity is when stripping code. Go to Edit > Project Settings > Player, open the Other Settings panel, then click the Managed Stripping Level dropdown and select the level of code stripping you want. For more information on code stripping, see the Managed Code Stripping documentation.

Note: Code stripping might be too aggressive in some cases and might remove code that you rely on, especially when you use reflection. You can use Preserve attributes and link.xml files to prevent specific types and functions from being stripped.

Garbage collection

Unity uses the Boehm garbage collector for both the Mono and IL2CPP backends. Unity uses the Incremental mode by default. You can disable the Incremental mode to use “stop the world” garbage collection, although Unity recommends use of Incremental mode.

To toggle between Incremental mode and “stop the world”, go to Edit > Project Settings > Player, open the Other Settings panel and click the Use incremental GC checkbox. In Incremental mode, Unity’s garbage collector only runs for a limited period of time and does not necessarily collect all objects in one pass. This spreads the time it takes to collect objects over a number of frames and reduces the amount of stuttering and CPU spikes. For more information, see Understanding Automatic Memory Management.

To check the number of allocations and possible CPU spikes in your application, use the Unity Profiler. You can also use the GarbageCollector API to completely disable garbage collection in players. When the collector is disabled, you should be careful to avoid allocating excess memory.

.NET system libraries

Unity supports many platforms and might use different scripting backends depending on the platform. The .NET system libraries require platform-specific implementations to work correctly in some cases. While Unity tries its best to support as much of the .NET ecosystem as possible, there are some exceptions to parts of the .NET system libraries that Unity explicitly does not support.

Unity makes no performance nor allocation guarantees of the .NET system libraries across Unity versions. As a general rule of thumb, Unity does not fix any performance regressions in the .NET system libraries.

Unity does not support the System.Drawing library and it is not guaranteed to work on all platforms.

A JIT scripting backend allows you to emit dynamic C#/.NET Intermediate Language (IL) code generation during the runtime of your application, whereas an AOT scripting backend does not support dynamic code generation. This is important to consider when you use third-party libraries, because they might have different code paths for JIT and AOT, or they might use code paths that rely on dynamically generated code. For more information on how to generate code at runtime, see Microsoft’s ModuleBuilder documentation.

Although Unity supports multiple .NET API profiles, you should use the .NET Standard 2.0 API Compatibility Level for all new projects for the following reasons:

  • .NET Standard 2.0 is a smaller API surface and therefore has a smaller implementation. This reduces the size of your final executable file.
  • .NET Standard 2.0 has better cross-platform support, so your code is more likely to work across all platforms.
  • .NET Standard 2.0 is supported by all .NET runtimes, so your code works across more VM/runtime environments (e.g. .NET Framework. .NET Core, Xamarin, Unity).
  • .NET Standard moves more errors to compile time. A number of APIs in .NET 4.7.1 are available at compile time, but have implementations on some platforms that throw an exception at runtime.

Other profiles can be useful if, for example, you need to provide support for an older existing application. If you want a different API compatibility level, change the .NET Profile in the Player Settings. To do this go to Edit > Project Settings > Player > Other Settings, then select the level you want from the Api Compatibility Level dropdown.

Using third-party .NET libraries

You should only use third-party .NET libraries that have been extensively tested on a wide range of Unity configurations and platforms.

Note: The performance characteristics of JIT and AOT code paths in third-party libraries might be significantly different. AOT generally reduces startup times and is suited to larger applications for this reason but increases the binary file size to accommodate the compiled code. AOT also takes longer to build during development and cannot change the behavior of the compiled code to target any one specific platform. JIT adjusts at runtime based on the platform it’s running on, which can increase running performance at the cost of a potentially longer application startup time. As such, you should profile your application in both the Editor, and on your target platform. For more information, see the Unity Profiler documentation.

You should profile the usage of your .NET system libraries on all target platforms because their performance characteristics might vary depending on the scripting backends, .NET versions and profiles you use.

When you review a third-party library, consider the following areas:

  • Compatibility: Third-party libraries might not be compatible with some Unity platforms and scripting backends.
  • Performance: Third-party libraries might have vastly different performance characteristics in Unity compared to other .NET runtimes.
  • AOT binary size: Third-party libraries might increase AOT binary size significantly because of the number of dependencies the library uses.

C# reflection overhead

Mono and IL2CPP internally cache all C# reflection (System.Reflection) objects and by design, Unity does not garbage collect them. The result of this behavior is that the garbage collector continuously scans the cached C# reflection objects during the lifetime of your application, which causes unnecessary and potentially significant garbage collector overhead.

To minimize the garbage collector overhead, avoid methods such as Assembly.GetTypes and Type.GetMethods() in your application, which create a lot of C# reflection objects at runtime. Instead, you should scan assemblies in the Editor for the required data and serialize and/or codegen it for use at runtime.

UnityEngine.Object special behavior

UnityEngine.Object is a special type of C# object in Unity, because it is linked to a native C++ counterpart object. For instance, when you use a Camera component, Unity does not store the state of the object on the C# object, but rather on its native C++ counterpart.

Unity does not currently support the use of the C# WeakReference class with UnityEngine.Objects. For this reason, you should not use a WeakReference to reference a loaded asset. See Microsoft’s WeakReference documentation for more information on the WeakReference class.

Unity C# and Unity C++ share UnityEngine Objects

When you use a method such as Object.Destroy or Object.DestroyImmediate to destroy a UnityEngine.Object derived object, Unity destroys (unloads) the native counter object. You cannot destroy the C# object with an explicit call, because the garbage collector manages the memory. Once there are no longer any references to the managed object, the garbage collector collects and destroys it.

If a destroyed UnityEngine.Object is accessed again, Unity recreates the native counterpart object for most types. Two exceptions to this recreation behavior are MonoBehaviour and ScriptableObject: Unity never reloads them once they have been destroyed.

MonoBehaviour and ScriptableObject override the equality (==) and inequality (!=) operators. So, if you compare a destroyed MonoBehaviour or ScriptableObject against null, the operators return true when the managed object still exists and has not yet been garbage collected.

Because the ?? and ?. operators are not overloadable, they are not compatible with objects derived from UnityEngine.Object. The operators do not return the same results as the equality and inequality operators when you use them on a destroyed MonoBehaviour or ScriptableObject while the managed object still exists.

Avoid using async and await

The Unity API is not thread safe and therefore, you should not use async and await tasks. Async tasks often allocate objects when invoked, which might cause performance issues if you overuse them. Additionally, Unity does not automatically stop async tasks that run on managed threads when you exit Play Mode.

Unity overwrites the default SynchronizationContext with a custom UnitySynchronizationContext and runs all the tasks on the main thread in both Edit and Play modes. To utilize async tasks, you must manually create and handle your own threads with a TaskFactory, as well as use the default SynchronizationContext instead of the Unity version. To listen for enter and exit play mode events to stop the tasks manually, use EditorApplication.playModeStateChanged. However, if you take this approach, most of the Unity scripting APIs are not available to use because you are not using the UnitySynchronizationContext.

Unity architecture
.NET profile support