How Long Does a Temp Allocation Last?
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:
- Allocate memory
- Don’t call
Dispose
- Memory is automatically disposed by Unity
- Write to (disposed) memory
- 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:
- Allocate memory
- Memory is automatically disposed by Unity
- Allocate memory
- 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.
#1 by benjamin guihaire on December 23rd, 2019 ·
I am guessing the same behavior is applied when allocating temp memory on a different thread (not on the main simulation thread), so knowing the behavior is critical here.