Temp memory is backed by a fixed size block that’s cleared by Unity every frame. Allocations on subsequent frames return pointers to this same block. The allocated memory therefore isn’t unique. How much of a problem is this? Today we’ll do some experiments to find out!

Outside of a Job

First up, let’s try allocating Temp memory from outside of a job. We’ll allocate once in Start and write 123 to the allocated memory. Then we’ll allocate once every Update. If the pointer to the newly-allocated memory matches the pointer to the first memory allocation, we’ll print the frame the reuse was detected on and the value stored at the allocation.

using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
 
unsafe class TestScript : MonoBehaviour
{
    private void* first;
 
    private void Start()
    {
        first = UnsafeUtility.Malloc(4, 4, Allocator.Temp);
        *(int*)first = 123;
    }
 
    private void Update()
    {
        void* alloc = UnsafeUtility.Malloc(4, 4, Allocator.Temp);
        if (alloc == first)
        {
            print("Reused on frame " + Time.frameCount + ": " + *(int*)alloc);
        }
    }
}

Running this on Unity 2019.3.0f6, we see a print on every single frame starting at 1:

Reused on frame 1: 123
Reused on frame 2: 123
Reused on frame 3: 123
Reused on frame 4: 123
Reused on frame 5: 123
...

The value we read is always 123, indicating that Unity isn’t clearing the contents of the fixed size block, such as by setting all the bytes to zero, but rather just resetting its internal tracking of the next byte to allocate. This is faster since there’s no need to write zeroes to the whole block, which is about 16 KB, but it may cause more subtle bugs to creep in.

For example, a game might inadvertently rely on Temp memory not being cleared across frames only to later have a seemingly-unrelated change to the code cause that memory to be overwritten by writing to another allocation. In the case of the above script, imagine writing 456 to alloc and then later reading first and expecting it to still be 123. Because the allocated memory was reused, the problem of aliasing has been introduced and can be quite difficult to debug.

NativeArray

Next up, we’ll use NativeArray to perform the allocation. We won’t use UnsafeUtility.Malloc or deal with raw pointers anymore.

This script has the same elements as the previous one: an initial allocation in Start followed by an allocation every Update. We can compare the allocations using the overloaded == operator in NativeArray. We’ll also pass NativeArrayOptions.UninitializedMemory to the NativeArray constructor so as to not clear out the contents of the array.

using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
    private NativeArray<int> first;
 
    private void Start()
    {
        first = new NativeArray<int>(1, Allocator.Temp);
        first[0] = 123;
    }
 
    private void Update()
    {
        NativeArray<int> alloc = new NativeArray<int>(
            1,
            Allocator.Temp,
            NativeArrayOptions.UninitializedMemory);
        if (alloc == first)
        {
            print("Reused on frame " + Time.frameCount + ": " + alloc[0]);
        }
    }
}

This prints the same messages as before:

Reused on frame 1: 123
Reused on frame 2: 123
Reused on frame 3: 123
Reused on frame 4: 123
Reused on frame 5: 123
...

The reason is simple if the programmer knows how the NativeArray constructor is implemented. Here’s a decompiled version:

public unsafe NativeArray(int length, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory)
{
    NativeArray<T>.Allocate(length, allocator, out this);
    if ((options & NativeArrayOptions.ClearMemory) != NativeArrayOptions.ClearMemory)
        return;
    UnsafeUtility.MemClear(this.m_Buffer, (long) this.Length * (long) UnsafeUtility.SizeOf<T>());
}
 
private static unsafe void Allocate(int length, Allocator allocator, out NativeArray<T> array)
{
    long size = (long) UnsafeUtility.SizeOf<T>() * (long) length;
    if (allocator <= Allocator.None)
        throw new ArgumentException("Allocator must be Temp, TempJob or Persistent", nameof (allocator));
    if (length < 0)
        throw new ArgumentOutOfRangeException(nameof (length), "Length must be >= 0");
    NativeArray<T>.IsUnmanagedAndThrow();
    if (size > (long) int.MaxValue)
        throw new ArgumentOutOfRangeException(nameof (length), string.Format("Length * sizeof(T) cannot exceed {0} bytes", (object) int.MaxValue));
    array = new NativeArray<T>();
    array.m_Buffer = UnsafeUtility.Malloc(size, UnsafeUtility.AlignOf<T>(), allocator);
    array.m_Length = length;
    array.m_AllocatorLabel = allocator;
    array.m_MinIndex = 0;
    array.m_MaxIndex = length - 1;
    DisposeSentinel.Create(out array.m_Safety, out array.m_DisposeSentinel, 1, allocator);
}

The first line calls NativeArray<T>.Allocate which calls UnsafeUtility.Malloc with the Allocator.Temp parameter we passed into the constructor. Because we didn’t pass NativeArrayOptions.ClearMemory for the contructor’s options parameter, UnsafeUtility.MemClear wasn’t called to clear out the memory.

Essentially, we’ve recreated what the first script did using NativeArray. The printed messages are therefore the same. That said, there are some interesting behaviors to note here.

Reading the contents of the NativeArray created in Update with alloc[0] worked every time. No exception was thrown from any of the “sentinels” or “safeties” that NativeArray uses. Specifically, here’s how the indexer looks:

public unsafe T this[int index]
{
    get
    {
        this.CheckElementReadAccess(index);
        return UnsafeUtility.ReadArrayElement<T>(this.m_Buffer, index);
    }
    [WriteAccessRequired] set
    {
        this.CheckElementWriteAccess(index);
        UnsafeUtility.WriteArrayElement<T>(this.m_Buffer, index, value);
    }
}
 
internal AtomicSafetyHandle m_Safety;
 
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
private unsafe void CheckElementReadAccess(int index)
{
    if (index < this.m_MinIndex || index > this.m_MaxIndex)
        this.FailOutOfRangeError(index);
    AtomicSafetyHandleVersionMask* versionNode = (AtomicSafetyHandleVersionMask*) (void*) this.m_Safety.versionNode;
    if ((this.m_Safety.version & AtomicSafetyHandleVersionMask.Read) != ~(AtomicSafetyHandleVersionMask.WriteInv | AtomicSafetyHandleVersionMask.Write) || this.m_Safety.version == (*versionNode & AtomicSafetyHandleVersionMask.WriteInv))
        return;
    AtomicSafetyHandle.CheckReadAndThrowNoEarlyOut(this.m_Safety);
}

AtomicSafetyHandle doesn’t protect us from reading memory that’s been allocated to another NativeArray. The protection Unity offers is that we’re no longer allowed to write to first after it’s implicitly disposed at the end of the frame. To see what happens, let’s add a write before the print:

first[0] = 123;
print("Reused on frame " + Time.frameCount + ": " + alloc[0]);

Now we get this exception from first[0] = 123:

InvalidOperationException: The NativeArray has been deallocated, it is not allowed to access it
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckWriteAndThrowNoEarlyOut (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) <0x160398f10 + 0x00052> in <96c9baffa9414fe5b4eb911e74f8f90e>:0
Unity.Collections.NativeArray`1[T].CheckElementWriteAccess (System.Int32 index) (at /Users/builduser/buildslave/unity/build/Runtime/Export/NativeArray/NativeArray.cs:132)
Unity.Collections.NativeArray`1[T].set_Item (System.Int32 index, T value) (at /Users/builduser/buildslave/unity/build/Runtime/Export/NativeArray/NativeArray.cs:147)
TestScript.Update () (at Assets/TestScript.cs:69)

This guarantees that the memory allocation is only accessible by NativeArray instances for the frame it was allocated. A NativeArray created with NativeArrayOptions.UninitializedMemory on a future frame might rely on this old memory, but this is the cost of that option.

Allocations in Jobs

Finally, let’s allocate Temp memory from within a job, do several frames of pointless work, then allocate again. We’ll store these pointers in a NativeArray allocated with Persistent. A script will run the job asynchronously, and check on its status every Update. When the job is done, we’ll print the frame it was done on, the two allocated pointers, and the distance between them.

using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using UnityEngine;
 
[BurstCompile]
unsafe struct TestJob : IJob
{
    public NativeArray<long> Reused;
 
    public void Execute()
    {
        // First allocation
        Reused[0] = (long)UnsafeUtility.Malloc(4, 4, Allocator.Temp);
 
        // Do several frames of pointless work
        for (int i = 0; i < 1000000000; ++i)
        {
            if (Reused[0] == 0)
            {
                Reused[0]++;
            }
        }
 
        // Second allocation
        Reused[1] = (long)UnsafeUtility.Malloc(4, 4, Allocator.Temp);
    }
}
 
class TestScript : MonoBehaviour
{
    private NativeArray<long> reused;
    private JobHandle handle;
    private bool isDone;
 
    private void Start()
    {
        reused = new NativeArray<long>(2, Allocator.Persistent);
        handle = new TestJob { Reused = reused }.Schedule();
    }
 
    private void Update()
    {
        if (!isDone && handle.IsCompleted)
        {
            isDone = true;
            handle.Complete();
            print(
                "\nFrame:    " + Time.frameCount
                + "\nFirst:    " + reused[0]
                + "\nSecond:   " + reused[1]
                + "\nDistance: " + (reused[1] - reused[0] - 4));
        }
    }
 
    private void OnDestroy()
    {
        handle.Complete();
        reused.Dispose();
    }
}

Here’s what this prints:

Frame:    77
First:    5086422048
Second:   5086422080
Distance: 28

The pointless work we had the job do successfully delayed the second allocation by 76 frames. Unity should have cleared the Temp allocator after the first frame and the next 75 frames. However, the second allocation doesn’t match the first allocation so there’s been no reuse. It’s 28 bytes after its end instead of the 16 bytes we saw before, but that’s likely due to the upgrade to Unity 2019.3 since previous articles on this topic.

This points to there being a Temp allocator for jobs that is distinct from the Temp allocator that’s used outside of jobs because we’ve seen in the first script and before that the allocator is cleared every frame.

Conclusion

Raw Temp memory allocation with UnsafeUtility.Malloc outside of a job is dangerous. The memory will be implicitly reclaimed and reallocated later. The resulting aliasing may cause bugs that are difficult to debug and there will be no warnings from Unity to help.

Using native collections like NativeArray with properly-implemented safety checks doesn’t eliminate the potentially-surprising implicit deallocations, but at least Unity will produce warnings in the form of exceptions.

Jobs apparently have their own Temp allocator. It appears to not be cleared every frame like the Temp allocator outside of jobs is. Multi-frame jobs therefore don’t need to worry about reuse of the fixed size block that backs the Temp allocator. They already have plenty to worry about.