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.