Quick answer: Burst defaults to FloatMode.Fast which allows FMA fusion and reassociation. Annotate your job with [BurstCompile(FloatPrecision.High, FloatMode.Strict)] to match the managed result bit-for-bit.
You ran a Burst job that computes player physics and compared its output against a managed C# reference for unit testing. The two agreed on most values but diverged on a few by 1e−7. Multiplied across a few thousand frames of integration, the discrepancy compounded into noticeable positional drift — enough to break lockstep multiplayer.
Why Burst and Managed Diverge
Burst is an LLVM-based ahead-of-time compiler. By default, it enables aggressive floating-point optimizations:
- FMA fusion:
a * b + cbecomes a single fused multiply-add (one rounding step instead of two). - Reassociation:
(a + b) + cmay be reordered toa + (b + c); with non-zero round-off, the results differ. - Reciprocal substitution:
x / ybecomesx * (1/y), with the reciprocal approximated.
The managed (Mono/IL2CPP) pipeline does none of these. It emits the literal IL operations defined in the source, so a * b + c performs a multiply and an add with two rounding steps. The two results differ in the lowest mantissa bits — below the threshold of single-step visibility but above the threshold of cumulative integration error.
The Fix: Strict FloatMode
using Unity.Burst;
using Unity.Jobs;
using Unity.Collections;
[BurstCompile(FloatPrecision.High, FloatMode.Strict)]
public struct PhysicsStepJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> velocities;
public NativeArray<float3> positions;
public float deltaTime;
public void Execute(int index)
{
positions[index] += velocities[index] * deltaTime;
}
}
FloatPrecision.High disables fast reciprocals and trigonometric approximations; FloatMode.Strict disables FMA fusion and reassociation. Performance drops, typically 10–30% on a math-bound job, but the output matches managed C# exactly.
Per-Operation Control
If only a few operations need strict semantics, you can wrap them locally instead of changing the whole job:
[BurstCompile(FloatMode.Fast)]
public struct MixedJob : IJob
{
public NativeArray<float> data;
public void Execute()
{
// Fast for bulk math
for (int i = 0; i < data.Length; i++)
data[i] = math.sin(data[i]);
// Strict for a critical hash step
data[0] = StrictMath.PreciseAdd(data[0], 1.0f);
}
}
[BurstCompile(FloatMode.Strict)]
public static class StrictMath
{
public static float PreciseAdd(float a, float b) => a + b;
}
Burst applies the attribute on the calling function’s static class, so StrictMath.PreciseAdd compiles under strict rules even when called from a Fast-mode job.
Audit for Other Determinism Issues
Strict FloatMode is necessary but not sufficient for cross-platform determinism. Also check:
- Different SIMD widths: Burst auto-vectorizes for the target CPU. SSE4.2 and AVX2 produce different bit patterns from horizontal reductions. Use
[BurstCompile(CompileSynchronously = true, OptimizeFor = OptimizeFor.Performance)]with explicit instruction-set targeting, or disable vectorization for hot paths. - Math library: Use
Unity.Mathematicsconsistently. Mixingmath.sinwithSystem.Math.Sinacross job and managed paths yields different results because they call different implementations. - Order of iteration:
IJobParallelFordoesn’t guarantee execution order; if your math is order-sensitive (e.g., reduction sums), useIJobfor the order-dependent step.
Verifying
Run a unit test that executes the same operation managed and Burst-compiled, then compares with bitwise equality:
[Test]
public void PhysicsStep_ManagedAndBurst_Match()
{
var managed = ManagedReference(input);
var burst = RunBurstJob(input);
Assert.That(BitConverter.DoubleToInt64Bits(managed),
Is.EqualTo(BitConverter.DoubleToInt64Bits(burst)));
}
Bitwise comparison rules out cases where values are “close” but not identical.
“Fast Burst gives you speed; Strict Burst gives you determinism. Pick the one that matches your needs.”
Lockstep multiplayer demands Strict mode from day one. Cosmetic effects can stay Fast.