Showing posts with label cpp. Show all posts
Showing posts with label cpp. Show all posts

Sunday, January 3, 2016

Seeing Sharp C++: The love/hate relationship with Enums

Enums are great. They give you an identifier to either an explicit value (Apples = 1, Oranges = 2), or a compiler derived value. They're something you can easily refactor. They're quasi-type-safe. They're just great.

Except when it comes to generics. And IL2CPP.

In a project I'm currently working on, I make use of a custom BitVector32 struct for cases like object flags storage, etc. For the project I've added some additional methods to interop with it using an Enum via generics, where each member in a given Enum maps to a bit index:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#region Access via Enum
private bool ValidateBit<TEnum>(TEnum bit, int bitIndex)
 where TEnum : struct, IComparable, IFormattable, IConvertible
{
 if (bitIndex < 0 || bitIndex >= this.Length)
 {
  Assert("Enum member {0} with value {1} is out of range for indexing",
   bit, bitIndex);
  return false;
 }

 return true;
}

public bool Test<TEnum>(TEnum bit)
 where TEnum : struct, IComparable, IFormattable, IConvertible
{
 int bitIndex = bit.ToInt32(null);
 if (!ValidateBit(bit, bitIndex))
  return false;

 return Bitwise.Flags.Test(mWord, ((uint)1) << bitIndex);
}

public void Set<TEnum>(TEnum bit, bool value)
 where TEnum : struct, IComparable, IFormattable, IConvertible
{
 int bitIndex = bit.ToInt32(null);
 if (!ValidateBit(bit, bitIndex))
  return;

 var flag = ((uint)1) << bitIndex;

 Bitwise.Flags.Modify(value, ref mWord, flag);
}
#endregion

The "where" clause uses as many Enum-specific constraints as possible since whatever lightbulbs behind the language thought it was a bright idea to not allow "where T : enum" (but you can in IL and F#). I mean, what the hell? But I digress, this isn't a Unity problem.

The IConvertible interface gives us the power of ToInt32(), which is needed to generically 'interpret' the TEnum value "bit" as a bit index. For the rest of this article I'm just going to focus on the Set method (line 25). Let's inspect the IL and how IL2CPP (as of 5.3.1p1) transforms it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
.method public hidebysig 
 instance void Set<valuetype .ctor ([mscorlib]System.ValueType, [mscorlib]System.IComparable, [mscorlib]System.IFormattable, [mscorlib]System.IConvertible) TEnum> (
  !!TEnum bit,
  bool 'value'
 ) cil managed 
{
 // Method begins at RVA 0x159f8
 // Code size 51 (0x33)
 .maxstack 3
 .locals init (
  [0] int32,
  [1] uint32
 )

 IL_0000: ldarga.s bit
 IL_0002: ldnull
 IL_0003: constrained. !!TEnum
 IL_0009: callvirt instance int32 [mscorlib]System.IConvertible::ToInt32(class [mscorlib]System.IFormatProvider)
 IL_000e: stloc.0
 IL_000f: ldarg.0
 IL_0010: ldarg.1
 IL_0011: ldloc.0
 IL_0012: call instance bool BitVector32::ValidateBit<!!TEnum>(!!0, int32)
 IL_0017: brtrue IL_001d

 IL_001c: ret

 IL_001d: ldc.i4.1
 IL_001e: ldloc.0
 IL_001f: ldc.i4.s 31
 IL_0021: and
 IL_0022: shl
 IL_0023: stloc.1
 IL_0024: ldarg.2
 IL_0025: ldarg.0
 IL_0026: ldflda uint32 BitVector32::mWord
 IL_002b: ldloc.1
 IL_002c: call bool Bitwise.Flags::Modify(bool, uint32&, uint32)
 IL_0031: pop
 IL_0032: ret
} // end of method BitVector32::Set

Here's the IL2CPP of Set instanced with an unimportant enum named MyEnum ('KM00' marks my own comments, not the compiler):
UPDATE: A fix for the double boxing described below is in the pipes on Unity's end. It is now in as of 5.3.1p4, "Removed an unnecessary Box used to null check before calling a virtual method". 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// generic sharing
#define IL2CPP_RGCTX_DATA(rgctxVar,index) (InitializedTypeInfo(rgctxVar[index].klass))
#define IL2CPP_RGCTX_METHOD_INFO(rgctxVar,index) (rgctxVar[index].method)

// System.Void BitVector32::Set<MyEnum>(TEnum,System.Boolean)
// System.Void BitVector32::Set<MyEnum>(TEnum,System.Boolean) // [sic] KM00: why is this written twice?
extern TypeInfo* IConvertible_t1162873557_0_il2cpp_TypeInfo_var;
extern const uint32_t BitVector32_Set_TisMyEnum_t1322059486_0_m_97831884_0_MetadataUsageId;
extern "C"  void BitVector32_Set_TisMyEnum_t1322059486_0_m_97831884_0_gshared (BitVector32_t_84621378_0 * __this, int32_t ___bit, bool ___value, const MethodInfo* method)
{
 static bool s_Il2CppMethodIntialized;
 if (!s_Il2CppMethodIntialized)
 {
  il2cpp_codegen_initialize_method (BitVector32_Set_TisMyEnum_t1322059486_0_m_97831884_0_MetadataUsageId);
  s_Il2CppMethodIntialized = true;
 }
 int32_t V_0 = 0;
 uint32_t V_1 = 0;
 {
  // KM00: RGCTX stands for Runtime Generic Context
  // Index 0 in the RGCTX data should be a TypeInfo pointer to MyEnum.
  // So this should be trying to box ___bit to a full MyEnum object (including vtable, etc)
  // And yes, this is resulting in double the garbage: Null checked Box, followed by another Box for the actual ToInt32 call.
  NullCheck((Object_t *)Box(IL2CPP_RGCTX_DATA(method->rgctx_data, 0), (&___bit)));
  // KM00: the actual ToInt32() call
  int32_t L_0 = (int32_t)InterfaceFuncInvoker1< int32_t, Object_t * >::Invoke(7 /* System.Int32 System.IConvertible::ToInt32(System.IFormatProvider) */, IConvertible_t1162873557_0_il2cpp_TypeInfo_var, (Object_t *)Box(IL2CPP_RGCTX_DATA(method->rgctx_data, 0), (&___bit)), (Object_t *)NULL);
  V_0 = (int32_t)L_0;
  int32_t L_1 = ___bit;
  int32_t L_2 = V_0;
  // KM00: this is the ValidateBit() call
  bool L_3 = ((  bool (*) (BitVector32_t_84621378_0 *, int32_t, int32_t, const MethodInfo*))IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 1)->method)((BitVector32_t_84621378_0 *)__this, (int32_t)L_1, (int32_t)L_2, /*hidden argument*/IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 1));
  if (L_3)
  {
   // KM00: If ValidateBit() didn't fail
   goto IL_001d;
  }
 }
 {
  return;
 }

IL_001d:
 {
  int32_t L_4 = V_0;
  V_1 = (uint32_t)((int32_t)((int32_t)1<<(int32_t)((int32_t)((int32_t)L_4&(int32_t)((int32_t)31)))));
  bool L_5 = ___value;
  uint32_t* L_6 = (uint32_t*)&(__this->___mWord_3);
  uint32_t L_7 = V_1;
  Flags_Modify_m_912810781_0(NULL /*static, unused*/, (bool)L_5, (uint32_t*)L_6, (uint32_t)L_7, /*hidden argument*/NULL);
  return;
 }
}

Essentially what is happening here is that IL2CPP is missing a possible optimization of just treating the ___bit as itself: an int32_t value. Instead it goes through the full plumbing of Enum's IConvertible implementation, which 'requires' a virtual call which itself requires a complete Il2CppObject (vtable, etc).
Line 24 in the above snippet looks an awful lot like bad IL2CPP codegen. It's a bit painful to trace without source (hint hint!), but it appears to be generated in IL2CPP's MethodBodyWriter.CallMethod, specially where it invokes MethodBodyWriter.WriteNullCheckForInvocationIfNeeded. They should probably cache args[0] in a stack variable, then null check that, and then assign args[0] to that stack variable to avoid more boxing and garbage than needed.
As it turns out, calling ToInt32() on an enum value will always result in a GC operation in Mono, as it calls the get_value method which returns an Object. Internally, this is implemented in Mono via ves_icall_System_Enum_get_value. It essentially boxes the enum value to an instance of the underlying-type (int, long, etc) and does a memcpy on the field data. From there it would end up calling the IConvertible on the underlying type (in this case, Int32).

Final thoughts

No matter what your runtime, Unity's super-old Mono or IL2CPP (MS .NET can be more forgiving), you're probably running into more overhead than you realize with Enums and value types in general. Just because you use generics and constrain your type parameters to given interfaces doesn't mean that Mono or IL2CPP will properly optimize the output using that knowledge (in the event types are structs or otherwise sealed).

If C# had more relaxed forms of 'generics' as with C++'s templates, then I could have simply done an 'int bitIndex = (int)bit' cast, with the cast being resolved at template instantiation. The goal of these generic methods was to abstract the need for these casts from client code, but it looks like I'll need to roll back their usage in game loop code.

It also appears interface method calls on value types, as of 5.3.1p1, can result in an extra GC alloc and box operation. IL2CPP has come a long, long way over the past year but it obviously still has lots of room for improvement. For Unity and the Unity community's sake, more developers should inspect their IL2CPP code to not only ensure they're not generating funky IL, but that Unity itself isn't generating funky C++!

One day we'll be able to have nice things. Like enums and generics. Until then I'm just going to byte the bullet and explicitly cast them to integers :(

Saturday, January 2, 2016

IL2CPP codegen bugs as of Unity 5.3.1.p1

While researching the changes made in Patch 5.3.1p1 ("IL2CPP: Optimize method prologues for code size and incremental builds"), I came across both a codegen bug and filegen oversight.

Codegen bug

For IL2CPP codegen, Unity uses StreamWriter. Said class inherits from TextWriter, which exposes a NewLine property. Viewing Unity's mscorlib assembly on Windows in ILSpy shows the default value of NewLine is "\r\n", which is the line ending Windows uses. However, Unity prefers Unix-style line endings. Which wouldn't be a problem if they would either set or override (say, in a UnityStreamWriter class) NewLine to just be "\n"! But they don't.

For IL2CPP's CodeWriter class, it gracefully uses Unix-style since they wrap an internal StreamWriter object and expose WriteLine methods that just call the Write method. However, there are multiple instances across the IL2CPP assembly where StreamWriter is used directly without having NewLine set to Unix-style line endings.

Good Newlines
Bad Newlines
I wouldn't be surprised if there are other places in Unity's managed code where this is an issue. Perhaps Unity's test cases should include checks to ensure there are no carriage returns prior to new lines.

Filegen oversight

While looking through my project's IL2CPP I noticed that enums had method decl headers, like "mscorlib_System_AttributeTargets_59449993MethodDeclarations.h". However, IL2CPP doesn't actually spit out anything (of interest) in these headers for enums! For a project I'm working on IL2CPP spits out almost 13k header and source files. 512 of those are meaningless MethodDeclations for enums.

Should probably check type != enum too

Monday, December 14, 2015

Seeing Sharp C++: Behind the IL2CPP magic of Metadata and Statics

To the average C# programmer, how the .NET runtime glues together reflection in the final code isn't something they really need to be familiar with. It Just Works. However, we're Unity developers using IL2CPP for our .NET runtime so we're not the "average C# programmer". When it comes to buffing out every dip in our game's runtime performance, knowing the devil in the details is a requirement. So today we're going to look how (as of Unity 5.3.0f4) the use of metadata/reflection and class statics may be poking at your C#-turned-C++ code with a fiery pitchfork.

Metadata and Reflection in IL2CPP

Since IL2CPP is not a VM using generalized byte code instructions but compiled native code, it has to generate code to always ensure metadata is prepared for use before it is possibly needed. This is done via a sort of 'metadata initialization' prologue that is flagged for execution the very first time the the method is ran. Take for example the following IL2CPP code gen:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// C#: { (new SomeValueType(0)).GetType(); }

// Notice the extern on this TypeInfo*. There's only one TypeInfo* for every class (Int32, StringBuilder, etc)
extern TypeInfo* SomeValueType_t_il2cpp_TypeInfo_var;
extern "C" void IL2CPP_Method (Object_t * __this /* static, unused */, const MethodInfo* method)
{
 static bool s_Il2CppMethodIntialized;
 if (!s_Il2CppMethodIntialized)
 {
  // Will either get the cached TypeInfo*, or if this is the first time the type
  // is used, perform any lookups/etc needed to populate an internal cache
  SomeValueType_t_il2cpp_TypeInfo_var = il2cpp_codegen_type_info_from_index(910);
  s_Il2CppMethodIntialized = true;
 }
 Type_t * V_0 = {0};
 {
  SomeValueType_t  L_0 = {0};
  SomeValueType__ctor(&L_0, 0, /*hidden argument*/NULL);
  SomeValueType_t  L_1 = L_0;
  // GetType() is a method on the Object class. The struct
  // 'SomeValueType', having no reflection bits, needs to be boxed into an object
  // with the proper Il2CppObject header which has a TypeInfo* to the reflection 
  // bits needed for 'SomeValueType'
  Object_t * L_2 = Box(SomeValueType_t_il2cpp_TypeInfo_var, &L_1);
  NullCheck(L_2);
  Type_t * L_3 = Object_GetType(L_2, /*hidden argument*/NULL);
  ...

Here, all the C# code did was instance a struct named 'SomeValueType' then called 'GetType()' on that instance. But the native transformation of that C# must ensure the TypeInfo for the struct is prepared by the runtime.
IL2CPP optimization idea: It should be possible for IL2CPP to see cases where GetType() is called on structs or sealed classes. In these cases it should be possible for the code generator to instead output the same code as seen when using the typeof() operator (I'll leave this as an exercise for the user to investigate). In the case of structs this would result in no extra memory allocations due to boxing just to call Object.GetType
You may be thinking to yourself "My code doesn't make use of reflection so none of this magic should apply". Well, you could be right but also wrong. There's another case where you will see this prologue and more magic: statics and static constructors.

Class Statics

Static constructors (abbreviated as cctors; they're similar to 'dynamic initializers' in C++) are generated either manually or when you define static members with inline initialization (e.g., static int kMaxIterations = 4).

Unlike C++, these static values don't have a final memory location in a .bss or .data section of the executable (although it should be theoretically possible within IL2CPP). A buffer big enough to contain all the static fields is instead allocated in managed memory and then tracked via the class' TypeInfo. With IL2CPP this 'buffer' is actually a codegen'd C++ struct (so you can inspect it if you desire). This has the added benefit that static data is lazy loaded.
Statics background: This lazy loading behavior is part of the C# specification. However, struct cctors [Section 18.3.10] have slightly different requirements than class cctors [Section 17.11].
So if no code in your program ever used DateTime, then its statics should never be allocated and initialized. However, the draw back is that the runtime must ensure the TypeInfo is initialized (also where the statics buffer reference is kept) and that the cctor is ran. With IL2CPP this is implemented via injecting metadata initialization prologues and IL2CPP_RUNTIME_CLASS_INIT macro calls.

We can refer to the IL2CPP code for Runtime::ClassInit to see documentation on when they intend to insert instances of the macro previously mentioned:


1
2
3
4
5
6
7
8
// From vm/Runtime.cpp:

// We currently call Runtime::ClassInit in 4 places:
// 1. Just after we allocate storage for a new object (Object::NewAllocSpecific)
// 2. Just before reading any static field
// 3. Just before calling any static method
// 4. Just before calling class instance constructor from a derived class instance constructor
void Runtime::ClassInit (TypeInfo *klass);

Now, let's see this documentation in action!

ValueTypeWithCctor is a simple C# struct with a static field (and thus a cctor) and ValueTypeWithoutCctor is that same struct with instead a property that returns the same exact value of that static field. The IL2CPP transformations of both can be found here and here, respectively.

Again, the only difference between the two structs is the static 'One' member. In ValueTypeWithCctor it is a static field, and in ValueTypeWithoutCctor it is a static property that returns the result of the only user defined constructor. They both have a user defined addition operator that calls the static Add() method. While the logic for Add() doesn't make use of any static fields it still results in ValueTypeWithCctor::operator+ having a metadata initialization prologue and RUNTIME_CLASS_INIT call (keeping in line with the C# specs and bullet number 3 in the documentation above).

I then have two very similar methods (only differ in which struct they use) that exercise the user defined ctor, One static, operator+, and Add() method: Il2Cpp_ValueTypeWithCctor() and Il2Cpp_ValueTypeWithoutCctor().

In Il2Cpp_ValueTypeWithCctor's C++ you can see that it has a metadata initialization prologue and then a call to RUNTIME_CLASS_INIT. It requires this in order to access the 'One' static field (bullet #2). Even if you removed the access to 'One' (change 'v1 = v0'), it would still have both calls (again, due to bullet #3 "Just before calling any static method"). Albeit, RUNTIME_CLASS_INIT would occur before the call to operator+. However, recall that operator+ already does this! Ouch, code bloat.

Now in Il2Cpp_ValueTypeWithoutCctor's C++ you can see there's no prologue or RUNTIME_CLASS_INIT call. In fact, if we trace the call to ValueTypeWithoutCctor::get_One we can see a likely candidate for code inlining! For simple value types, replacing static fields with getter properties can not only have improvements to code size at call sites, but can trade potential cache misses for inline code. We're not having to hit the TypeInfo (or the per-method init check) to then get the memory of the statics allocation to copy the value of 'One'.
IL2CPP optimization idea: It could be beneficial to expose IL2CPP specific attributes, just like Il2CppSetOption, that say a given type should always be initialized at runtime so that the prologue and IL2CPP_RUNTIME_CLASS_INIT can be excluded from call sites.

Final Thoughts

Walking away from this blog you keep in mind these simple things:
  • Metadata/Reflection are not free lunches. In fact, they could be leading to obese code where you least expect it!
  • Calling GetType() on a concrete type results in a call to Object.GetType(), which in the case of structs requires the value to be boxed to an object (needless memory allocation!).
  • Be mindful of how you use static fields and constructors in structs, as they too can be bloating your code. This is especially the case if you came from a C++ background, as cctors are not like the 'dynamic initializers' you're used to.
I didn't investigate bullets #1 and #4 from the Runtime::ClassInit code. Primarily because I wasn't really focusing on reference types in the article. However, with #1 you can 'rest' assured that after every first instance of "var = new T()" in a method there's a RUNTIME_CLASS_INIT call inserted immediately before it. Whether this really impacts your core game logic is an exercise for you to run.

I should also note that string literals ("Hello World!", etc) are considered metadata too and so using them will result in similar prologues. However, their initialization isn't as liberally applied to call sites of the method which actually uses them like is the case with types containing static fields. I'd say the main time they'd be a concern is when you throw argument exceptions (naming the problem arg in a string) in core game logic. But if you're doing such sanity checks in release builds of performance critical code you probably have bigger issues at hand than how many native instructions your method spills out to.

Bonus Round: IL2CPP Ideas

In the future of IL2CPP, I can see a handful of interesting optimizations that could be done: C++-like statics, constexpr mimicking, and (while note entirely related to this article) bitfields to name a few. All of which should be doable without upsetting the Mono environment which our Editor builds still need to be ran in.

C++-like statics: Some performance critical types could have all or maybe just some statics marked as 'permanent' statics. By this I mean their storage exists in a .data section of the object code, instead of being allocated in strict managed memory (along with all the compiler generated checks we saw above). If these statics are pointerless (not containing managed references), then they should not even need to be registered with the GC. If they are pointerful (do contain managed references), it should just be a matter of instead creating a .roots section and using the GC's APIs for marking foreign memory ranges as candidates for root scanning.

Constexpr - C++11 introduced constant expressions and I believe with IL2CPP we could have constant expressions for our C# too! You could have types with static data which are executed as part of the IL2CPP build step to generate readonly data. Imagine we could apply a (made up) ConstExprAttribute to Vector3.up (a static property getter). IL2CPP could sandbox the UnityEngine assembly or some driver and use the results of Vector3.up to initialize a C++-like static const field with its value (or as an actual constexpr if the C++ compiler used supports it!). It should be possible to even elevate this to arrays of simple/pointerless structs even (eg, a integer lookup table). Under the hood it could just use a std::array for storage, translating .Length and indexers to the matching C++ constructs (some care has to be taken here, since C++ would be using the unsigned size_t for the array length while Length is signed in C#).

Bitfields - It may be possible to mark boolean fields has having their C++ transformations become "bool mSomeFlag : 1". If the type doesn't have a StructLayoutAttribute, IL2CPP could even re-order the bitfield booleans to exist sequentially in the C++ declaration. There could be some issues with this regarding reflection or usage as ref/out arguments but if you're going to go as far as use bitfields then you should know of these problems or respect any compiler glue that may be generated to make them Just Work.

Cherry On Top: History of Metadata Initialization Prologue

I want to point out that metadata init prologues weren't always generated. It wasn't until Unity 4.6.p3 and 5.2.1p2 that these two improvements were made:

  • IL2CPP: Optimize System.Reflection access to metadata.
  • IL2CPP: Reduce initialization time of IL2CPP scripting backend.

Up until then all TypeInfo was initialized at startup, instead of only when needed which is now ensured by injecting the prologue in user code. That initialization time was really taxing the iOS game I was working on at the time (of course it probably doesn't help how type heavy the game code is and that the game had to run on iPhone 4s).

Sunday, November 29, 2015

Seeing Sharp C++: The Il2CppSetOption attribute

The Mysterious Attribute

On November 5th Unity released patches 4.6.9p2 and 5.2.2p3 which included the following improvement:

"IL2CPP: Allow null checks, array bounds checks, and divide by zero checks to be selectively included or omitted from generated C++ code by using a C# attribute on type, method, or property definitions."

Note the lack of documentation of which C# attribute allows you to net these advance controls. Perhaps that's the thing though, that Unity considers these fringe tools and that the people who truly need it will find the attribute on their own. Well, while working on another blog entry related to IL2CPP I recalled this patch note and assumed a few keywords in a search on Google would cure my curiosity. Alas, no answers were served and my curiosity grew.

UPDATE: Unity's Josh Peterson pointed out to me that they do already have existing documentation on the new attribute on the Unity forums (he also covers the divide-by-zero check there, which I don't). Information on these options will show up soon in the Unity manual (in the Scripting section) soon.

Back in the Spring I was working on getting a project running with IL2CPP and I had to do my own digging into the IL2CPP.exe's guts and the Editor's pipeline. I remembered there being options it would set on the IL2CPP.exe command line like "--emit-null-checks" (it and the other options are not publicly documented either, since they're internally determined by the project settings), and figured I'd throw the .exe back into ILSpy again and see how it resolved this mysterious C# attribute.

So as I'm digging into the Unity program folder for the .exe, what do I immediately find? A lone C# file called Il2CppSetOptionAttribute.cs. Turns out the oil was closer to the surface than I thought. If you have the patches mentioned above or later, the path to this C# file should be: %UNITY_INSTALL_DIR%/Editor/Data/il2cpp/Il2CppSetOptionAttribute.cs.

I imagine that one day this attribute will become a more formal construct (maybe getting added to a default assembly like UnityEngine.dll) as IL2CPP matures and surpasses Mono as the primary runtime. I know Unity has been really pushing faster release cycles (kudos to them!), so documentation and mainline integration may take a back seat on such fringe tools.

However, I suppose I probably shouldn't stop here. Why not take a look at how these tools change the C++ code that was generated from IL-ized C#?

Using the Attribute

Using Unity 5.2.3p1 I created a blank project and added Il2CppSetOptionAttribute.cs to it so I could test drive the attribute. I changed the target platform to iOS and kept its stock build settings. From there I added the code found in [Listing1] and built the Xcode project. [Listing2] shows the IL which Mono generated and [Listing3] contains the C++ code that IL2CPP output (I removed uninteresting code, like the method initialization prologue).

Il2CppSetOption_ChecksEnabled is a method with some basic operations that mimic typical C# code.

  • It allocates an int[] with one element (to test NullChecks and ArrayBoundChecks later)
  • Multiplies the 1st (and only) element by one (no-op)
  • Returns the non-existant 2nd element


Il2CppSetOption_ChecksDisabled is the same exact code, but with Il2CppSetOption attributes applied so that NullChecks and ArrayBoundChecks are excluded from its C++.

Here's the C++ code, with comments mapping instructions back to the source C# and notes for which instructions are removed with Il2CppSetOption:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// System.Int32 SharpCpp.SharpCode::Il2CppSetOption_ChecksEnabled()
extern "C" int32_t SharpCode_Il2CppSetOption_ChecksEnabled_m_1098767581_0 (Object_t * __this /* static, unused */, const MethodInfo* method)
{
 // essentially creating a "int[]" pointer here
 Int32U5BU5D_t1872284309_0* V_0 = {0};
 {
  //var array = new int[1];
  V_0 = ((Int32U5BU5D_t1872284309_0*)SZArrayNew(Int32U5BU5D_t1872284309_0_il2cpp_TypeInfo_var, (uint32_t)1));

  Int32U5BU5D_t1872284309_0* L_0 = V_0;
  NullCheck(L_0); // NOTE: Removed with Il2CppSetOption
  IL2CPP_ARRAY_BOUNDS_CHECK(L_0, 0); // NOTE: Removed with Il2CppSetOption
  //address of array[0]
  int32_t* L_1 = ((int32_t*)(int32_t*)SZArrayLdElema(L_0, 0, sizeof(int32_t)));
  //array[0] *= 1;
  *((int32_t*)(L_1)) = (int32_t)((int32_t)((int32_t)(*((int32_t*)L_1))*(int32_t)1));

  //return array[2];
  Int32U5BU5D_t1872284309_0* L_2 = V_0;
  NullCheck(L_2); // NOTE: Removed with Il2CppSetOption
  IL2CPP_ARRAY_BOUNDS_CHECK(L_2, 2); // NOTE: Removed with Il2CppSetOption
  int32_t L_3 = 2;
  return (*(int32_t*)(int32_t*)SZArrayLdElema(L_2, L_3, sizeof(int32_t)));
 }
}

Looking at the code we can see that it's creating a lot of temporary variables to hold values which don't mutate. The Common Intermediate Language is stack based, so all results are pushed/popped to and from the stack (instead of choice registers like in x86 assembly). So when Unity performs IL->CPP transformations they end up tracing the stack operations modeled by the instructions to figure out what variables they need, even if they are really just aliases. A modern C++ compiler should be able to catch these needless copies and collapse them to the bare minimum when compiling to native assembly (as least in Release configurations). This means it becomes a non-problem for the IL2CPP team, while somewhat of a eye sore for readers of the codegen :-).

The lines with "NOTE: Removed with Il2CppSetOption" are absent from the Il2CppSetOption_ChecksDisabled method. There's only two constructs which are removed (NullCheck and IL2CPP_ARRAY_BOUNDS_CHECK), but due to the variable aliasing we end up with repeated (redundant) NullChecks.

Refer to [Listing4] to investigate these constructs and determine what their impact is..

Nothing too crazy is going on here. For the general case all these checks are fine (and I imagine equate to the same number of times the checks are ran in the Mono runtime). However, you can imagine how verbose these can become when you're writing a tight loop that performs many reads and\or writes from an array. If you performed loop unrolling yourself, these checks would explode your loop's body's logic! Don't believe me? Just look at Il2CppSetOption_ChecksEnabledMegaLogic's C++!

If you have a low-level background, I bet you're already picturing the many, many possible branches these managed checks resolve to. However, since the branches are for error cases, they shouldn't be hit except in abnormal conditions so the branch predictor (found on modern CPUs, including the ARM in the iPhone 4s) won't fail during normal processing. But this leads to another factor: code size. The function requires more instructions to represent whatever branch-ful assembly the compiler comes up with. Meaning you can fit less meaningful instructions in the processor's cache.

Some of you may be scoffing at why C# code should even need to worry about this, that if the process needs to be that performant then one should just do it all in C++! Sure, you could do that...but don't forget to account for the extra steps it takes to maintain a native library which needs to be hooked up to run on your target platforms and within the Unity editor. One of the awesome things about Unity is being able to write-one and deploy-to-many platforms, so the goal is to not venture out of this bubble if you can afford to.

However, maybe you already have a pre-existing 'native core' at your studio to where this link problem is already solved, or maintained by Mr Not-You down the hall. We have to keep in mind that for smaller studios or developers wanting to get their plugins up on the Unity Asset Store, this is not an option or a very unattractive one (especially to the consumers of said plugins).

Final Thoughts

Should the need ever arise, you now know how to disable these managed checks yourself in your own project. Hopefully this article has motivated you to investigate how your C# code is being transformed into C++ code with IL2CPP. You may find that you're producing non-optimal IL and in turn C++. Knowledge is power after all. Of course, with great power comes great responsibility, so always be sure to measure (profile) and if you're already this low-level, check the C++ compiler's output too.

You could even try building your project with Visual C# and seeing how the IL may differs from the very old Mono compiler that comes Unity. As a reward for reading this far, you can check out [Listing5] for the IL generated by Visual C# 2013 in Release mode and [Listing6] for the resulting C++ code. You can see are are some curious differences (the starting NOP I believe is for IL code alignment...not sure about the seemingly pointless GOTO).

Also, if you haven't already, I suggest you check out Unity's own IL2CPP Internals blog series. I have some more IL2CPP things in the blog pipe and willing be using the series as reference point.