IL2CPP build size improvements

NOTE: The original post is outdated. Please refer to this knowledge base article instead - https://support.unity3d.com/hc/en-us/articles/208412186

Deprecated original article:

We created a small guide on IL2CPP build sizes. What is important, what can be improved and how you can improve it. We constantly work on the improvements and could already gain massive size improvements in the latest patches. Ensure always to use the latest patch, as we fix bug on daily basis and work on size improvements in parallel. :slight_smile: Although, many parts are similar to WebGL this guide is mainly targeted to iOS. But all the fixes we make for IL2CPP have direct affect on WebGL and iOS builds.

Why is the the binary size of the IL2CPP project larger that the mono version?

Two things apply:

  • If you make a universal build, there’s a 32bit slice and a 64bit slice, which contains the exact same executable for 2 different architectures so nearly doubled size.
  • IL2CPP produced bigger builds even if you just compare ARMv7 Mono with ARMv7 IL2CPP; this had to do with how we dealt with the type metadata. It was resolved in Unity 4.6.4p2.

With “Universal” builds, you will always yield larger binary sizes than just having ARMv7 or ARM64. The removal of the managed assemblies already made the builds smaller (in Unity 4.6.3f1), the real improvement came when we were able to construct at least generic typeinfos at runtime, but this is now partly possible (since Unity 4.6.4p1). But we have some more improvements in production, so stay tuned. Universal builds include 32-bit and 64-bit slices in the fat executable which results in at least doubled size of standard mono 32bit.

We made improvements to generic type and array generation in Unity 4.6.4p1 too. Apart from that, there’s information here about reducing your iOS build size: Unity - Manual: Optimizing the size of the built iOS Player. For additional information read in Stripping below.

Please always make a comparison table with mono (ARMv7), IL2CPP (ARMv7), IL2CPP (ARM64), IL2CPP Universal and which striping settings you use to have all information to compare!

It’s always worth to check the actual binary size, as this is the one which is affected by the source code compilation referred to as the fat file containing binary from ARMv7 and ARM64 if it is a universal build and therefore nearly doubled size of a single ARMv7 mono or IL2CPP build. If you make a universal build, there’s a 32bit slice and a 64bit slice, which contains the exact same executable for 2 different architectures so nearly doubled size. If you want to compare the output directly please compare ARMv7 mono with ARMv7 IL2CPP.

Which sizes limit does Apple check?

Apple has a 80MB limit for the 32bit+64bit code segment in total if you support a minimum OS of less than iOS 7.0. Apps that are set to minimum OS of 7.0 or greater have a binary size limit of 60MB (60 000 000 bytes) per architecture slice. 100MB (mobile data) application limit (user needs to download via Wifi if >100MB) and max 4GB application limit. 80MB and 60MB/60MB is the limit for code segment size included in the fat binary for the 32bit and 64bit slice. You need to use otool to get the right numbers. 100MB applies to to the .ipa size (Use the estimate button in Xcode after archiving to get the right estimate), this determines if users need a WiFi connection to download your app. Follow following steps to get the right numbers:

  • Build your app in release mode. Do not use debug mode, as it does not represent your final app. Ensure you use correct optimisation, more information can be found here: Unity - Manual: Optimizing the size of the built iOS Player.
  • Archive your app in XCode.
  • Use the estimate button to get the estimated size of your app to ensure if you are above or below 100MB or the overall size of 4GB.
  • Note:* since XCode 6.3 there is no estimate button anymore. You can calculate it by using following formula, but be aware that the compression coefficient might vary:
app_store_size = sizeof(.ipa/(zip with exec_binary removed)) + sizeof(codesegment(exec_binary)) + 0.2 * sizeof(datasegment(exec_binary))

The formula is coming from broader knowledge that only the code segment gets encrypted and the data segment compression ratio can be verified pretty easy.
Extract the data segment from executable via dd command (you can specify byte offset + length) and then try to compress it. The code segment gets scrambled to look like a perfect noise. You need to descramble it with a decryption key before executing. iTunes/App Store app manages there keys. That’s why we add code segment as whole without adjusting for compression ratio.

  • Once you have archived your app, show the .xcodearchive in your finder

  • Show Package Content

  • Go to Products/Applications/Productname.app. (Which should have nearly the same size as the estimated size button in XCode returns)

  • Show the content of the Productname.app

  • Locate the ProductName binary (exec_binary) on the top level.

  • Use otool to generate the output for the 80MB limit.

otool -l path_to_exec_binary or even size path_to_exec_binary

  • Now you can retrieve the output depending on the architecture (armv7 + a section arm64 if it’s a universal build) Gather the information for armv7 (LC_SEGMENT) and do the same for arm64 if applicable (LC_SEGMENT_64)

  • Locate the LC_SEGMENT with segname __TEXT and take the filesize
    code segment size = 30474240 ~= 30MB

  • Locate the LC_SEGMENT with segname __DATA and take the filesize
    data segment size (mostly metadata) = 10420224 ~= 10 MB

This results in following otool result table:

architecture armv7:
code segment size = 30474240 ~= 30MB
data segment size (mostly metadata) = 10420224 ~= 10 MB
segment + data segment = 30 + 10 = 40MB

architecture arm64:
code segment size = 25559040 ~= 26MB
data segment size (mostly metadata) = 17727488 ~= 18 MB
segment + data segment = 26 + 18 = 44MB

Apple uses for their check armv7(code segment)+arm64 (code segment) which results in a otool report of 30MB + 26MB = 56MB in this example which is below the 80MB for <7.0 and 30MB & 26MB are each below 60MB >=7.0 for a universal build.

It’s easy to test this, make a test app in release mode and submit it to the app store, the static project checker in the beginning should alert you if a slice is over a certain limit. For the estimated size XCode should take all the DRM measurements into account, but if you encounter a different behaviour please let us know! Again, as soon as you uploaded a test app to iTunesConnect, it should show you any pitfalls and you should see also the app size in the Version Summary page.

Stripping
The IL2CPP scripting backend always does byte code stripping, no matter what the “Stripping Level” setting is in the editor. The best option for a user is to set the “Stripping Level” option in the editor to a value of “Disabled”, as the affect of any other “Stripping Level” option will likely be minimal on executable size, because IL2CPP will strip anyway. If the you choose a “Stripping Level” value other than “Disabled” you could run into problems, because then the IL2CPP build toolchain in the editor will attempt to determine which native engine code is used by you scripting assemblies, and only register that native code when the player starts. If you encounter problems with your stripping settings and you believe they are wrong, please submit a bug report.

Exceptions
By default Mono exceptions are caught in Xcode, but IL2CPP exceptions are not.

You can make the Xcode debugger break on exceptions by adding an Exception breakpoint as described here: https://developer.apple.com/library/mac/recipes/xcode_help-breakpoint_navigator/articles/adding_an_exception_breakpoint.html

Replace the text “std::underflow_error” with “Il2CppExceptionWrapper” in the text-box shown on that page. You may need to restart Xcode, or possibly just the debug session.

In general the il2cpp managed callstacks are currently unreliable and on iOS only partially working - the way we do them often prints wrong functions. So for now you need to check the real callstacks int the XCode callstack window. We work on improvements on these areas though.

A managed exception is not necessarily an app crash. But to break on a managed exception in Xcode, set a breakpoint at il2cpp_codegen_raise_exception or in case of a nullreference exception you can set a breakpoint in NullCheck method in il2cpp-codegen.h You don’t want to break when the method is called, as it will be called very often, but you can break after the if check at the start of the NullCheck method.

5.3.x build size increase due to BitCode
Recently we are seeing number of questions scattered around forums regarding build size increase when building iOS applications with Unity 5.3.x often due to BitCode support enabled which is new in 5.3.x. This post aims to clarify some aspects of it in single place. Unity 5.3.x build size increase FAQ - Unity Engine - Unity Discussions

Some more information to read about IL2CPP and 64bit can be found on our blog:
IL2CPP blog posts
iOS 64bit support

I hope this helps and if you have any questions or suggestions to improve the post, please let me know! :sunglasses:

8 Likes

Can you elaborate on the generics part a bit more?
For example Dictionary<K,V> is bad. You suggest replacing it with something. Does replacing it with a Hashtable which is not generified do the trick? You mentioned using your “own implementation”.

Also you say generic lists and arrays. Is ArrayList a good substitute for List and what about arrays. Did you mean object[ ] arr; is bad?

We have encountered a bug in the il2cpp generation where a happily compiling project can create not compiling Xcode files by simplly adding or removing a large .cs file(iTween).

I noticed that our Bulk_Generics has hit file #100 which sounds fishy being such a nice round number. The error is related to a type missing:
ArraySegment_1_t12037

I wonder if we just have too much generics for this to handle. Our code base is pretty huge.

Hello,

did you file a bug for this codegen issue? We will be very happy in checking and fixing it :slight_smile:
Which version of Unity are you using? Are you still having the same issue with the latest patch release btw?

Thanks,
Gabriele

@greyhoundgames I had to remove the generic parts for now, as it lacks information and a proper example. I’ll work on one now and once I can provide you proper stats and examples, I’ll push it again. What I generally do is to compare my codebase or the function I need against what is provided by the default implementation and estimate if it is suitable to make my own minimal implementation which I can compare then in a specific minimal example to compare the output. However, this is not easy and needs to be evaluated very carefully. The array part did not come out correctly, in the latest version it’s not a problem to use them anymore, but in older versions the sizes of the generated code for arrays was not as improved as it is now. This should have been a hint if you have problems with the generated code sizes, you need to consider upgrading to one of the latest patch releases.

We are on 5.0.1p1. I eagrly await some sample ideas :wink: Also the comment about the 100 gen files. Does that sound normal or crazy high?

To Gabriele I have not yet made a bug. We have a massive project and I have to get permission to upload the source since its a high value project as well. Is there anything I can do for you in the meanwhile that would help?
[Update]
I tried again on today’s release(p3) but still cannot delete that file without the errors coming.

Oh can you also clarify which of these is a bad idea to use still
List
List
Dictionary<int, object>
Dictionary<MyStruct,object>
Dictionary<string,int>

Any chance you can create a smaller repro case that does not contain the sensitive information and send that one to us? It is by far the best way to get the issue fairly quickly :slight_smile:

Thanks,
Gabriele

@greyhoundgames

It is not surprising to have 100 Bulk_Generics .cpp files, it just happens to be exactly 100. As you make changes to the project or we make changes to the code generation, I would expect that number to differ. The il2cpp.exe chooses a number of types to include in each Biuld_Generics file, but it doesn’t do anything about the number of Bulk_Generics files generated, so this should not be a problem.

None of the generic types you mentioned are bad to use. I would continue to use them, as they provide nice expressive power in C#, and the alternatives are often more difficult to use. We have had some customers see binary size decreases by making some targeted changes to the use or implementation of these generic types. For most projects though, this is an optimization that you should only undertake after measuring.

It’s rather scary to read ‘generics are bad’ when you’ve got a sizeable codebase making heavy use of them and a project with serious memory issues (System.ExecutableAndDlls taking 92MB)…

System.ExecutableAndDlls is actually not where the memory pressure comes, because most of these pages are read only and can be reloaded from disk. You should be looking into Xcode Instruments Activity Monitor “Real memory” column or VM Tracker Dirty memory numbers.

@bluescrn

It’s not really correct to say that "generics are bad, actually. In past releases, the IL2CPP conversion process did cause generics to lead to increased code size. However, recent improvements in IL2CPP mean that generics are not a significant problem any more. n the next few weeks we will land changes to complete generics sharing, mean that the implementation of generics with the IL2CPP scripting backend will be on par with the implementation from the Mono scripting backend.

So I would not encourage you to move away from generics in your code. In fact, mscorlib makes heavy use of generics, so we need to do everything that we can to improve generics support no matter how much they are used in user script code.

With all of this said, we have has some users experience executable size decrease by moving away from generic types in some cases. This improvement is more about analysis of what parts of the managed code are actually being used (and removal of parts that are not necessary) than it is about generics in general.

As usual with any type of optimization, measurement and data are the best options.

The XCode figure is quite a lot larger than the Unity profiler figures…

In one test case, XCode shows a real memory usage of around 260MB

A detailed memory profiler capture shows numbers which add up to 209MB (92MB of which is System.ExecutableAndDlls).

The green ‘total allocated’ line on the memory profiler graph shows a ‘total allocated’ size of just 140MB?

So from the 260MB that’s allocated (according to Instruments), less than 120MB is accounted for by ‘assets’, ‘scene memory’ and ‘not saved’ (the textures, meshes, and other assets that we’d expect to be using the bulk of the memory!) - Something doesn’t seem quite right there?

Even if we are losing a whole 92MB to compiled code, that still leaves about 50MB not accounted for?

It’s Unity profiler missing big chunk of allocated memory. It is expected to be some underestimation on Unity side (because not all the low level measurement tools are available to Unity, that are available for Xcode), but this one sounds like serious problem. Are you running on Metal or GLES? Also do you have custom native plugins that would be making lots of allocations?

As IL2CPP could do stripping automatically , for reducing IPA size and memory pressure, is it necessary to use link.xml for IL2CPP build now?

In a IL2CPP build, “System.ExectableAndDlls” takes a usage of 133MB(whole usage of the game is 250MB), I didn’t get any way to decrease this value. Beside reducing asset loading, how to reduce memory pressure for “IL2CPP app”?

@big_march

It is necessary to use link.xml only if you need to preserve some types from being stripped. Usually this happens with serializers, as they tend to use types at runtime. The need to link.xml will vary on a project by project basis.

I’m not sure how to reduce memory pressure other than to reduce the use of assets that are unnecessary. We’re looking now at how to lower the IL2CPP memory usage to bring it in line with Mono, but we don’t have anything ready yet.

So in IL2CPP, using “stripping level” and “link.xml” was no longer a way to reduce memory usage, right?

@big_march

In IL2CPP stripping is always enabled. So even if the “Stripping Level” setting in the editor has a value of “Disabled”, the IL2CPP scripting backend is still doing the equivalent of the “Byte code stripping” value. So you can same that “Stripping Level” is no longer a way to reduce executable size (and therefore memory), but that’s only because IL2CPP is already doing stripping!

The link.xml file is used to augment stripping, to actually include code that would otherwise be stripped. It’s role is unchanged with IL2CPP.

I hope this clears things up, let me know if it is not clear. Thanks.

I’ve learned.Thank you for detail explanation.
Now I got a reason to stop trying to use stripping to reduce memory usage in IL2CPP.
But in IL2CPP bulid, “System.ExectableAndDlls” in Profiler still cost 135MB, which is over half of the total memory usage, is there any way to make it smaller?

@big_march

As long as you are sure that there are not any managed or native plugins in the build that you don’t actually need, there are not too many options to make that smaller. We’re working on some things from our end to decrease the executable size though, so this should decrease in future releases.

Hi everybody. Just sharing the idea that worked for us.

When uploading the build to the store from the Organizer there is an option to include/exclude the symbols. (dSYM file). It is not required by Apple but the checkbox is checked by default. Maybe it could be an idea for you to try if you didn’t try it yet. On our case excluding the debug symbols lowered to game from 101 MB to 64 MB. (We were surprised. We expected to save around 1-2 MB but it seems it uses a lot of space). This size is according to the test flight. It might add up a bit when it goes live due to encryption .

4 Likes