Allocating Memory Within a Job: Part 2
Last week’s article came to the conclusion that allocating Temp
memory from within a job was safe. This week we’ll look into that a little deeper to find out that it might not be as safe as it looks!
A comment from Benjamin Guihaire on last week’s article alerts us to danger allocating Temp
memory from within a job:
Careful with Allocator.Temp if the job is a lazy job that can last more than one frame !
We had only tested jobs that took less than one frame to complete. Since they appear to be automatically cleared every frame, any job lasting longer than one frame might use memory that’s been returned to the allocator to be re-allocated and used by subsequent allocations. These conflicts would cause data corruption and potentially be very difficult to debug.
So let’s design a basic job that lasts more than one frame and see how it behaves. In fact, we’ll make a job with an infinite loop just to ensure that it runs for many frames. We’ll also allocate 820 4-byte blocks of 4-byte aligned Temp
memory to exhaust the fixed-sized block of memory it allocates from and include one “overflow” allocation by the alterate allocator.
using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; using UnityEngine; [BurstCompile] unsafe struct TestJob : IJob { public void Execute() { for (int i = 0; i < 820; ++i) { UnsafeUtility.Malloc(4, 4, Allocator.Temp); } while (true) { } } } class TestScript : MonoBehaviour { private void Start() { new TestJob().Schedule(); } }
We can enter play mode in the editor and then exit just fine. Entering play mode a second time hangs the editor forever while it waits for the job to complete, which obviously never happens due to the infinite loop.
Now let’s add a little bit more to the test just to check every frame if the job is completed. We know it won’t be, but we’re proceeding with caution and adding just a little bit at a time here.
class TestScript : MonoBehaviour { private JobHandle handle; private void Start() { handle = new TestJob().Schedule(); } private void Update() { bool _ = handle.IsCompleted; } }
Notice that the return value of handle.IsCompleted
isn’t even used. We’re just calling the property and throwing away whatever status it returns.
When we run this, now Unity continually prints warnings in the Console pane:
Internal: JobTempAlloc has allocations that are more than 4 frames old - this is not allowed and likely a leak To Debug, enable the define: TLA_DEBUG_STACK_LEAK in ThreadsafeLinearAllocator.cpp. This will output the callstacks of the leaked allocations
This is interesting for two reasons! First, why does calling handle.IsCompleted
and discarding the return value have any effect at all on the program? What could it possibly have to do with memory allocators? Looking at a decompilation of it, we see this:
/// <summary> /// <para>Returns false if the task is currently running. Returns true if the task has completed.</para> /// </summary> public bool IsCompleted { get { return JobHandle.ScheduleBatchedJobsAndIsCompleted(ref this); } }
Since this just calls JobHandle.ScheduleBatchedJobsAndIsCompleted
, let’s look a few lines down and see it:
[NativeMethod(IsFreeFunction = true)] [MethodImpl(MethodImplOptions.InternalCall)] private static extern bool ScheduleBatchedJobsAndIsCompleted(ref JobHandle job);
As an extern
function, this isn’t defined in .NET code. It’s implemented inside the Unity engine in closed-source code. Unfortunately, this means we won’t be able to easily figure out why just calling handle.IsCompleted
matters.
The second reason these warning messages are interesting is that we see them refer to JobTempAlloc
. We also see these same warnings when directly using the TempJob
allocator. This indicates clearly what we’ve expected for several articles and about three months now: the alternate allocator that handles “overflow” allocations for the Temp
allocator is the TempJob
allocator!
Now let’s try changing the job so that it deallocates every allocation to see what effect that has:
[BurstCompile] unsafe struct TestJob : IJob { public void Execute() { for (int i = 0; i < 820; ++i) { void* alloc = UnsafeUtility.Malloc(4, 4, Allocator.Temp); UnsafeUtility.Free(alloc, Allocator.Temp); } while (true) { } } }
Entering play mode still prints all the same warning messages about JobTempAlloc
. Even immediately deallocating all of the allocations, not just the final “overflow” allocation, isn’t enough to stop the warnings. Do we need to deallocate the overflow allocatin by passing TempJob
to UnsafeUtility.Free
? Let’s try that:
[BurstCompile] unsafe struct TestJob : IJob { public void Execute() { for (int i = 0; i < 820; ++i) { void* alloc = UnsafeUtility.Malloc(4, 4, Allocator.Temp); UnsafeUtility.Free( alloc, i != 819 ? Allocator.Temp : Allocator.TempJob); } while (true) { } } }
Now when we enter play mode we no longer see any warnings! We can’t rely on the Temp
allocator to properly deallocate from its overflow allocator, so we need to pass TempJob
to UnsafeUtility.Free
to force the deallocation. Since we don’t get any obvious or convenient indication that an allocation was an overflow or not, it’s very unlikely that we’ll be able to write code that passes the correct Allocator
to UnsafeUtility.Free
.
We’ve seen today that allocating memory in jobs that last more than one frame is not a safe operation for several reasons:
- “Overflow” allocations will not be automatically freed, leading to tons of warnings about memory leaks
- “Overflow” allocations will not be freed when passing
Allocator.Temp
toUnsafeUtility.Free
as we’d expect them to be - Passing
Allocator.TempJob
toUnsafeUtility.Free
only for “overflow” allocations is often extremely complex and error-prone - Calling
JobHandle.IsComplete
mysteriously changes the behavior of memory allocation in the job
For these reasons, job code should be very careful when allocating memory. All other jobs need to be examined closely to ensure that “overflow” allocations never occur. If they do, it’s likely that Unity will leak the memory and endlessly print cryptic warning messages about it.