Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unity Support + Mono #36

Open
DaZombieKiller opened this issue Aug 12, 2024 · 2 comments
Open

Unity Support + Mono #36

DaZombieKiller opened this issue Aug 12, 2024 · 2 comments

Comments

@DaZombieKiller
Copy link

First off, awesome project! I am happy to look into contributing parts of this feature if necessary.

Due to the fact that Unity does not use MSBuild or support NuGet, it is currently non-trivial to consume DistIL from it. Some Unity-specific code would need to be written that uses CompilationPipeline.assemblyCompilationFinished to run DistIL. As Unity only supports .NET Standard 2.1 at the latest, it would likely need to use DistIL.Cli rather than running it in-process.

The JIT codegen in legacy Mono (and by extension, Unity) is typically rather poor, but can be significantly improved with the right tricks. I see a lot of potential in this project for improving the situation.

Some potential optimization passes that come to mind are:

  • System.Numerics.Vector4 receives hardware acceleration in Mono (System.Numerics.Plane and System.Numerics.Quaternion do too, if we're talking about dotnet/runtime Mono) so an optimization pass that replaces UnityEngine.Vector4/Unity.Mathematics.float4 with System.Numerics.Vector4 when possible can provide significant improvements.
    • The same actually applies to CoreCLR to an extent -- replacing Godot/XNA's Vector2/Vector3/Vector4/etc. types with the System.Numerics equivalents can significantly improve codegen due to them being intrinsified in the JIT.
    • Vector<T> is not currently accelerated in Unity due to a bug. A fix exists but it has yet to make its way into an update.
  • With the exception of System.Numerics.Vector4 due to its intrinsification, passing numerics types (and their third-party equivalents) by reference may often result in better code under Mono.
  • Some APIs in Unity are unnecessarily implemented in native code and are merely internal calls on the managed side (such as UnityEngine.Quaternion.Inverse, which just negates the x, y and z components. These could be transformed into regular IL implementations to benefit from inlining and avoid the icall overhead.
  • There are many frequently-accessed properties in Unity whose values do not change, yet each access results in an internal call (such as UnityEngine.GameObject.get_transform). An optimization pass could inject a new field into types and cache the result of the internal call to avoid the overhead. This could arguably be considered an invalid optimization due to it changing type layouts.
  • Unity has several APIs that mimic those on System.Runtime.CompilerServices.Unsafe, but due to the fact that they are not intrinsified by Mono they may not always be inlined and can produce worse code. This is not ideal as the Unsafe class is not available in-box due to Unity targeting .NET Standard 2.1 and lacking any NuGet support.
  • Unity has a NativeArray<T> type that is essentially a pointer and length with some allocation and permissions tracking. Many of its members produce less than ideal code under JIT and due to its indexer being by-value rather than by-ref, it can result in a lot of copying. Burst (Unity's toolchain that runs a subset of IL through LLVM) will often optimize these copies out, among other things. An IL optimization pass can bring regular JIT-mode code a bit closer to its performance.
  • In Unity, MemoryExtensions.IndexOf and related APIs that take a StringComparison currently allocate a string copy of the input and forward to the string-based variants. This results in a significant memory and runtime performance issue. It can be somewhat mitigated by checking for StringComparison.Ordinal and forwarding to the other overloads that do not take a StringComparison, but the issue naturally persists for the other comparison types. An IL post processing pass could either embed a Span-based implementation of these comparisons, or call into the private Mono BCL methods that perform them (if I recall correctly, they take pointers at the lowest level).
@dubiousconst282
Copy link
Owner

Thanks for your interest. I'm not very familiar with the Unity ecosystem at this time so any patches would be welcome.

Invoking DistIL.Cli directly would be fine, and it's what the MSBuild task currently does. This is a bit wasteful since it has to reload and process everything from scratch every time, so it might become a problem if it needs to happen very frequently.

I should mention that I'm also not very sure how well DistIL output will play with Mono at its current state, because it hardcodes some assumptions about .NET core being the only/main target, so more work might be needed than just the build pipeline. (for example, the IR type system has some reliance on System.Private.CoreLib, and some opts emit direct access to private List/Span fields.)

Most of the optimizations you have listed don't seem super complicated to implement at first (the devil is always in the details ofc), apart mostly from interprocedural rewrites. I feel like it would be more productive to fix some of them directly instead of implementing opt passes, but that's not a strong opinion and I understand this might involve some bureaucracy and things move slowly lol.

Some minor implementation notes I can think of:

  • Property caching could be partially handled by value numbering if side-effect info is added in GlobalFunctionEffects, but this would have limited impact overall.
  • Replacing native/bad impls could be done by re-implementing them in a separate assembly, so that call instructions can be redirected without much effort.
  • Some of the Unsafe intrinsics could be replaced with the corresponding IL instructions directly (maybe apart from As<> and others, which I believe are needed by the JIT for type tracking)

@DaZombieKiller
Copy link
Author

I should mention that I'm also not very sure how well DistIL output will play with Mono at its current state, because it hardcodes some assumptions about .NET core being the only/main target, so more work might be needed than just the build pipeline. (for example, the IR type system has some reliance on System.Private.CoreLib, and some opts emit direct access to private List/Span fields.)

Mono's BCL is roughly a 30%/30%/40% split between .NET Framework, .NET Core and custom. List<T> and Span<T> use the .NET Core implementations (for Span<T> it is the ByReference<T> implementation rather than one using ref fields). So I think that should be fine for the most part.

Most of the optimizations you have listed don't seem super complicated to implement at first (the devil is always in the details ofc), apart mostly from interprocedural rewrites. I feel like it would be more productive to fix some of them directly instead of implementing opt passes, but that's not a strong opinion and I understand this might involve some bureaucracy and things move slowly lol.

Yeah, ideally stuff like that would just be fixed within Unity itself. That said, many projects are stuck on specific versions of the engine with no feasible upgrade path, so they wouldn't be able to benefit from it.

Some of the Unsafe intrinsics could be replaced with the corresponding IL instructions directly (maybe apart from As<> and others, which I believe are needed by the JIT for type tracking)

Mono has intrinsic recognition for the Unsafe type, so it may be more beneficial to call directly into the internal implementation available in the BCL. That said, I haven't really verified what the codegen difference is like between Unsafe and the equivalent IL under Mono yet.

I should also note: Mono (specifically talking about legacy Mono & Unity, rather than the newer Mono from dotnet/runtime) does not recognize IgnoresAccessChecksToAttribute. The closest equivalent under Mono would be this:

[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]

Which will give you the same behavior as if you had [IgnoresAccessChecksTo]'d every single assembly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants