Posted On: 2022-09-19
Today's post will be a brief introduction to Unity's Burst "compiler", what it is, how it's useful, and why I put the word "compiler" in quotes. Importantly, this is intended as an approachable introduction to the topic (rather than a tutorial or how-to guide), and thus aimed at all audiences.
As I've previously explained, Unity is a C++ engine with a C# scripting layer built on top. This is an old design choice - the engine has been this way since its first release - and, although it made sense at the time, it hasn't aged well. Unity's C# runtime is relatively slow, even compared to other C# runtimes: Mono (the open-source, cross-platform C# runtime Unity built into Unity) is much better now than it was 15+ years ago, and Microsoft's .Net implementation(s) for C# have become (at least partially) open source and cross-platform, while also massively improving speed and runtime optimization. Unfortunately, Unity's design has tied it to an early version of Mono, meaning that many of the performance improvements of the past 15+ years are not available*.
All that is to say: while Unity was never the fastest game engine, it's also not able to get much faster without significant changes*. To address that issue (and capture the market of developers who are passionate about their game's performance), Unity has set up something of a skunk works project with the aim of prioritizing performance above all else. The result is a combination of technologies and tools that Unity calls DOTS (Data Oriented Technology Stack), which has, at its core, a de-facto replacement for Unity's C# runtime: the Burst "Compiler".
The best way to describe Burst is to start with the parts that game developers see: developers write code using only a small subset of C# to instruct the system at a much "lower level" than C# ordinarily allows. Burst pushes developers to control how their data is stored in memory (not really possible with Unity's normal C# runtime), and encourages multithreading (by alerting developers of any race conditions in their code). Importantly, Burst is something that developers opt into on a case-by-case basis: developers can mark individual methods that they want to be Burst "compiled", allowing high performance code to live alongside normal C# code.
From a game developer's point of view, Burst feels like its own language and compiler - just one that happens to look a lot like C#. This is not, however, what is actually going on. In all cases, Burst or not, the C# compiler converts source code into the intermediate language (IL) used by the C# runtime. Normally, the IL just stays that way: it's included with the final product, shipped to users' machines, and then when they run the program, the C# runtime converts the IL into code for that user's specific machine*. For Burst "compiled" code, however, IL is just another step in a longer journey: IL code is interpreted and analyzed by Burst, and, if there are no errors (ie. thread safety issues, unsupported language features, etc.) , Burst outputs a different kind of intermediate representation (IR) - one that can be understood by LLVM**. LLVM is a mature, open-source compiler - and it can turn the IR provided by Burst into highly optimized, machine-specific code***.
The term "compiler" is usually associated with going from a higher level of abstraction (ie. C#) into a lower level one (ie. IL). Burst, however, is actually moving across the same level of abstraction, just converting from one set of tools (C#) to another (LLVM). Thus, Burst is a transpiler, rather than a traditional compiler - it would be much more accurate to refer to LLVM as the compiler for Burst "compiled" code. I think this distinction is important, primarily because writing a compiler that generates stable, well-optimized machine code is incredibly hard. For the creators of Burst, leaning on LLVM no doubt accelerated development, and for developers considering using Burst, LLVM's (comparatively) long history of success should increase confidence in doing so.
At the end of the day, the simplest way to describe Burst is: it's harder to use, but your code will be faster. This will only become more important as Unity's scripting systems continue to age: speaking from personal experience, code that was only a bit slow in a modern C# implementation (~20ms in .Net Core) was unbearably slow when I used it directly in Unity (~4000ms). Updating the code to work in Burst got it back down to a reasonable time (~20ms) - meaning my choice of engine is not the limiting factor for my design. Developer skill and effort will always be key for a project's performance (with dedication I can surely improve the time further), but Burst offers an excellent place to start that process.