Last week we learned a lot about Allocator.Temp, but we left some questions open. One of them was what happens when we explicitly deallocate Temp memory. We know we don’t need to and that it’ll be deallocated at the end of the frame, but what happens when we explicitly deallocate it? Today we’ll dive in and try to find out.

Let’s start with a tiny C# script just to call Dispose:

using Unity.Collections;
using UnityEngine;
 
class TestScript : MonoBehaviour
{
    public NativeArray<int> Array;
 
    void Start()
    {
        Array.Dispose();
    }
}

Then we’ll use Unity 2019.2.15f1 to build for iOS and open the Xcode project. In Assembly-CSharp.cpp, we find the C++ function equivalent of TestScript.Dispose that IL2CPP generated:

IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void TestScript_Start_m37FFC63451F28F640DBC8E81F6921CA912BCF9BE (TestScript_t292BEAEA5C665F1E649B7CCA16D364E5E836A4D1 * __this, const RuntimeMethod* method)
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_method (TestScript_Start_m37FFC63451F28F640DBC8E81F6921CA912BCF9BE_MetadataUsageId);
        s_Il2CppMethodInitialized = true;
    }
    {
        NativeArray_1_tC6374EC584BF0D6DD4AD6FA0FD00C2C82F82CCAF * L_0 = __this->get_address_of_Array_4();
        NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54((NativeArray_1_tC6374EC584BF0D6DD4AD6FA0FD00C2C82F82CCAF *)L_0, /*hidden argument*/NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54_RuntimeMethod_var);
        return;
    }
}

The beginning static bool and if is just the usual IL2CPP method overhead and the end has the Dispose call, named NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54:

inline void NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54 (NativeArray_1_tC6374EC584BF0D6DD4AD6FA0FD00C2C82F82CCAF * __this, const RuntimeMethod* method)
{
    ((  void (*) (NativeArray_1_tC6374EC584BF0D6DD4AD6FA0FD00C2C82F82CCAF *, const RuntimeMethod*))NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54_gshared)(__this, method);
}

This function just wraps a call to the function pointed at by NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54_gshared. Looking around the Xcode project, we find it in Generics9.cpp:

IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54_gshared (NativeArray_1_tC6374EC584BF0D6DD4AD6FA0FD00C2C82F82CCAF * __this, const RuntimeMethod* method)
{
    {
        void* L_0 = (void*)__this->get_m_Buffer_0();
        int32_t L_1 = (int32_t)__this->get_m_AllocatorLabel_2();
        UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89((void*)(void*)L_0, (int32_t)L_1, /*hidden argument*/NULL);
        __this->set_m_Buffer_0((void*)(((uintptr_t)0)));
        __this->set_m_Length_1(0);
        return;
    }
}

The beginning is a call to UnsafeUtility.Free and the end sets m_Buffer to null and m_Length to 0. The Free call is the interesting part, so let’s look at it in UnityEngine.CoreModule.cpp:

IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR void UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89 (void* ___memory0, int32_t ___allocator1, const RuntimeMethod* method)
{
    typedef void (*UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89_ftn) (void*, int32_t);
    static UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89_ftn _il2cpp_icall_func;
    if (!_il2cpp_icall_func)
    _il2cpp_icall_func = (UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89_ftn)il2cpp_codegen_resolve_icall ("Unity.Collections.LowLevel.Unsafe.UnsafeUtility::Free(System.Void*,Unity.Collections.Allocator)");
    _il2cpp_icall_func(___memory0, ___allocator1);
}

The first part is a one-time call to get a pointer to the UnsafeUtility::Free function in the Unity engine and the second part just calls it. This is visible via a C# source file in the Unity installation directory’s Managed/UnityEngine/UnityEngine.CoreModule.dll. Decompiling it with ILSpy, we see this at the top of the file:

namespace Unity.Collections.LowLevel.Unsafe
{
    /// <summary>
    ///   <para>Unsafe utility class.</para>
    /// </summary>
    [NativeHeader("Runtime/Export/Unsafe/UnsafeUtility.bindings.h")]
    [StaticAccessor("UnsafeUtility", StaticAccessorType.DoubleColon)]
    public static class UnsafeUtility
    {
        [MethodImpl(4096)]
        [ThreadSafe]
        public unsafe static extern void* Malloc(long size, int alignment, Allocator allocator);
 
        /// <summary>
        ///   <para>Free memory.</para>
        /// </summary>
        /// <param name="memory">Memory pointer.</param>
        /// <param name="allocator">Allocator.</param>
        [MethodImpl(4096)]
        [ThreadSafe]
        public unsafe static extern void Free(void* memory, Allocator allocator);

At this point, we can’t see the engine source code without a special license from Unity. However, we can see what everything up to this point looks like when compiled. So let’s jump back to TestScript.Start and look at its assembly from Xcode 11.3:

    push    {r4, r5, r7, lr}
    add r7, sp, #8
    movw    r5, :lower16:(__ZZ58TestScript_Start_m37FFC63451F28F640DBC8E81F6921CA912BCF9BEE25s_Il2CppMethodInitialized-(LPC0_0+4))
    mov r4, r0
    movt    r5, :upper16:(__ZZ58TestScript_Start_m37FFC63451F28F640DBC8E81F6921CA912BCF9BEE25s_Il2CppMethodInitialized-(LPC0_0+4))
LPC0_0:
    add r5, pc
    ldrb    r0, [r5]
    cbnz    r0, LBB0_2
    movw    r0, :lower16:(L_TestScript_Start_m37FFC63451F28F640DBC8E81F6921CA912BCF9BE_MetadataUsageId$non_lazy_ptr-(LPC0_1+4))
    movt    r0, :upper16:(L_TestScript_Start_m37FFC63451F28F640DBC8E81F6921CA912BCF9BE_MetadataUsageId$non_lazy_ptr-(LPC0_1+4))
LPC0_1:
    add r0, pc
    ldr r0, [r0]
    ldr r0, [r0]
    bl  __Z32il2cpp_codegen_initialize_methodj
    movs    r0, #1
    strb    r0, [r5]
LBB0_2:
    movw    r0, :lower16:(L_NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54_RuntimeMethod_var$non_lazy_ptr-(LPC0_2+4))
    movt    r0, :upper16:(L_NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54_RuntimeMethod_var$non_lazy_ptr-(LPC0_2+4))
LPC0_2:
    add r0, pc
    ldr r0, [r0]
    ldr r1, [r0]
    add.w   r0, r4, #12
    pop.w   {r4, r5, r7, lr}
    b.w _NativeArray_1_Dispose_mBCF86C8B6F156439D7104C3E57F0C71F3D2E4D54_gshared

The details aren’t that important, but we can see here that there’s a call to NativeArray.Dispose at the end. Notice the _gshared at the end, indicating that it’s a call via the function pointer. The function that wraps it has been inlined by the C++ compiler.

Now let’s look at NativeArray.Dispose that was called to see what executes:

    push    {r4, r5, r7, lr}
    add r7, sp, #8
    mov r4, r0
    ldr r0, [r0]
    ldr r1, [r4, #8]
    movs    r2, #0
    movs    r5, #0
    bl  _UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89
    str r5, [r4, #4]
    str r5, [r4]
    pop {r4, r5, r7, pc}

This looks just like the C++, which is basically just a call to UnsafeUtility.Free:

    push    {r4, r5, r6, r7, lr}
    add r7, sp, #12
    movw    r6, :lower16:(__ZZ60UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89E18_il2cpp_icall_func-(LPC5_0+4))
    mov r4, r1
    movt    r6, :upper16:(__ZZ60UnsafeUtility_Free_mAC082BB03B10D20CA9E5AD7FBA33164DF2B52E89E18_il2cpp_icall_func-(LPC5_0+4))
    mov r5, r0
LPC5_0:
    add r6, pc
    ldr r2, [r6]
    cbnz    r2, LBB5_2
    movw    r0, :lower16:(L_.str-(LPC5_1+4))
    movt    r0, :upper16:(L_.str-(LPC5_1+4))
LPC5_1:
    add r0, pc
    bl  __Z28il2cpp_codegen_resolve_icallPKc
    mov r2, r0
    str r0, [r6]
LBB5_2:
    mov r0, r5
    mov r1, r4
    pop.w   {r4, r5, r6, r7, lr}
    bx  r2

Again, this is a literal translation of the C++ which calls il2cpp_codegen_resolve_icall inside the Unity engine.

We’ve once again reached the end of the trail without Unity source code access, but we can still draw some conclusions from what we’ve seen. The main conclusion is that calling Dispose is not free. Several functions are called directly and via function pointers, memory is accessed that is possibly not in the CPU cache, and who knows what is happening inside the engine with UnsafeUtility::Free.

If it’s true that Temp is a bump allocator, and it appears so, then Dispose will ultimately have no effect. This means there’s no real reason to call it since Unity calls it at the end of the frame and it’ll only incur all of the extra work we’ve seen today for no effect.

Continue to part two