Easily “Compile Out” Function Calls
Taking another break from the iterator series, this week we’ll take a look at an exciting .NET feature that can easily and cleanly remove the calls to a function throughout the whole code base. Unity uses this for Debug.Assert
and you can use it for all sorts of functions, too. Wouldn’t it be nice if we could strip out all the debug functions from the production build of our game but leave them in during development? Read on to learn how!
Let’s say our game has a logging class:
public class Logger { public void Debug(string message) { UnityEngine.Debug.Log(message); } public void Error(string message) { UnityEngine.Debug.LogError(message); } } void Foo() { var logger = new Logger(); logger.Debug("debug message"); logger.Error("error message"); }
Now we want to shut off the debug logs in our production build to improve performance and not leak implementation details. How do we do this? One approach is to go into the Debug
function and make it do nothing instead of logging:
public void Debug(string message) { #if DISABLE_DEBUG_LOGGING // Logging is disabled so don't do anything #else // Logging is enabled so log the message UnityEngine.Debug.Log(message); #endif }
Then we disable debug logging by going to Edit > Project Settings > Player
then adding DISABLE_DEBUG_LOGGING
to the Scripting Define Symbols
section of the Inspector pane:
The debug log won’t be printed, but this didn’t fully shut off the debug logging. All the calls to logger.Debug
are still there taking up space in the binary, taking up cycles of the CPU, and, perhaps most importantly, having all of their parameters evaluated. Consider a function call like this:
logger.Debug( DateTime.Now + " - User " + user.Id + " sent us a message object: " + JsonUtility.ToJson(messageObj) );
We’re still getting the DateTime.Now
value, concatenating a bunch of strings, and serializing an object to JSON. That’s a lot of expensive work considering that the Debug
function is just going to ignore it!
Much better would be if we could eliminate the whole call to logger.Debug
in the first place. One first pass would be to use another #if
like this:
#if DISABLE_DEBUG_LOGGING #else logger.Debug( DateTime.Now + " - User " + user.Id + " sent us a message object: " + JsonUtility.ToJson(messageObj) ); #endif
But there are four main issues with this approach. First, our code is now ugly. Second, we need to remember to do this every time we call the function. Third, we need to make sure we use the exact same “scripting define symbol” (a.k.a. preprocessor definition) for every function call. Fourth, all of these strings will bloat up our game’s size and be available for anyone who wants to comb through the binary looking for vulnerabilities.
Enter the [Conditional]
attribute in System.Diagnostics
. Yes, it works in Unity’s old Mono implementation and works like a charm. Here’s how we can change the function:
using System.Diagnostics; [Conditional("ENABLE_DEBUG_LOGGING")] public void Debug(string message) { UnityEngine.Debug.Log(message); } logger.Debug("debug message");
Now the call to logger.Debug
and all of its parameters will not be compiled if ENABLE_DEBUG_LOGGING
isn’t defined. Unfortunately, there’s no way to specify that calls to the function should exist only if a symbol isn’t defined. But we can work around that quite easily:
#if DISABLE_DEBUG_LOGGING [Conditional("__NEVER_DEFINED__")] #endif public void Debug(string message) { UnityEngine.Debug.Log(message); }
If the DISABLE_DEBUG_LOGGING
symbol is defined then we include the [Conditional]
attribute, otherwise we don’t and calls to the function are always made. The “condition” isn’t much of a condition though: we only allow calls to the function if the __NEVER_DEFINED__
symbol is defined. Since that should never be defined, calls to the function are effectively disabled.
To finish things up, let’s look at a couple of extensions of this idea. First, can we put a [Conditional]
attribute on an interface function? If so, do we need to put it on the class that implements the interface?
public interface ILogger { #if DISABLE_DEBUG_LOGGING [Conditional("__NEVER_DEFINED__")] #endif void Debug(string message); #if DISABLE_ERROR_LOGGING [Conditional("__NEVER_DEFINED__")] #endif void Error(string message); } public class Logger : ILogger { // Need [Conditional]? public void Debug(string message) { UnityEngine.Debug.Log(message); } // Need [Conditional]? public void Error(string message) { UnityEngine.Debug.LogError(message); } }
Unfortunately the answer is “no”. You’ll get a compiler error for putting [Conditional]
on an interface function. On the plus side, you are allowed to put it on an abstract class! You also don’t have to add it to the concrete (i.e. non-abstract) class that derivies from it. That means this “final” version works out just fine:
public abstract class AbstractLogger { #if DISABLE_DEBUG_LOGGING [Conditional("__NEVER_DEFINED__")] #endif abstract public void Debug(string message); #if DISABLE_ERROR_LOGGING [Conditional("__NEVER_DEFINED__")] #endif abstract public void Error(string message); } public class Logger : AbstractLogger { override public void Debug(string message) { UnityEngine.Debug.Log(message); } override public void Error(string message) { UnityEngine.Debug.LogError(message); } } void Foo() { var logger = new Logger(); logger.Debug("debug message"); // unless DISABLE_DEBUG_LOGGING logger.Error("error message"); // unless DISABLE_ERROR_LOGGING }
We now have an easy way to “compile out” all of the calls to a function across the whole code base. We only need to specify the symbol in one place, all the parameters are removed and not evaluated, we save executable size, and the code is just as clean as it was before.
I hope you find this useful in your projects. Let me know in the comments if you use this or a similar technique and how it’s worked out for you!
#1 by Tim Keating on August 4th, 2016 ·
Actually, any compiler worth its salt will eliminate calls to noop functions. I can’t say for certain that the Unity mono compiler does this, but I suspect it does.
#2 by jackson on August 4th, 2016 ·
In my decompiling I didn’t see Unity’s C# compiler removing calls to empty functions. However I’m not sure if I tested calls to non-virtual functions or only virtual functions like those in
AbstractLogger
. For calls to a virtual function through a base class or interface it’s much more difficult for the compiler to conclusively determine the concrete class, devirtualize the function call, and then remove it due to the function being a no-op.In any case,
[Conditional]
makes the question moot by always removing the function calls. It’s a good option when the compiler isn’t removing them or you just want to make sure.#3 by Mirko on November 12th, 2016 ·
How easy is to setup that way so that i can include or exclude parts of the code to make logs, for example i want to turn on logging for some major components and not for others or make some combination…ofcourse turn off completelly non development builds?
Thanks!
#4 by jackson on November 12th, 2016 ·
It’s really easy to do. You can even copy the logger classes straight out of the article and use them directly. Feel free to modify them to suit your needs. For example, if you want to turn off logging on a per-component basis instead of a per-severity basis then you might have functions like
AI
andNetworking
instead ofDebug
andError
.