Documentation

Unity Engine


User Manual

Script Reference

Unity Engine


Introduction to memory aliasing

Read time 4 minutesLast updated 13 days ago

Memory aliasing provides additional information to Burst about how your code uses data, which Burst can use to generate more efficient runtime code.
Memory aliasing happens when locations in the memory overlap each other.
The following example shows a job that copies data from an input array to an output array:
[BurstCompile]private struct CopyJob : IJob{ [ReadOnly] public NativeArray<float> Input; [WriteOnly] public NativeArray<float> Output; public void Execute() { for (int i = 0; i < Input.Length; i++) { Output[i] = Input[i]; } }}

No memory aliasing

If the arrays
Input
and
Output
don't overlap, which means that their respective memory location doesn't overlap, the code returns the following result after running this job on a sample input/output:

Memory with no aliasing

If Burst has the required noalias information, it can work at the scalar level to optimize the previous scalar loop. It does this through a process called vectorizing, where it rewrites the loop to process elements in a small batch. For example, Burst could work at vector level in 4 by 4 elements:

Memory with no aliasing vectorized

Memory aliasing

If the
Output
array overlaps the
Input
array by one element (for example
Output[0]
points to
Input[1]
), then this means that the memory is aliasing. This gives the following result when you run
CopyJob
without the auto vectorizer:

Memory with aliasing

If Burst isn't aware of the memory aliasing, it tries to auto-vectorize the loop, which results in the following:

Memory with aliasing and invalid vectorized code

The result of this code is invalid and might lead to bugs if Burst can't identify them.

Generated code

In the
CopyJob
example, there is an
x64
assembly targeted at
AVX2
in its loop. The instruction
vmovups
moves 8 floats, so a single auto vectorized loop moves 4 × 8 floats, which equals 32 floats copied per loop iteration, instead of just one:
.LBB0_4: vmovups ymm0, ymmword ptr [rcx - 96] vmovups ymm1, ymmword ptr [rcx - 64] vmovups ymm2, ymmword ptr [rcx - 32] vmovups ymm3, ymmword ptr [rcx] vmovups ymmword ptr [rdx - 96], ymm0 vmovups ymmword ptr [rdx - 64], ymm1 vmovups ymmword ptr [rdx - 32], ymm2 vmovups ymmword ptr [rdx], ymm3 sub rdx, -128 sub rcx, -128 add rsi, -32 jne .LBB0_4 test r10d, r10d je .LBB0_8
The following example shows the same Burst compiled loop, but Burst's aliasing is artificially disabled:
.LBB0_2: mov r8, qword ptr [rcx] mov rdx, qword ptr [rcx + 16] cdqe mov edx, dword ptr [rdx + 4*rax] mov dword ptr [r8 + 4*rax], edx inc eax cmp eax, dword ptr [rcx + 8] jl .LBB0_2
The result is entirely scalar and runs approximately 32 times slower than the highly optimized, vectorized variant that the original alias analysis produces.

Function cloning

For function calls where Burst is aware of the aliasing between parameters to the function, Burst can infer the aliasing. It can then propagate this onto the called function to improve optimization:
[MethodImpl(MethodImplOptions.NoInlining)]int Bar(ref int a, ref int b){ a = 42; b = 13; return a;}int Foo(){ var a = 53; var b = -2; return Bar(ref a, ref b);}
The assembly for
Bar
would be:
mov dword ptr [rcx], 42mov dword ptr [rdx], 13mov eax, dword ptr [rcx]ret
This is because Burst doesn't know the aliasing of
a
and
b
within the
Bar
function. This is in line with what other compiler technologies do with this code snippet.
However, Burst is more advanced than this. Through a process of function cloning, Burst creates a copy of
Bar
and discovers that the aliasing properties of
a
and
b
don't alias. It then replaces the original call to
Bar
with a call to the copy. This results in the following assembly:
mov dword ptr [rcx], 42mov dword ptr [rdx], 13mov eax, 42ret
In this scenario, Burst doesn't perform the second load from
a
.

Aliasing checks

Because aliasing is key to Burst's ability to optimize for performance, there are some aliasing intrinsics:
The following example demonstrates use of these intrinsics:
using static Unity.Burst.CompilerServices.Aliasing;[BurstCompile]private struct CopyJob : IJob{ [ReadOnly] public NativeArray<float> Input; [WriteOnly] public NativeArray<float> Output; public unsafe void Execute() { // NativeContainer attributed structs (like NativeArray) cannot alias with each other in a job struct! ExpectNotAliased(Input.GetUnsafePtr(), Output.GetUnsafePtr()); // NativeContainer structs cannot appear in other NativeContainer structs. ExpectNotAliased(in Input, in Output); ExpectNotAliased(in Input, Input.GetUnsafePtr()); ExpectNotAliased(in Input, Output.GetUnsafePtr()); ExpectNotAliased(in Output, Input.GetUnsafePtr()); ExpectNotAliased(in Output, Output.GetUnsafePtr()); // But things definitely alias with themselves! ExpectAliased(in Input, in Input); ExpectAliased(Input.GetUnsafePtr(), Input.GetUnsafePtr()); ExpectAliased(in Output, in Output); ExpectAliased(Output.GetUnsafePtr(), Output.GetUnsafePtr()); }}
These checks only run when optimizations are enabled, because proper aliasing deduction is intrinsically linked to the optimizer's ability to see through functions via inlining.

Additional resources