Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Limit generation of IL2CPP CCWs and GenericComDefinitions?

Discussion in 'Windows' started by holo-krzysztof, May 17, 2022.

  1. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    We have a moderately-sized project that we build for Windows Standalone (Mono) as well as HoloLens (UWP IL2CPP) and I have noticed that the latter generates lots of files (around 600) with the name `Il2CppGenericComDefinitionsXXX.cpp`, most of which contain this:
    Code (CSharp):
    1. #include "pch-cpp.hpp"
    2.  
    3. #ifndef _MSC_VER
    4. # include <alloca.h>
    5. #else
    6. # include <malloc.h>
    7. #endif
    8.  
    9.  
    10. IL2CPP_EXTERN_C_BEGIN
    11. IL2CPP_EXTERN_C_END
    12.  
    In one of the files which actually contains something it looks like it's instantiations of generic WinRT types, like `IAsyncOperation<SomeType>`. There's around 5 of them, the rest looks like the code block above.
    Is it possible to reduce the number of these generated files somehow?

    The other kind of generated file is `Il2CppCCWsXXX.cpp`.
    These actually all contain code and the name says it's COM callable wrappers, but what exactly are they for? To interop with WinRT collection types internally, maybe (like IVector<T>)?

    Is it possible to limit the amount of generated CCWs somehow? I'm not 100% sure if we need so many of them, the UWP-specific code is small in volume compared to the size of the project itself. Also, CCWs are consistently showing up in the top 10 files taking the longest time to build.

    Any help/explanations would be appreciated. I haven't found any docs concerning the generation of those files, and the names suggest they're Windows-specific. Unfortunately that's the only platform we support right now, so I can't check if e.g. Android would generate a similar amount of them (or any at all).
     
    Last edited: May 17, 2022
  2. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    You are right that these types are for interopping with WinRT code. COM callable wrappers are managed objects wrapped in COM interfaces. When you're passing a managed object to a WinRT API, that WinRT API is actually taking a specific COM interface. In each case, IL2CPP has to generate code to convert each parameter from a managed object to a COM interface, and to do that, it uses these COM calling wrappers.

    There are two types of COM callable wrappers:

    1. Specific COM callable wrappers for specific types. These get used when a type implements any interface that are either Windows Runtime interfaces, or project to Windows Runtime - in other words, they can be used by code on the other side of WinRT API call (term we use within IL2CPP is "exposed to Windows Runtime"). The list of interfaces that project to Windows Runtime can be found here: https://github.com/jbevain/cecil/bl.../Mono.Cecil/WindowsRuntimeProjections.cs#L140
    2. COM callable wrappers for types that don't implement any interfaces that are exposed to Windows Runtime - these just use the COM callable wrapper class for System.Object.

    Unfortunately, IL2CPP has to be pretty conservative and generate specific COM callable wrappers for all types for it to work properly due to how WinRT interop is designed. COM callable wrappers for types that implement generic interfaces that are exposed to Windows Runtime (like IList<T>) get put into Il2CppGenericComDefinitionsXXX.cpp.

    Imagine a WinRT API like this:

    Code (csharp):
    1. public class NativeClass
    2. {
    3.     void NativeMethod(object o);
    4. }
    You can call this API with any managed object. IL2CPP has no visibility on what the code on the other side of Windows Runtime API boundary does. For all you know, it could be casting the object to IVector<HSTRING>, which projects as "IList<string>" in C# land and IL2CPP has to ensure the cast not only succeeds, but the native code is able to call every method on that interface. When we first implemented Windows Runtime projection support to IL2CPP, we tried to avoid this but unfortunately this is a very common pattern found throughout Windows Runtime APIs and things kept breaking until we decided to generate COM Callable Wrappers for every scenario.

    That said if you're seeing files without any code in them, that is probably a bug. If you could report it, we'd investigate and fix it.
     
  3. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    Very interesting, thank you for the response.

    I'll try to repro the empty files thing with a smaller project and report back if there's something.

    I do wonder though, would it help if there was a way to say "this assembly does not reference WinRT at all"?
    Then again, for public types of that assembly at least, you'd probably still have to generate CCWs since some other assembly that does use WinRT might stick something into a dictionary and pass that into a WinRT object method...
     
  4. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    Yup, exactly. If you couldn't do that, we would be able to get rid of a lot of that code :(.
     
  5. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    Right now, if an assembly does not call WinRT in any way, are CCWs generated for it still? Does reference rewriter always add .winmd references when building for UWP?
     
  6. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    Also, I've reported the empty Il2CppGenericComDefinitionsXXX files bug with a sample project that contains MRTK.
    It's number 1429416.
     
  7. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    It doesn't matter if the assembly is using it or not. mscorlib.dll never references windows.winmd, but you can still pass List<T> to windows runtime APIs.

    Reference Rewriter doesn't add new references. It just fixes up differences between Microsoft's .NET for UWP and IL2CPP differences.

    Thanks for the bug report!
     
  8. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    Sorry, I got a bit confused; I meant a setup like this:

    - AssemblyThatUsesDictionariesButNoWinRT.dll
    - AssemblyThatUsesWinRT.dll

    and there is no reference between those two. In theory there should be no CCWs for the 1st assembly's public types, assuming it doesn't use WinRT itself internally?
     
  9. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    The problem is that figuring that out is very hard - as I said we tried doing such analysis and not only it was prohibitively slow, corner cases kept popping up. Here's a convoluted example that uses a very common dependency injection pattern:

    Code (csharp):
    1. // In AssemblyThatUsesDictionariesButNoWinRT.dll:
    2. interface IDoStuff
    3. {
    4.     void RaiseEvent(object sender, string name);
    5. }
    6.  
    7. class Doer
    8. {
    9.     IDoStuff m_DoStuff;
    10.  
    11.     public Doer(IDoStuff doStuff)
    12.     {
    13.         m_DoStuff = doStuff;
    14.     }
    15.  
    16.     public void CompleteDo()
    17.     {
    18.         m_DoStuff.RaiseEvent(new Dictionary<object, object>(), "Complete!");
    19.     }
    20. }
    21.  
    22. // In AssemblyThatUsesWinRT.dll
    23. public class SomeAPIWrapper
    24. {
    25.     public static void NotifyEventRaised(object sender, string name)
    26.     {
    27.         WinRTAPI.NotifyEventRaised(sender, name);
    28.     }
    29. }
    30.  
    31. // In Assembly-CSharp.dll:
    32. class MyScript : MonoBehaviour, IDoStuff
    33. {
    34.     Doer m_Doer;
    35.  
    36.     void Awake()
    37.     {
    38.         m_Doer = new Doer(this);
    39.     }
    40.  
    41.     void Update()
    42.     {
    43.         m_Doer.CompleteDo();
    44.     }
    45.  
    46.     void IDoStuff.RaiseEvent(object sender, string name)
    47.     {
    48.         SomeAPIWrapper.NotifyEventRaised(sender, name);
    49.     }
    50. }
     
  10. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    I see, so it's also transitive dependencies/references and passing around objects/IInspectable... :/

    Realistically, is there anything we can do to improve codegen/build times aside from being cautious about not to overuse generics and WinRT APIs maybe?
    I read that 2021 LTS improves build times a bit, so might get a speedup there, but we're not quite ready to migrate yet.

    Also, thank you for the detailed explanations so far. :)
     
  11. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    Excluding your project/build folders from Windows Defender will significantly improve build times if you haven't done that yet. When we fix those empty files issue, that should also result in lower build times.

    How long of build times are we talking about?
     
  12. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    I've already excluded my project directory from defender, so it should not slow down the process.

    I just ran a build into a fresh directory, timings are:

    1:30 from Unity (with fully cached shaders), without script debugging
    3:10 compiling/linking IL2CPP binary (Release mode)
    then maybe another minute to actually deploy to HoloLens, but that's Microsoft's tools then.

    With managed debugger support it takes longer than that because there's more code being generated. Do you need those numbers as well?

    So it's like 5 minutes from code change to actually testing it on device, and I got a pretty beefy machine to begin with (Core i9-12900K, 64GB RAM). Building on a high-end laptop from 2020/2021 can take 8-10 minutes.
    Later there's also incremental builds, those don't take as much, but like still around a minute or 2 depending on how big the change is and how many files are regenerated by IL2CPP.
     
  13. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    Switching to ExecutableOnly build mode (instead of D3D project) should make iteration speed faster as it skips generating Visual Studio project (which then you also don't have to build). Perhaps you could try it out?

    3 minutes sounds crazy high... I don't think I've ever seen it take that long. How much code does IL2CPP actually generate? Do you by any chance have excessive link.xml files in the project to prevent stripping? What's your stripping setting at?
     
  14. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    Just tried ExecutableOnly build, but it fails with:

    Exception: Unity.IL2CPP.Building.BuilderFailedException: schemaClass.cpp
    C:\ARES\Library\Il2cppBuildCache\UWP\VSProject\il2cppOutput\schemaClass.cpp(24): fatal error C1083: Cannot open include file: '{{ libraryPath }}/{{ cls.GetHeaderFile() }}': No such file or directory


    Anyway, it compiles the same amount of code and in the end has to link against a UWP app stub, right? I don't think it'd be much different timing-wise, except for not having to open VS to build, which can be nice.

    About link.xml files:
    There's a bunch of them in the project. Some 3rd party dependencies have their own (MRTK, Photon, Zenject, Odin). Also, for some reason the AWS S3 SDK is also set to preserve in one of our own link.xml files which looks unnecessary to me.
    I guess if we could trim some of those, we'd get a better build time.

    Also, stripping level is set to low. Maybe I could try going with medium and see if it improves things.
     
  15. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    I have deleted some stuff from link.xml and the results are:

    1:22 from Unity (with fully cached shaders), without script debugging
    Total compilation time: 129424 milliseconds.
    Total link time: 29596 milliseconds.
    ObjectFiles: 1987 of which compiled: 1987 (previously around 2050 object files)

    So it's 2:40 building from VS, already an improvement. I hope I didn't break anything in the process, but smoke test didn't show anything. Stripping level is still low.

    Here's some of the stats that the build process gives me:
    1> Time Compile: 128868 milliseconds Il2CppCCWs27.cpp
    1> Time Compile: 127900 milliseconds Il2CppCCWs345.cpp
    1> Time Compile: 104105 milliseconds Il2CppCCWs177.cpp
    1> Time Compile: 24676 milliseconds Generics70.cpp
    1> Time Compile: 17976 milliseconds HoloLight.GeneralImportService.Runtime2.cpp
    1> Time Compile: 17925 milliseconds Il2CppInvokerTable.cpp
    1> Time Compile: 17585 milliseconds Il2CppPCCWMethods.cpp
    1> Time Compile: 16773 milliseconds Il2CppCCWs409.cpp
    1> Time Compile: 13957 milliseconds Il2CppCCWs412.cpp
    1> Time Compile: 12632 milliseconds Generics29.cpp


    I think the biggest offender is our "main" assembly which has lots of unnecessary stuff inside and generates 18 .cpp files.
    Also, there's 32 GenericMethods and 144 Generics .cpp files generated; there's too many generics crimes in the codebase. I want to reduce those, but it's going to take some time.
    It's probably going to reduce the amount of CCWs that have to be generated.
     
  16. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    schemaClass.cpp - is that something that's in your project folder? Looks like it relies on it getting patched after the build is done or something?

    It avoids moving a lot of files to the output folder. The stub is precompiled so you don't need to compile it. Our automated tests sped up 2x when we moved to ExecutableOnly build type.
     
  17. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    It's from the USD package as far as I can see. Not quite sure what it does though, haven't encountered it before.
    I know that USD has a post-build step, might be related to that.
    Found it here: https://github.com/Unity-Technologi.../usd/resources/codegenTemplates/schemaClass.h

    Not sure if this is the exact version because we have a fork of that package that adds HoloLens support (which was made by an external company and not updated ever since), but it used to exist upstream at some point.
     
    Last edited: May 25, 2022
  18. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    Probably that HoloLens support expects the .cpp files to not get compiled by Unity and expects Unity to only put them into the generated VS project :(.
     
  19. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    I'm not sure if the HoloLens support is at fault here, but those files are gone in newer USD package versions.

    However, due to it being a fork etc. we'd have to look at upgrading/possibly upstreaming and this almost certainly won't happen right away, so ExecutableOnly is not an option ATM, sadly. :/

    Those files do exist for x64 builds too, but we use Mono for desktop, so didn't run into it there.
     
  20. holo-krzysztof

    holo-krzysztof

    Joined:
    Apr 5, 2017
    Posts:
    74
    So the respose I got on my bug report is that it's been fixed in 2021.2.
    Is it possible to backport this to 2020 LTS? We are not yet ready to migrate to 2021 and might not be able to do that until sometime next year.
     
  21. Tautvydas-Zilys

    Tautvydas-Zilys

    Unity Technologies

    Joined:
    Jul 25, 2013
    Posts:
    10,504
    Let me follow up on that.
     
    holo-krzysztof likes this.