Chapter 7. Masterful Memory Management

Using memory properly within the Unity Engine requires a good amount of understanding of the underlying Unity Engine and Mono Framework. This can be a bit of an intimidating place for some developers, since many picked Unity as their solution primarily to avoid the kind of low-level grunt work that comes from engine development and memory management. They would prefer instead to focus on higher-level concerns related to gameplay implementation, level design, and art asset management.

Many games of limited scope can get away with focusing on such higher-level concerns, at the cost of wasted resources, and may never run into any problems related to memory. This is all well and good until the day it becomes a problem. At this point, their neglect in understanding the important components of the engine leads to a scramble to find solutions that can be difficult to understand and implement without proper knowledge to back it up.

Therefore, understanding what is happening with memory allocations and C# language features, how they interact with the Mono Platform, how Mono interacts with the underlying engine, and the various libraries we have available are absolutely paramount to making high-quality, efficient script code. So, in this chapter, you will learn about all of the nuts and bolts of the underlying Unity Engine, Mono Platform, C# language, and .NET Framework.

Let's start by exploring the familiar part of the engine, which handles most of the work for our game's scripting code—the Mono platform.

The Mono platform

Mono is a magical sauce, mixed into the Unity recipe, which gives it a lot of its cross-platform capability. Mono is an open source project that built its own framework and libraries based on the API, specifications, and tools from Microsoft's .NET Framework and common libraries. Essentially, it is a recreation of the .NET Framework, as it was accomplished with little to no access to the source code. Note that, despite Mono's libraries being an open source recreation of Microsoft's base .NET class library, it is fully compatible with the original library from Microsoft.

The goal of the Mono project is to provide a framework to allow cross-platform compatibility using the .NET Framework as a common layer. It allows applications to be written in a common programming language and run against many different hardware platforms, including Linux, OS X, Windows, ARM, PowerPC, and more. Mono also supports many languages, not just the C#, Boo, and UnityScript we may be familiar with. Any language that can be compiled into .NET's pure Common Intermediate Language (CIL – more on this later) is sufficient to integrate with the Mono platform. This includes C#, but even includes languages such as F#, Java, Visual Basic .NET, PythonNet, and IronPython.

A common misconception about the Unity Engine is that it is built on top of the Mono platform. This is untrue, as the Mono side does not handle many important game tasks such as audio, rendering, physics, and so on. Unity Technologies built a native C++ backend for the sake of speed, and allows its users control of the engine through Mono as a scripting interface. As such, Mono is merely a component of the underlying Unity Engine. This is equivalent to many other game engines, which run C++ under the hood, handling important tasks such as rendering, animation, resource management, and so on, while simultaneously providing a scripting language for gameplay logic to be implemented. The Mono platform was chosen by Unity Technologies for this task.

Tip

Native code simply means code that is compiled directly to the target OS, and executes without additional layers of complexity in the runtime environment. This keeps overhead costs low, but at the expense of needing to manage memory and other tasks within the code in a more direct fashion.

Scripting languages typically abstract away complex memory management through automatic garbage collection, and provide various safety features, which simplify the act of programming at the expense of runtime overhead. Some scripting languages can also be interpreted at runtime, meaning that they don't need to be compiled before execution. The raw instructions are converted dynamically into machine code and executed the moment they are read during runtime. The last feature, and probably the most important one, is that they allow simpler syntax of programming commands. This usually improves the development workflow immensely, as team members without much experience using languages such as C++ can contribute to the codebase. This enables them to implement things such as gameplay logic in a simpler format, at the expense of a certain amount of control and runtime execution speed.

Note that such languages are often called "managed" languages, which feature managed code. Technically, this was a term coined by Microsoft to refer to any source code that must run inside their Common Language Runtime (CLR) environment (more later), as opposed to code that is compiled and run natively through the target Operating System (OS). But, because of the prevalence and common features that exist between the CLR and other languages that feature their own similarly designed runtime environments (such as Java), the term "managed" has since become a little vague. It tends to be used to refer to any language or code that depends on its own runtime environment and that may, or may not, include automatic garbage collection. For the rest of this chapter, we will use the term "managed" to refer to code that both depends on a separate runtime environment and has undergone automatic garbage collection.

The runtime performance cost of managed languages is becoming less and less significant every year. This is partly due to gradual optimizations in tools and runtime environments, and partly due to the computing power of the average device gradually becoming greater. But the main point of controversy in managed languages still remains their automatic memory management. Managing memory manually can be a complex task that can take many years of difficult debugging to be proficient at, but many developers feel that managed languages solve this problem in ways that are too unpredictable, risking too much product quality. Such developers might cite that managed code will never reach the same level of performance as native code, and it is foolhardy to build high-performance applications with them.

This is true to an extent, as managed languages invariably inflict runtime overheads, and we lose partial control over runtime memory allocations. But, as with all things, it becomes a balancing act, since not all resource usage will necessarily result in a bottleneck, and the best games aren't necessarily the ones that use every single byte to their fullest. For example, imagine a user interface that refreshes in 30 microseconds via native code versus 60 microseconds in managed code due to an extra 100 percent overhead (extreme example). The managed code version is still fast enough such that the user will never be able to notice the difference, so is there really any harm in using managed code for such a task?

In reality, working with managed languages often just means that developers have a unique set of concerns to worry about compared to native code developers. As such, choosing to use a managed language is partly a matter of preference, and partly a compromise of control over development speed.

The compilation process

When we make changes to our C# code, it is typically compiled immediately after we switch back from our favorite IDE (which is typically either MonoDevelop or, the much more feature-rich Visual Studio) to the Unity Editor. However, the C# code is not converted directly into machine code, as we would expect to happen with static compilers in the land of C++. Instead, the code is converted into an intermediate language called Common Intermediate Language (CIL), which is an abstraction above native code. CIL is similar to Java bytecode, upon which it is based, but CIL code is entirely useless on its own, as CPUs have no idea how to run the instructions defined in this language.

At runtime, this intermediate code is run through the Mono Virtual Machine (VM), which is an infrastructure component that allows the same code to run against multiple platforms without needing to change the code itself. This is an implementation of the .NET Common Language Runtime or CLR. If we're running on iOS, we run on the iOS-based Virtual Machine infrastructure, and if we're running on Linux, then we simply use a different one that is better suited for Linux.

Within the CLR, the intermediate CIL code will actually be compiled into native code on demand. This on-demand native compilation can be accomplished either by an Ahead-Of-Time (AOT) or Just-In-Time (JIT) compiler, depending on which platform is being targeted. These compilers allow code segments to be compiled into native code (that is, machine code specific to the OS it is running against), and the main difference between the two types is when the code is compiled.

AOT compilation happens early (ahead of time), either during the build process or during initialization. In either case, the code has been precompiled and no further runtime costs are inflicted due to dynamic compilation. In the current version of Unity (Version 5.2.2), the only platforms that support AOT compilation are WebGL (and only when UnityScript is being used) and iOS.

JIT compilation happens dynamically at runtime in a separate thread and begins just prior to execution ("just in time" for execution). Often, this dynamic compilation causes the first invocation of a piece of code to run a little (or a lot!) more slowly, because the code must finish compiling before it can be executed. But, from that point forward, whenever the same code block is executed, there is no need for recompilation, and the instructions run through the previously compiled native code.

It is common in software that 90 percent of the work is being done by only 10 percent of the code. This generally means that JIT compilation turns out to be a net positive on performance than if we simply tried to interpret the CIL code directly. However, because the JIT compiler must compile code quickly, it is not able to make use of many optimization techniques that static compilers are able to exploit.

Manual JIT compilation

In the event that JIT compilation is causing a runtime performance loss, be aware that it is actually possible to force JIT compilation of a method at any time via reflection. Reflection is a useful feature of the C# language, that allows our codebase to explore itself introspectively for type information, methods, values, and metadata. Using reflection is often a very costly process. It should be avoided at runtime or, at the very least, only used during initialization or other loading times. Not doing so can easily cause significant CPU spikes and gameplay freezing.

We can manually force JIT compilation of a method using reflection to obtain a function pointer to it:

var method = typeof(MyComponent).GetMethod("MethodName");
if (method != null) {
    method.MethodHandle.GetFunctionPointer();
    Debug.Log("JIT compilation complete!");
}

This code only works on public methods. Obtaining non-public methods can be accomplished through the use of BindingFlags:

using System.Reflection;
// ...
var method = typeof(MyComponent).GetMethod("MethodName", BindingFlags.NonPublic | BindingFlags.Instance);

This kind of code should only be run for very targeted methods where we are certain that JIT compilation is causing CPU spikes. This can be verified by restarting the application and profiling a method's first invocation versus all subsequent invocations. The difference will tell us the JIT compilation overhead.

Tip

Note that the official method for forcing JIT compilation in the .NET library is RuntimeHelpers.PrepareMethod(), but this is not properly implemented in the current version of Mono that comes with Unity (Mono Version 2.6.5). The aforementioned workaround should be used until Unity has pulled in a more recent version of the Mono project.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.180.43