IL2CPP Output: readonly, sizeof, IntPtr, typeof, GetType
Today’s article looks at the IL2CPP and C++ compiler output for a variety of C# language features. Do you want to know what happens when you use them? Read on to find out!
readonly
The readonly
keyword disallows assigning a field after it’s initially assigned either inline or in a constructor. Does this yield any optimization in the C++ code that IL2CPP outputs? Let’s write a tiny example to see:
public class ClassWithReadonlyInstanceField { public readonly int Field; } public class ClassWithReadonlyStaticField { public static readonly int Field; } public static class TestClass { public static int UseReadonlyInstanceField(ClassWithReadonlyInstanceField x) { return x.Field; } public static int UseReadonlyStaticField() { return ClassWithReadonlyStaticField.Field; } }
Now let’s build for iOS with Unity 2017.4.1f1 to see the C++ code that gets generated:
struct ClassWithReadonlyInstanceField_t4047643351 : public RuntimeObject { public: // System.Int32 ClassWithReadonlyInstanceField::Field int32_t ___Field_0; public: inline static int32_t get_offset_of_Field_0() { return static_cast<int32_t>(offsetof(ClassWithReadonlyInstanceField_t4047643351, ___Field_0)); } inline int32_t get_Field_0() const { return ___Field_0; } inline int32_t* get_address_of_Field_0() { return &___Field_0; } inline void set_Field_0(int32_t value) { ___Field_0 = value; } }; struct ClassWithReadonlyStaticField_t2110888214_StaticFields { public: // System.Int32 ClassWithReadonlyStaticField::Field int32_t ___Field_0; public: inline static int32_t get_offset_of_Field_0() { return static_cast<int32_t>(offsetof(ClassWithReadonlyStaticField_t2110888214_StaticFields, ___Field_0)); } inline int32_t get_Field_0() const { return ___Field_0; } inline int32_t* get_address_of_Field_0() { return &___Field_0; } inline void set_Field_0(int32_t value) { ___Field_0 = value; } }; extern "C" int32_t TestClass_UseReadonlyInstanceField_m435387658 (RuntimeObject * __this /* static, unused */, ClassWithReadonlyInstanceField_t4047643351 * ___x0, const RuntimeMethod* method) { { ClassWithReadonlyInstanceField_t4047643351 * L_0 = ___x0; NullCheck(L_0); int32_t L_1 = L_0->get_Field_0(); return L_1; } } extern "C" int32_t TestClass_UseReadonlyStaticField_m3367044601 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_UseReadonlyStaticField_m3367044601_MetadataUsageId); s_Il2CppMethodInitialized = true; } { int32_t L_0 = ((ClassWithReadonlyStaticField_t2110888214_StaticFields*)il2cpp_codegen_static_fields_for(ClassWithReadonlyStaticField_t2110888214_il2cpp_TypeInfo_var))->get_Field_0(); return L_0; } }
Both generated classes have no trace of the readonly
keyword. C++ has a more-or-less equivalent const
keyword, but that’s not present in the C++ that IL2CPP generated. As for usage, there’s zero difference regardless of whether the field is static or not.
Conclusion: the readonly
keyword has no impact on generated code.
sizeof
Let’s use sizeof
to get the size (in bytes) of various kinds of C# types:
public struct MyStruct { } public enum MyEnum { } public static class TestClass { public static int SizofPrimitive() { return sizeof(int); } public static unsafe int SizofStruct() { return sizeof(MyStruct); } public static int SizofEnum() { return sizeof(MyEnum); } public static unsafe int SizofPointer() { return sizeof(int*); } public static unsafe int SizofIntPtr() { return sizeof(IntPtr); } public static unsafe int SizofUIntPtr() { return sizeof(UIntPtr); } }
Now let’s look at the C++ output:
extern "C" int32_t TestClass_SizofPrimitive_m3143674074 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { { return 4; } } extern "C" int32_t TestClass_SizofStruct_m1791752848 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_SizofStruct_m1791752848_MetadataUsageId); s_Il2CppMethodInitialized = true; } { uint32_t L_0 = il2cpp_codegen_sizeof(MyStruct_t123831593_il2cpp_TypeInfo_var); return L_0; } } extern "C" int32_t TestClass_SizofEnum_m467648496 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { { return 4; } } extern "C" int32_t TestClass_SizofPointer_m2755818356 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_SizofPointer_m2755818356_MetadataUsageId); s_Il2CppMethodInitialized = true; } { uint32_t L_0 = il2cpp_codegen_sizeof(Int32U2A_t2910199323_il2cpp_TypeInfo_var); return L_0; } } extern "C" int32_t TestClass_SizofIntPtr_m459331448 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_SizofIntPtr_m459331448_MetadataUsageId); s_Il2CppMethodInitialized = true; } { uint32_t L_0 = il2cpp_codegen_sizeof(IntPtr_t_il2cpp_TypeInfo_var); return L_0; } } extern "C" int32_t TestClass_SizofUIntPtr_m1437711209 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_SizofUIntPtr_m1437711209_MetadataUsageId); s_Il2CppMethodInitialized = true; } { uint32_t L_0 = il2cpp_codegen_sizeof(UIntPtr_t_il2cpp_TypeInfo_var); return L_0; } }
In the case of primitives like int
and enums like MyEnum
, the sizeof(x)
expression is simply replaced with a constant like 4
. This is great since zero work needs to be done at runtime to determine this size.
On the other hand, taking the sizeof
structs, pointers, IntPtr
, and UIntPtr
generates method initialization code plus a call to il2cpp_codegen_sizeof
which looks like this:
inline uint32_t il2cpp_codegen_sizeof(RuntimeClass* klass) { if (!klass->valuetype) { return sizeof(void*); } return il2cpp::vm::Class::GetInstanceSize(klass) - sizeof(RuntimeObject); }
Since these are all value types, the if
won’t pass and il2cpp::vm::Class::GetInstanceSize
will be called. Here’s what it looks like:
int32_t Class::GetInstanceSize(const Il2CppClass *klass) { IL2CPP_ASSERT(klass->size_inited); return klass->instance_size; }
Given how simple these functions are, we might suspect that the C++ compiler will inline them. To find out, let’s look at the ARM assembly this compiles to:
push {r4, r7, lr} add r7, sp, #4 movw r4, :lower16:(__ZZ34TestClass_SizofUIntPtr_m1437711209E25s_Il2CppMethodInitialized-(LPC10_0+4)) movt r4, :upper16:(__ZZ34TestClass_SizofUIntPtr_m1437711209E25s_Il2CppMethodInitialized-(LPC10_0+4)) LPC10_0: add r4, pc ldrb r0, [r4] cbnz r0, LBB10_2 movw r0, :lower16:(L_TestClass_SizofUIntPtr_m1437711209_MetadataUsageId$non_lazy_ptr-(LPC10_1+4)) movt r0, :upper16:(L_TestClass_SizofUIntPtr_m1437711209_MetadataUsageId$non_lazy_ptr-(LPC10_1+4)) LPC10_1: add r0, pc ldr r0, [r0] ldr r0, [r0] bl __ZN6il2cpp2vm13MetadataCache24InitializeMethodMetadataEj movs r0, #1 strb r0, [r4] LBB10_2: movw r0, :lower16:(L_UIntPtr_t_il2cpp_TypeInfo_var$non_lazy_ptr-(LPC10_2+4)) movt r0, :upper16:(L_UIntPtr_t_il2cpp_TypeInfo_var$non_lazy_ptr-(LPC10_2+4)) LPC10_2: add r0, pc ldr r0, [r0] ldr r0, [r0] ldrb.w r1, [r0, #177] lsls r1, r1, #31 bne LBB10_4 movs r0, #4 pop {r4, r7, pc} LBB10_4: bl __ZN6il2cpp2vm5Class15GetInstanceSizeEPK11Il2CppClass subs r0, #8 pop {r4, r7, pc}
The whole first part is the method initialization code. At the end we see the call to il2cpp::vm::Class::GetInstanceSize
. This means that just the call to il2cpp_codegen_sizeof
was inlined and we still have function call overhead for the call to il2cpp::vm::Class::GetInstanceSize
.
Conclusion: sizeof
is free for primitives and enums, but has method initialization and function call overhead for other types.
IntPtr and UIntPtr
IntPtr
and UIntPtr
are commonly used for interfacing with native plugins (e.g. UnityNativeScripting) to represent pointers without the need for so-called “unsafe” code. Let’s try a few ways of creating a “null” version of them:
public static class TestClass { public static IntPtr IntPtrZeroField() { return IntPtr.Zero; } public static IntPtr IntPtrDefaultCtor() { return new IntPtr(); } public static IntPtr IntPtrDefaultKeyword() { return default(IntPtr); } public static IntPtr IntPtrZeroCtor() { return new IntPtr(0); } public static UIntPtr UIntPtrZeroField() { return UIntPtr.Zero; } public static UIntPtr UIntPtrDefaultCtor() { return new UIntPtr(); } public static UIntPtr UIntPtrDefaultKeyword() { return default(UIntPtr); } public static UIntPtr UIntPtrZeroCtor() { return new UIntPtr(0); } }
Here’s the C++ that’s output:
extern "C" intptr_t TestClass_IntPtrZeroField_m4289709144 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_IntPtrZeroField_m4289709144_MetadataUsageId); s_Il2CppMethodInitialized = true; } { return (intptr_t)(0); } } extern "C" intptr_t TestClass_IntPtrDefaultCtor_m2172108580 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { intptr_t V_0; memset(&V_0, 0, sizeof(V_0)); { il2cpp_codegen_initobj((&V_0), sizeof(intptr_t)); intptr_t L_0 = V_0; return L_0; } } extern "C" intptr_t TestClass_IntPtrDefaultKeyword_m3009645941 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { intptr_t V_0; memset(&V_0, 0, sizeof(V_0)); { il2cpp_codegen_initobj((&V_0), sizeof(intptr_t)); intptr_t L_0 = V_0; return L_0; } } extern "C" intptr_t TestClass_IntPtrZeroCtor_m3237535456 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { { intptr_t L_0; memset(&L_0, 0, sizeof(L_0)); IntPtr__ctor_m987082960((&L_0), 0, /*hidden argument*/NULL); return L_0; } } extern "C" uintptr_t TestClass_UIntPtrZeroField_m2946046438 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_UIntPtrZeroField_m2946046438_MetadataUsageId); s_Il2CppMethodInitialized = true; } { IL2CPP_RUNTIME_CLASS_INIT(UIntPtr_t_il2cpp_TypeInfo_var); return (0); } } extern "C" uintptr_t TestClass_UIntPtrDefaultCtor_m694697819 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { uintptr_t V_0; memset(&V_0, 0, sizeof(V_0)); { il2cpp_codegen_initobj((&V_0), sizeof(uintptr_t)); uintptr_t L_0 = V_0; return L_0; } } extern "C" uintptr_t TestClass_UIntPtrDefaultKeyword_m1569370405 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { uintptr_t V_0; memset(&V_0, 0, sizeof(V_0)); { il2cpp_codegen_initobj((&V_0), sizeof(uintptr_t)); uintptr_t L_0 = V_0; return L_0; } } extern "C" uintptr_t TestClass_UIntPtrZeroCtor_m4231348176 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { { uintptr_t L_0; memset(&L_0, 0, sizeof(L_0)); UIntPtr__ctor_m4250165422((&L_0), 0, /*hidden argument*/NULL); return L_0; } }
Regardless of whether Using the default constructor or default
keyword results in the same C++ code. Let’s go one step further and look at the generated assembly:
// _TestClass_UIntPtrDefaultCtor_m694697819 movs r0, #0 bx lr // _TestClass_UIntPtrDefaultKeyword_m1569370405 movs r0, #0 bx lr
Here we see that the memset
and everything else has been removed and replaced with the #0
constant.
The version where we’re passing 0
to the constructor adds on a call to the constructor. Will that get removed by the compiler, too? Let’s find out by looking at the compiled assembly:
// _TestClass_UIntPtrZeroCtor_m4231348176 push {r7, lr} mov r7, sp sub sp, #4 movs r0, #0 movs r1, #0 str r0, [sp] mov r0, sp movs r2, #0 bl _UIntPtr__ctor_m4250165422 ldr r0, [sp], #4 pop {r7, pc}
Unfortunately, this version results in a full call to the constructor function.
When it comes to using the Zero
field of both types, we get method initialization overhead because these are static readonly
fields. Interestingly, the version for IntPtr
varies slightly from the UIntPtr
version. Everything’s the same, except that there’s an additional call to IL2CPP_RUNTIME_CLASS_INIT(UIntPtr_t_il2cpp_TypeInfo_var);
with UIntPtr
. Here’s what it looks like:
#define IL2CPP_RUNTIME_CLASS_INIT(klass) do { if((klass)->has_cctor && !(klass)->cctor_finished) il2cpp::vm::Runtime::ClassInit ((klass)); } while (0)
il2cpp::vm::Runtime::ClassInit
is a large function involving many thread atomic operations:
void Runtime::ClassInit(Il2CppClass *klass) { // Nothing to do if class has no static constructor. if (!klass->has_cctor) return; // Nothing to do if class constructor already ran. if (os::Atomic::CompareExchange(&klass->cctor_finished, 1, 1) == 1) return; s_TypeInitializationLock.Lock(); // See if some thread ran it while we acquired the lock. if (os::Atomic::CompareExchange(&klass->cctor_finished, 1, 1) == 1) { s_TypeInitializationLock.Unlock(); return; } // See if some other thread got there first and already started running the constructor. if (os::Atomic::CompareExchange(&klass->cctor_started, 1, 1) == 1) { s_TypeInitializationLock.Unlock(); // May have been us and we got here through recursion. os::Thread::ThreadId currentThread = os::Thread::CurrentThreadId(); if (os::Atomic::CompareExchange64(&klass->cctor_thread, currentThread, currentThread) == currentThread) return; // Wait for other thread to finish executing the constructor. while (os::Atomic::CompareExchange(&klass->cctor_finished, 1, 1) == 0) { os::Thread::Sleep(1); } } else { // Let others know we have started executing the constructor. os::Atomic::Exchange64(&klass->cctor_thread, os::Thread::CurrentThreadId()); os::Atomic::Exchange(&klass->cctor_started, 1); s_TypeInitializationLock.Unlock(); // Run it. Il2CppException* exception = NULL; const MethodInfo* cctor = Class::GetCCtor(klass); if (cctor != NULL) { vm::Runtime::Invoke(cctor, NULL, NULL, &exception); } // Let other threads know we finished. os::Atomic::Exchange(&klass->cctor_finished, 1); os::Atomic::Exchange64(&klass->cctor_thread, 0); // Deal with exceptions. if (exception != NULL) { const Il2CppType *type = Class::GetType(klass); std::string n = StringUtils::Printf("The type initializer for '%s' threw an exception.", Type::GetName(type, IL2CPP_TYPE_NAME_FORMAT_IL).c_str()); Il2CppException* typeInitializationException = Exception::GetTypeInitializationException(n.c_str(), exception); Exception::Raise(typeInitializationException); } } }
Unfortunately, the if (!klass->has_cctor)
check that skips all the expensive work won’t pass because there is a static constructor to set the Zero
field. The result is assembly code that has both method initialization overhead and a call to the expensive il2cpp::vm::Runtime::ClassInit
function:
push {r4, r7, lr} add r7, sp, #4 movw r4, :lower16:(__ZZ38TestClass_UIntPtrZeroField_m2946046438E25s_Il2CppMethodInitialized-(LPC15_0+4)) movt r4, :upper16:(__ZZ38TestClass_UIntPtrZeroField_m2946046438E25s_Il2CppMethodInitialized-(LPC15_0+4)) LPC15_0: add r4, pc ldrb r0, [r4] cbnz r0, LBB15_2 movw r0, :lower16:(L_TestClass_UIntPtrZeroField_m2946046438_MetadataUsageId$non_lazy_ptr-(LPC15_1+4)) movt r0, :upper16:(L_TestClass_UIntPtrZeroField_m2946046438_MetadataUsageId$non_lazy_ptr-(LPC15_1+4)) LPC15_1: add r0, pc ldr r0, [r0] ldr r0, [r0] bl __ZN6il2cpp2vm13MetadataCache24InitializeMethodMetadataEj movs r0, #1 strb r0, [r4] LBB15_2: movw r0, :lower16:(L_UIntPtr_t_il2cpp_TypeInfo_var$non_lazy_ptr-(LPC15_2+4)) movt r0, :upper16:(L_UIntPtr_t_il2cpp_TypeInfo_var$non_lazy_ptr-(LPC15_2+4)) LPC15_2: add r0, pc ldr r0, [r0] ldr r0, [r0] ldrb.w r1, [r0, #178] lsls r1, r1, #31 beq LBB15_5 ldr r1, [r0, #96] cbnz r1, LBB15_5 bl __ZN6il2cpp2vm7Runtime9ClassInitEP11Il2CppClass LBB15_5: movs r0, #0 pop {r4, r7, pc}
Conclusion: use the default constructor or default
keyword. Don’t pass 0
to the constructor or use the Zero
field.
typeof
Let’s try using typeof
to get a Type
for various types:
public class MyClass { } public static class TestClass { public static Type TypeofPrimitive() { return typeof(int); } public static Type TypeofStruct() { return typeof(MyStruct); } public static Type TypeofEnum() { return typeof(MyEnum); } public static Type TypeofClass() { return typeof(MyClass); } }
Here’s the C++ that is generated:
extern "C" Type_t * TestClass_TypeofPrimitive_m3064036489 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_TypeofPrimitive_m3064036489_MetadataUsageId); s_Il2CppMethodInitialized = true; } { RuntimeTypeHandle_t3027515415 L_0 = { reinterpret_cast<intptr_t> (Int32_t2950945753_0_0_0_var) }; IL2CPP_RUNTIME_CLASS_INIT(Type_t_il2cpp_TypeInfo_var); Type_t * L_1 = Type_GetTypeFromHandle_m1620074514(NULL /*static, unused*/, L_0, /*hidden argument*/NULL); return L_1; } } extern "C" Type_t * TestClass_TypeofStruct_m2510147412 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_TypeofStruct_m2510147412_MetadataUsageId); s_Il2CppMethodInitialized = true; } { RuntimeTypeHandle_t3027515415 L_0 = { reinterpret_cast<intptr_t> (MyStruct_t123831593_0_0_0_var) }; IL2CPP_RUNTIME_CLASS_INIT(Type_t_il2cpp_TypeInfo_var); Type_t * L_1 = Type_GetTypeFromHandle_m1620074514(NULL /*static, unused*/, L_0, /*hidden argument*/NULL); return L_1; } } extern "C" Type_t * TestClass_TypeofEnum_m372310367 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_TypeofEnum_m372310367_MetadataUsageId); s_Il2CppMethodInitialized = true; } { RuntimeTypeHandle_t3027515415 L_0 = { reinterpret_cast<intptr_t> (MyEnum_t2344833737_0_0_0_var) }; IL2CPP_RUNTIME_CLASS_INIT(Type_t_il2cpp_TypeInfo_var); Type_t * L_1 = Type_GetTypeFromHandle_m1620074514(NULL /*static, unused*/, L_0, /*hidden argument*/NULL); return L_1; } } extern "C" Type_t * TestClass_TypeofClass_m3517807824 (RuntimeObject * __this /* static, unused */, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_TypeofClass_m3517807824_MetadataUsageId); s_Il2CppMethodInitialized = true; } { RuntimeTypeHandle_t3027515415 L_0 = { reinterpret_cast<intptr_t> (MyClass_t3388352440_0_0_0_var) }; IL2CPP_RUNTIME_CLASS_INIT(Type_t_il2cpp_TypeInfo_var); Type_t * L_1 = Type_GetTypeFromHandle_m1620074514(NULL /*static, unused*/, L_0, /*hidden argument*/NULL); return L_1; } }
The result is the same for each of these. We’ve already seen IL2CPP_RUNTIME_CLASS_INIT
, so let’s look at Type_GetTypeFromHandle_m1620074514
:
extern "C" Type_t * Type_GetTypeFromHandle_m1620074514 (RuntimeObject * __this /* static, unused */, RuntimeTypeHandle_t3027515415 ___handle0, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (Type_GetTypeFromHandle_m1620074514_MetadataUsageId); s_Il2CppMethodInitialized = true; } { intptr_t L_0 = RuntimeTypeHandle_get_Value_m1525396455((&___handle0), /*hidden argument*/NULL); bool L_1 = IntPtr_op_Equality_m408849716(NULL /*static, unused*/, L_0, (intptr_t)(0), /*hidden argument*/NULL); if (!L_1) { goto IL_0018; } } { return (Type_t *)NULL; } IL_0018: { intptr_t L_2 = RuntimeTypeHandle_get_Value_m1525396455((&___handle0), /*hidden argument*/NULL); IL2CPP_RUNTIME_CLASS_INIT(Type_t_il2cpp_TypeInfo_var); Type_t * L_3 = Type_internal_from_handle_m3156085815(NULL /*static, unused*/, L_2, /*hidden argument*/NULL); return L_3; } }
RuntimeTypeHandle_get_Value_m1525396455
is just an accessor and IntPtr_op_Equality_m408849716
is just an ==
, but Type_internal_from_handle_m3156085815
does more:
extern "C" Type_t * Type_internal_from_handle_m3156085815 (RuntimeObject * __this /* static, unused */, intptr_t ___handle0, const RuntimeMethod* method) { typedef Type_t * (*Type_internal_from_handle_m3156085815_ftn) (intptr_t); using namespace il2cpp::icalls; return ((Type_internal_from_handle_m3156085815_ftn)mscorlib::System::Type::internal_from_handle) (___handle0); }
This really just calls into mscorlib::System::Type::internal_from_handle
, which looks like this:
Il2CppReflectionType * Type::internal_from_handle(intptr_t ptr) { const Il2CppType* type = (const Il2CppType*)ptr; Il2CppClass *klass = Class::FromIl2CppType(type); return il2cpp::vm::Reflection::GetTypeObject(klass->byval_arg); }
Class::FromIl2CppType
looks like this:
Il2CppClass* Class::FromIl2CppType(const Il2CppType* type) { #define RETURN_DEFAULT_TYPE(fieldName) do { IL2CPP_ASSERT(il2cpp_defaults.fieldName); return il2cpp_defaults.fieldName; } while (false) switch (type->type) { case IL2CPP_TYPE_OBJECT: RETURN_DEFAULT_TYPE(object_class); case IL2CPP_TYPE_VOID: RETURN_DEFAULT_TYPE(void_class); case IL2CPP_TYPE_BOOLEAN: RETURN_DEFAULT_TYPE(boolean_class); case IL2CPP_TYPE_CHAR: RETURN_DEFAULT_TYPE(char_class); case IL2CPP_TYPE_I1: RETURN_DEFAULT_TYPE(sbyte_class); case IL2CPP_TYPE_U1: RETURN_DEFAULT_TYPE(byte_class); case IL2CPP_TYPE_I2: RETURN_DEFAULT_TYPE(int16_class); case IL2CPP_TYPE_U2: RETURN_DEFAULT_TYPE(uint16_class); case IL2CPP_TYPE_I4: RETURN_DEFAULT_TYPE(int32_class); case IL2CPP_TYPE_U4: RETURN_DEFAULT_TYPE(uint32_class); case IL2CPP_TYPE_I: RETURN_DEFAULT_TYPE(int_class); case IL2CPP_TYPE_U: RETURN_DEFAULT_TYPE(uint_class); case IL2CPP_TYPE_I8: RETURN_DEFAULT_TYPE(int64_class); case IL2CPP_TYPE_U8: RETURN_DEFAULT_TYPE(uint64_class); case IL2CPP_TYPE_R4: RETURN_DEFAULT_TYPE(single_class); case IL2CPP_TYPE_R8: RETURN_DEFAULT_TYPE(double_class); case IL2CPP_TYPE_STRING: RETURN_DEFAULT_TYPE(string_class); case IL2CPP_TYPE_TYPEDBYREF: RETURN_DEFAULT_TYPE(typed_reference_class); case IL2CPP_TYPE_ARRAY: { Il2CppClass* elementClass = FromIl2CppType(type->data.array->etype); return Class::GetBoundedArrayClass(elementClass, type->data.array->rank, true); } case IL2CPP_TYPE_PTR: return Class::GetPtrClass(type->data.type); case IL2CPP_TYPE_FNPTR: NOT_IMPLEMENTED(Class::FromIl2CppType); return NULL; //mono_fnptr_class_get (type->data.method); case IL2CPP_TYPE_SZARRAY: { Il2CppClass* elementClass = FromIl2CppType(type->data.type); return Class::GetArrayClass(elementClass, 1); } case IL2CPP_TYPE_CLASS: case IL2CPP_TYPE_VALUETYPE: return Type::GetClass(type); case IL2CPP_TYPE_GENERICINST: return GenericClass::GetClass(type->data.generic_class); case IL2CPP_TYPE_VAR: return Class::FromGenericParameter(Type::GetGenericParameter(type)); case IL2CPP_TYPE_MVAR: return Class::FromGenericParameter(Type::GetGenericParameter(type)); default: NOT_IMPLEMENTED(Class::FromIl2CppType); } return NULL; #undef RETURN_DEFAULT_TYPE }
il2cpp::vm::Reflection::GetTypeObject
looks like this:
Il2CppReflectionType* Reflection::GetTypeObject(const Il2CppType *type) { il2cpp::os::FastAutoLock lock(&s_ReflectionICallsMutex); Il2CppReflectionType* object = NULL; if (s_TypeMap->TryGetValue(type, &object)) return object; Il2CppReflectionType* typeObject = (Il2CppReflectionType*)Object::New(il2cpp_defaults.monotype_class); typeObject->type = type; s_TypeMap->Add(type, typeObject); return typeObject; }
We could keep following the function calls, but at this point we’ve seen enough to draw conclusions.
Conclusion: avoid typeof
in performance-critical code. Cache its result to avoid duplicating the work.
GetType
Finally, let’s look at object.GetType()
to see what differences there are to typeof
:
public static class TestClass { public static Type GetTypePrimitive(int x) { return x.GetType(); } public static Type GetTypeStruct(MyStruct x) { return x.GetType(); } public static Type GetTypeEnum(MyEnum x) { return x.GetType(); } public static Type GetTypeClass(MyClass x) { return x.GetType(); } }
Here’s the IL2CPP output:
extern "C" Type_t * TestClass_GetTypePrimitive_m349042583 (RuntimeObject * __this /* static, unused */, int32_t ___x0, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_GetTypePrimitive_m349042583_MetadataUsageId); s_Il2CppMethodInitialized = true; } { RuntimeObject * L_0 = Box(Int32_t2950945753_il2cpp_TypeInfo_var, (&___x0)); NullCheck(L_0); Type_t * L_1 = Object_GetType_m88164663(L_0, /*hidden argument*/NULL); ___x0 = *(int32_t*)UnBox(L_0); return L_1; } } extern "C" Type_t * TestClass_GetTypeStruct_m1184800290 (RuntimeObject * __this /* static, unused */, MyStruct_t123831593 ___x0, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_GetTypeStruct_m1184800290_MetadataUsageId); s_Il2CppMethodInitialized = true; } { RuntimeObject * L_0 = Box(MyStruct_t123831593_il2cpp_TypeInfo_var, (&___x0)); NullCheck(L_0); Type_t * L_1 = Object_GetType_m88164663(L_0, /*hidden argument*/NULL); ___x0 = *(MyStruct_t123831593 *)UnBox(L_0); return L_1; } } extern "C" Type_t * TestClass_GetTypeEnum_m118029280 (RuntimeObject * __this /* static, unused */, int32_t ___x0, const RuntimeMethod* method) { static bool s_Il2CppMethodInitialized; if (!s_Il2CppMethodInitialized) { il2cpp_codegen_initialize_method (TestClass_GetTypeEnum_m118029280_MetadataUsageId); s_Il2CppMethodInitialized = true; } { RuntimeObject * L_0 = Box(MyEnum_t2344833737_il2cpp_TypeInfo_var, (&___x0)); NullCheck(L_0); Type_t * L_1 = Object_GetType_m88164663(L_0, /*hidden argument*/NULL); ___x0 = *(int32_t*)UnBox(L_0); return L_1; } } extern "C" Type_t * TestClass_GetTypeClass_m1216189806 (RuntimeObject * __this /* static, unused */, MyClass_t3388352440 * ___x0, const RuntimeMethod* method) { { MyClass_t3388352440 * L_0 = ___x0; NullCheck(L_0); Type_t * L_1 = Object_GetType_m88164663(L_0, /*hidden argument*/NULL); return L_1; } }
These are all mostly the same. They’re mostly a call to Object_GetType_m88164663
. When value types (i.e. primitives, enums, and structs) are used, boxing also occurs and creates garbage for the GC. Let’s have a look at Object_GetType_m88164663
to see what work is being done:
extern "C" Type_t * Object_GetType_m88164663 (RuntimeObject * __this, const RuntimeMethod* method) { typedef Type_t * (*Object_GetType_m88164663_ftn) (RuntimeObject *); using namespace il2cpp::icalls; return ((Object_GetType_m88164663_ftn)mscorlib::System::Object::GetType) (__this); }
This is another wrapper function, this time calling mscorlib::System::Object::GetType
. Let’s check it out:
Il2CppReflectionType* Object::GetType(Il2CppObject* obj) { return il2cpp::vm::Reflection::GetTypeObject(obj->klass->byval_arg); }
This in turn calls il2cpp::vm::Reflection::GetTypeObject
, which is the big expensive function we saw above with typeof
. So there’s not much difference between calling GetType
and using typeof
, other than the boxing for value types.
Conclusion: avoid GetType
in performance-critical code, especially with value types. Cache its result to avoid duplicating the work.