Last week we dove into the code that executes when we deallocate Allocator.Temp memory to try to find out what happens. We ended up at a dead-end and were only able to draw conclusions about what doesn’t happen when we deallocate. Today we’ll try another approach to see if we gain gain more insight into the Temp allocator.

Approach

Rather than diving into the C++ IL2CPP output, supporting IL2CPP C++ code, and the assembly they compile to, as we did last week, today we’ll observe the behavior of Allocator.Temp. This is a similar approach to what we did to figure out how long a Temp allocation lasts.

To design the test and make sense of the results, we need to hypothesize how Allocator.Temp works. As before, the idea is that it is backed by a “bump allocator.” This is a fixed-size block of memory that is allocated very simply and efficiently. We keep a pointer to the start (S), the end (E), and the current memory to allocate (C). Allocating N bytes is as simple as return C += N as long as there are at least N bytes available in the block. If not, an alternative allocator must be used to handle the overflow.

Deallocating a single allocation is not supported for allocations within the block and is delegated to the alternative allocator for overflow allocations. Deallocating all allocations, as we theorize happens after every frame, is as trivial as C = S.

In short, a bump allocator looks something like this pseudo-code:

public struct BumpAllocator
{
    private byte* S;
    private byte* E;
    private byte* C;
 
    public void* Allocate(int n)
    {
        // At least 'n' bytes available
        if (E - C > n)
        {
            return C += n;
        }
 
        // Overflow
        return PersistentAllocator.Allocate(n);
    }
 
    public void Deallocate(void* p)
    {
        // Only deallocate overflow allocations
        if (p < S || p > E)
        {
            PersistentAllocator.Deallocate(p);
        }
    }
 
    public void Clear()
    {
        C = S;
    }
}
Test Design

With this bump allocator hypothesis, we can design a test to see if Allocator.Temp behaves as we expect. As we’ve seen before, the distance between allocations is a constant 16 for the first 819 four-byte allocations. This appears to be the point where the fixed-sized memory block overflows and an alternative allocator is used.

So if we were to allocate 819 four-byte allocations and then deallocate them all, the deallocation should be a no-op and have no effect. The 820th allocation should still use the alternative allocator and we should see a non-16 distance. If, however, we see a 16 distance then we know that deallocation is having an observable effect. In this case it might be worth calling as opposed to letting Unity clear the allocations every frame. It might also mean that Allocator.Temp isn’t backed by a bump allocator.

Implementing this test is quite straightforward. The following script has a NoDeallocate method that simply allocates 820 times and prints the distance between the last two allocations. It also has a Deallocate method that allocates 819 times, deallocates them all, then allocates one more time (the 820th) and prints the distance between the last two allocations. A ShouldDeallocate boolean field allows for easy swapping between the two methods in the Unity editor.

using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
   public bool ShouldDeallocate;
 
   void Start()
   {
      if (ShouldDeallocate)
      {
         Deallocate();
      }
      else
      {
         NoDeallocate();
      }
   }
 
   private static unsafe void NoDeallocate()
   {
      // Allocate enough to run out of Temp space
      void*[] allocs = new void*[820];
      for (int i = 0; i < allocs.Length; ++i)
      {
         allocs[i] = UnsafeUtility.Malloc(4, 4, Allocator.Temp);
      }
 
      // Deallocate
      for (int i = 0; i < allocs.Length; ++i)
      {
         UnsafeUtility.Free(allocs[i], Allocator.Temp);
      }
 
      // Report distance of last allocation from second-last allocation
      ulong lastBegin = (ulong)allocs[allocs.Length - 1];
      ulong secondLastEnd = (ulong)allocs[allocs.Length - 2] + 4;
      ulong distance = lastBegin - secondLastEnd;
      print(distance);
   }
 
   private static unsafe void Deallocate()
   {
      // Allocate ALMOST enough to run out of Temp space
      void*[] allocs = new void*[820];
      int endIndex = allocs.Length - 1;
      for (int i = 0; i < endIndex; ++i)
      {
         allocs[i] = UnsafeUtility.Malloc(4, 4, Allocator.Temp);
      }
 
      // Deallocate
      for (int i = 0; i < endIndex; ++i)
      {
         UnsafeUtility.Free(allocs[i], Allocator.Temp);
      }
 
      // Allocate one more time to run out of Temp space
      allocs[endIndex] = UnsafeUtility.Malloc(4, 4, Allocator.Temp);
 
      // Deallocate the last allocation
      UnsafeUtility.Free(allocs[endIndex], Allocator.Temp);
 
      // Report distance of last allocation from second-last allocation
      ulong lastBegin = (ulong)allocs[endIndex];
      ulong secondLastEnd = (ulong)allocs[endIndex - 1] + 4;
      ulong distance = lastBegin - secondLastEnd;
      print(distance);
   }
}
Test Results

Running NoDeallocate in Unity 2019.2.15f1 on macOS 10.14.6 five times yields these distances between the last two allocations:

18446603425094060916
18446603425849023348
18446603425093441108
18446603425849023348
18446603425848713444

Running Deallocate five times yields these distances:

18446603425094060916
18446603425093596324
18446603425848558756
18446603425094060916
18446603425093751012
Conclusion

The exact distances of the last two allocations vary quite a lot, but all of them are decidedly not 16. This means that deallocating all of the 819 allocations has had no effect on the 820th allocation. It was effectively a no-op, even though a fair amount of code is executed. While this doesn’t prove that a bump allocator is being used, it does seem likely given the constant allocation distances, constant point at which it seems to switch to an overflow allocator, and no-op deallocator.

Continue to part three