When we use Allocator.Temp with a collection like NativeArray, how long does the allocation last? We’ve seen that Temp allocations are automatically disposed without the need to explicitly call Dispose, but when does the automatic dispose happen? Today we’ll test to find out!

Goal

The goal of the test is to allocate some native memory with Allocator.Temp and see how many frames and how much time it takes for Unity to deallocate it without us calling Dispose manually. Even five major releases since 2018.1, Unity’s documentation is still extremely vague on the topic, saying only this:

Temporary allocation.

Test Design

We’ll allocate using Allocator.Temp in one MonoBehaviour message and check for disposal in another. To keep the time between allocation and dispose check as low as possible, we’ll choose to allocate toward the end of the frame and check toward the beginning of the frame. Specifically, we’ll allocate in LateUpdate and check in FixedUpdate.

Next, we have to decide how to check for disposal. One easy way is to try to write to the NativeArray that we allocated with Allocator.Temp. If its memory has been deallocated, an InvalidOperationException will be thrown. Putting these two parts together gives us this test:

using System;
using System.Diagnostics;
using Unity.Collections;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
    private int allocFrame;
    private NativeArray<int> array;
    private Stopwatch stopwatch;
    private bool isDone;
 
    void LateUpdate()
    {
        if (!isDone && !array.IsCreated)
        {
            allocFrame = Time.frameCount;
            array = new NativeArray<int>(1, Allocator.Temp);
            stopwatch = Stopwatch.StartNew();
        }
    }
 
    void FixedUpdate()
    {
        if (!isDone && array.IsCreated)
        {
            try
            {
                array[0] = 123;
            }
            catch (InvalidOperationException)
            {
                long millisToDealloc = stopwatch.ElapsedMilliseconds;
                int deallocFrame = Time.frameCount;
                int framesToDealloc = deallocFrame - allocFrame;
                print(
                    "Allocation frame: " + allocFrame + "n"
                    + "Deallocation frame: " + deallocFrame + "n"
                    + "Frames to deallocate: " + framesToDealloc + "n"
                    + "Millis to deallocate: " + millisToDealloc);
                stopwatch = null;
                isDone = true;
            }
        }
    }
}

Running this on macOS 10.14.6 in the Unity 2019.2.15f1 editor, we get the following report:

Allocation frame: 1
Deallocation frame: 2
Frames to deallocate: 1
Millis to deallocate: 1

There we have it: Temp allocations appear to be automatically disposed at the end of the frame. At least in the editor, but let’s check in a standalone build:

 

Because standalone builds have all the native collections safety checks removed for performance reasons, we never get any output.

Safety Digression

This is probably also a good time to reflect on the safety ramifications of native collections. Consider what we’re doing here:

  1. Allocate memory
  2. Don’t call Dispose
  3. Memory is automatically disposed by Unity
  4. Write to (disposed) memory
  5. No warning or error, just data corruption

Hopefully all of the game's use-after-dispose bugs will be caught while the safety checks are turned on in the editor, but they might not. The usual null reference and array bounds checking of the managed C# world is not present with native collections. On one hand this improves performance. On the other it allows for data corruption. Be careful!

Test Design v2

Since we really want to be able to confirm in a real build, not just the editor, we'll need to tweak the test so that we don't rely on the safety checks to throw an exception when we write to the memory after it's automatically disposed. There's no API for us to query when Allocator.Temp allocations have been disposed, so we'll have to come up with something indirect.

The theory behind this version of the test is that Allocator.Temp is backed by a "bump allocator." This is a block of memory that is simply allocated linearly by "bumping" a pointer forward by the number of bytes allocated. Deallocating a single allocation is a no-op. Instead, the entire block is deallocated at once by simply moving the pointer back to the beginning.

If this theory is correct, we should be able to perform the following test:

  1. Allocate memory
  2. Memory is automatically disposed by Unity
  3. Allocate memory
  4. Compare the allocated memory addresses from #1 and #3. If they're the same, the "bump allocator" was reset due to automatic disposal.

The Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtilityGetUnsafePtr extension method allows us to get the memory address of the memory allocated by NativeArray. So all we need to do is save that and keep allocating every frame until we get one that matches. Here's how the test looks:

using System.Diagnostics;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
 
unsafe class TestScript : MonoBehaviour
{
    private void* arrayPtr;
    private int allocFrame;
    private Stopwatch stopwatch;
    private bool isDone;
 
    void LateUpdate()
    {
        if (!isDone && arrayPtr == null)
        {
            allocFrame = Time.frameCount;
            arrayPtr = new NativeArray<int>(1, Allocator.Temp).GetUnsafePtr();
            stopwatch = Stopwatch.StartNew();
        }
    }
 
    void FixedUpdate()
    {
        if (!isDone)
        {
            void* ptr = new NativeArray<int>(1, Allocator.Temp).GetUnsafePtr();
            if (ptr == arrayPtr)
            {
                long millisToDealloc = stopwatch.ElapsedMilliseconds;
                int deallocFrame = Time.frameCount;
                int framesToDealloc = deallocFrame - allocFrame;
                print(
                    "Allocation frame: " + allocFrame + "n"
                    + "Deallocation frame: " + deallocFrame + "n"
                    + "Frames to deallocate: " + framesToDealloc + "n"
                    + "Millis to deallocate: " + millisToDealloc);
                isDone = true;
            }
        }
    }
}

Running this in the editor or as a standalone build yields this report:

Allocation frame: 1
Deallocation frame: 2
Frames to deallocate: 1
Millis to deallocate: 26
Conclusion

Allocator.Temp appears to be backed by a "bump allocator" that is cleared between every frame. Whether you call Dispose or not, you should only read and write to memory allocated by it during the same frame that you allocated it. Starting with the next frame, you'll either get an exception or data corruption.