-
Notifications
You must be signed in to change notification settings - Fork 1
Tips & Tricks
There are a number of cases where at first glance you can't use Jobs or Burst, but with a bit of cleverness you can work around it. This page is meant to document those tricks.
You can't directly pass a managed reference to a job, Unity won't let you. What you can instead do is pass a handle type that lets you recover the reference to the object.
You can use an ObjectHandle<T> type like this to make that easy
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Unity.Jobs;
/// <summary>
/// A helper struct that allows you to pass managed objects to jobs.
/// </summary>
/// <typeparam name="T"></typeparam>
public struct ObjectHandle<T>(T value) : IDisposable
where T : class
{
GCHandle handle = GCHandle.Alloc(value);
public T Target
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => (T)handle.Target;
}
public bool IsAllocated => handle.IsAllocated;
public void Dispose() => handle.Free();
public void Dispose(JobHandle job)
{
new DisposeJob { handle = this }.Schedule(job);
}
struct DisposeJob : IJob
{
public ObjectHandle<T> handle;
public void Execute() => handle.Dispose();
}
}You can't do this. What you can do, though, is pin the array first and then pass the pinned array pointer to the job. Just make sure to unpin it after the job is complete.
struct FreeHandleJob(ulong gchandle) : IJob
{
public void Execute() => UnsafeUtility.ReleaseGCObject(gchandle);
}
T[] array = ...;
void* ptr = UnsafeUtility.PinGCArrayAndGetDataAddress(out var gchandle);
NativeArray<T> narray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(ptr, array.Length, Allocator.Invalid);
var handle = new YourJob(array).Schedule();
new FreeHandleJob(gchandle).Schedule(handle);Now your job only uses unmanaged arrays and can be burst-compiled.
In general the UnsafeUtility class has a number of useful helpers for doing low-level operations.
LLVM's (and therefore Burst's) autovectorization is pretty good. A simple for loop that just does basic math will be vectorized pretty easily. One thing it cannot vectorize though, is external math calls like sin, cos, exp, sqrt, etc. Unity.Mathematics, however, comes with a wide variety of vectorized variants of these functions.
If your bottleneck is evaluating trig/exponential functions then massaging your problem so that it can use a vectorized variant can easily give you another 3-4x speedup.
The Unity.Jobs docs tell you how to create a custom job type but they don't tell you how to make it burst-compilable.
The trick is add a [JobProducerType] annotation that points towards the "actual" job struct. Something like this:
[JobProducerType(typeof(ICustomJobExtensions.JobStruct<>))]
public interface ICustomJob
{
public void Execute();
}
public static class ICustomJobExtensions
{
internal struct JobStruct<T>
where T : struct, ICustomJob
{
public static void Execute(ref T data, ...) { ... }
}
}You then implement the rest of ICustomJobExtensions like is documented in the unity guide. It is also helpful to look at how IJob and IJobParallelFor are implemented.
Caution
If you are expecting other mod DLLs to use your custom interface you will need to make sure that your AssemblyVersion is fixed. If you have another dll that uses your interface then the burst-compiled method will contain the version that that downstream dll was compiled against, so bumping your DLL version will mean that the burst-compiled method won't be found.
As a more detailed example:
- You have a custom job type in A.dll v1.0.0
- Mod B uses that custom job type and builds against A.dll v1.0.0
- You then publish A.dll v1.1.0
- The burst compiled methods in B for that custom job type will no longer be found.
One further trick is that the Execute method defined above doesn't actually have to be the main method for a job. The burst compiler will compile it no matter what the parameters are, so you can use this to create custom interfaces that are burst-compiled. You'll need to manually get and call the Execute method, but that is easy enough to wrap up in a nice interface. The only extra thing you need to do here is add a [BurstCompile] attribute on the Execute method so that BurstCompiler.CompileFunctionPointer doesn't throw an exception.
You can see an example of this in BurstPQS.