Skip to content

Tips & Tricks

Sean Lynch edited this page Apr 9, 2026 · 4 revisions

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.

Passing a managed reference to a job

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();
    }
}

Using a managed array in a job

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.

Even better performance by using Unity.Mathematics

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.

Creating a burst-compilable custom job

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.

Creating a custom burst-compilable interface

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.