-
Notifications
You must be signed in to change notification settings - Fork 1
Home
KSPBurst allows you to use Unity's Burst Compiler to compile parts of your code down to highly optimized native code. Just compiling parts of your code with Burst can usually speed those parts up by 2-5x and by fully taking advantage of Unity.Mathematics you can get speedups of 20x or more. The trade-off for this is that burst-compiled code needs to restrict itself to a subset of C#, see the restrictions section for more details.
This page is meant to walk you what you need to do to make use of Burst in your code. For Unity's documentation you should see:
There is also more detailed reference documentation for those that you can find by searching for it.
Since the 1.7.4.9 release, KSPBurst only compiles DLLs that have a KSPAssemblyDependency on KSPBurst or otherwise opt-in by declaring a KSPBURST_ASSEMBLY config node. In most cases you should be adding a KSPAssemblyDependency anyways.
To do this add the following to your AssemblyInfo.cs
[assembly: KSPAssemblyDependency("KSPBurst", 1, 7, 4)]
If you are using KSPBuildTools, then it also has helpers for generating this for you. See it's docs for how.
In most cases you should use the KSPAssemblyDependency approach above. However, if for whatever reason you don't depend on KSPBurst then you can opt-in to having your assembly considered for burst-compilation by creating a KSPBURST_ASSEMBLY config node:
KSPBURST_ASSEMBLY
{
name = <your assembly name>
}
For example, KSPTextureLoader optionally uses burst internally and declares
KSPBURST_ASSEMBLY
{
name = KSPTextureLoader
}
Be aware that by doing this you can't rely on any of the burst libraries being present.
The easiest way to use burst-compiled code is by using unity jobs. As a bonus, these will naturally push you towards writing code that performs better with burst.
Note
If you're already familiar with writing unity jobs then you can skip most of this section. All you need to to is slap a [BurstCompile]
annotation on your job struct and any containing classes, if any.
To declare a job you implement one of the existing job interfaces:
- IJob
- IJobFor
- IJobParallelFor
- IJobParallelForBatch
- There are a number of others available in the Unity.Jobs package or in other unity systems, such as the particle system.
- Or you can even make your own custom job interfaces.
Even before you get to burst-compilation there is one big restriction on job structs: they can't hold references to managed types. This means you can't use any classes or arrays. It is possible to work around this (see the advanced tricks page). Even if you do this, though, it will block you from using burst. So instead of using managed types, you should to use the native collection types that unity provides. 99% of the time this will end up being NativeArray<T> but the Unity.Collections package provides a whole bunch of others that you can use if you need to.
Alright, now let's look at an example. Suppose we have the following loop:
NativeArray<float> data = ...;
NativeArray<float> result = new NativeArray<float>(data.Length, Allocator.TempJob);
for (int i = 0; i < data.Length; ++i)
result[i] = Math.Acos(data[i]) + Math.Asin(data[i]);For whatever reason, this uses up a noticeable amount of time each frame. In practice, you'll probably have actually useful code, but this will work as an example.
To convert this to a job, we first need to write out our job struct. In this case IJobParallelFor is a simple choice:
using Unity.Jobs;
struct MyCustomJob : IJobParallelFor
{
public NativeArrray<float> data;
public NativeArray<float> result;
public void Execute(int i)
{
result[i] = Math.Acos(data[i]) + Math.Asin(data[i]);
}
}We can now schedule it across multiple threads by doing
var job = new MyCustomJob { data = data, result = result };
var handle = job.Schedule(data.Length, innerLoopBatchCount: 128);
handle.Complete();If you open this up in a profiler you'll see that unity has already spread the work out all the available job threads. Depending on how much work there is to do and how many cores you have this can speed up things by quite a bit.
We haven't actually burst yet, though. Let's do just that:
+using Unity.Burst;
using Unity.Jobs;
+[BurstCompile]
struct MyCustomJob : IJobParallelFor
{
[ReadOnly]
public NativeArrray<float> data;
[WriteOnly]
public NativeArray<float> result;
public void Execute(int i)
{
result[i] = Math.Acos(data[i]) + Math.Asin(data[i]);
}
}That's it. If you run the code again it will now be burst-compiled. The only extra thing you need to be aware of is if you have defined your job struct within a class then you need to add the [BurstCompile] to the class as well.
At this point you should look at the burst restrictions section for a more in depth explanation of what you can and can't do in burst-compiled code.
In some cases you might want to burst-compile some code but you don't necessarily want to go through the overhead of creating and scheduling a job. This is where directly calling a static burst function comes in. With jobs unity does the work of finding the right compiled pointer to call, but for static functions you need to do that manually.
You can declare a burst-compiled function like this
[BurstCompile]
class MyClass
{
[BurstCompile]
public static double DoTheThing(double x) => x * x;
}If you run KSP with a DLL containing this code you'll see something like MyClass.DoTheThing(...) ... show up in the big list of compiled functions that get printed by the compiler. However, if you try to directly call MyClass.DoTheThing(2.0) it will call the managed method, and not the burst-compiled version of it. To call the burst-compiled version you need to do some extra work.
Note
In newer unity versions you can actually just directly call a burst-compiled function and it will actually call into the native function. This works because unity will patch a direct call to the burst-compiled function into your managed DLL in a post-processing step. While it might be possible to do this as part of a build step, KSPBurst does not, so you'll have to deal with the manual way.
The steps you need to do to get call the burst-compiled version of DoTheThing are:
- Declare a delegate type for it
delegate double DoTheThingDelegate(double x);
- Get the actual native delegate for it
var func = BurstCompiler.CompileFunctionPointer<DoTheThingDelegate>(DoTheThing).Invoke;
- Call the delegate to call the native function
var result = func(2.0);
You will want to cache the delegate returned by BurstCompiler.CompilerFunctionPointer since creating it is somewhat expensive.
If you try to pass a struct directly into a function compiled with [BurstCompile] you'll get an error from the compiler. Inside burst-compiled code you can use structs freely, and same with outside, but crossing the boundary doesn't work.
That doesn't mean you can't pass in structs. You have two ways to do so:
- Pass a raw pointer to the struct
Struct*and then dereference in the function. - Pass a
ref/in/outparameter with the struct.
The ref parameters are nicer but come with some footguns.
Warning
Ref parameters come with some footguns that you need to be aware of when using them. Burst treats them the same as pointers while mono
does not. They are for objects on the stack, but if you're taking a reference to a field of a managed object you need to make sure that
you pin that object (i.e. by using a fixed block) before passing the reference through.
In fact, my recommended pattern if you want to use refs of class fields is this:
Class c = ...;
fixed (FieldStruct* f = &c.f)
{
SomeBurstFunction(ref *f);
}You can wrap this all up in a wrapper extension function that handles it for you (which you should probably be doing anyways) but you do genuinely need to be careful about this because debugging mismatched pointers is not pleasant.
Warning
Attempting to use a [BurstCompile] method on a struct will always fail
I'm not sure what causes this but for whatever reason it will always segfault. Instead of declaring them directly on the struct declare your burst entry points on a static helper class and then call back into the struct.
Note that this doesn't apply to the Execute method for
custom job interfaces.
Those seem to work fine even if requested through BurstCompiler.CompileFunctionPointer.
When you're writing code that is meant to be burst-compiled you need to restrict yourself to a subset of C# that unity calls HPC#. You can see their docs for the full details, but the summary is pretty much:
- No managed types: no classes, no arrays, no strings. Use unity collections instead.
- You can call
Debug.Logwith a limited set of format literals. It won't call ToString implementations, more complicated types will just be replaced with their type name. - You can throw exceptions, but they will instantly crash the game and print out the exception message to player.log and error.log. They are also somewhat buggy, see the warning below.
- Most other things should work. You can call struct methods that make internal calls into unity just fine, other builtin stuff in the runtime may be iffy.
Warning
In my experience with Burst 1.5.5, the codegen around throwing exceptions seems to be pretty bad. Like, the crash from throwing the exception might be moved before the if condition that checks whether it should be thrown bad.
To avoid this being a problem I recommend this pattern. Declare a helper method:
[MethodImpl(MethodImplOptions.NoInlining)]
static void ThrowMyException() =>
throw MyException("some message");And then you call ThrowException when to throw the exception. That way it gets forced into a separate method and doesn't mess up your
method. Be aware that debugging that this is happening can be extremely confusing.