|
switch (t.GetTypeCode()) |
|
{ |
|
case TypeCode.Object: |
|
{ |
|
if (t != typeof(object) && (IndexIsNotReturnType(0, target, pi) || t.IsValueType)) |
|
{ |
|
// if we're on the return type relaxed delegates makes it ok to use object |
|
goto default; |
|
} |
|
return FastCreate<Object>(target, pi); |
|
} |
|
case TypeCode.Int16: return FastCreate<Int16>(target, pi); |
|
case TypeCode.Int32: return FastCreate<Int32>(target, pi); |
|
case TypeCode.Int64: return FastCreate<Int64>(target, pi); |
|
case TypeCode.Boolean: return FastCreate<Boolean>(target, pi); |
|
case TypeCode.Char: return FastCreate<Char>(target, pi); |
|
case TypeCode.Byte: return FastCreate<Byte>(target, pi); |
|
case TypeCode.Decimal: return FastCreate<Decimal>(target, pi); |
|
case TypeCode.DateTime: return FastCreate<DateTime>(target, pi); |
|
case TypeCode.Double: return FastCreate<Double>(target, pi); |
|
case TypeCode.Single: return FastCreate<Single>(target, pi); |
|
case TypeCode.UInt16: return FastCreate<UInt16>(target, pi); |
|
case TypeCode.UInt32: return FastCreate<UInt32>(target, pi); |
|
case TypeCode.UInt64: return FastCreate<UInt64>(target, pi); |
|
case TypeCode.String: return FastCreate<String>(target, pi); |
|
case TypeCode.SByte: return FastCreate<SByte>(target, pi); |
|
default: return SlowCreate(target, pi); |
|
} |
|
} |
|
|
|
private static CallInstruction FastCreate<T0>(MethodInfo target, ParameterInfo[] pi) |
|
{ |
|
Type t = TryGetParameterOrReturnType(target, pi, 1); |
|
if (t == null) |
|
{ |
|
if (target.ReturnType == typeof(void)) |
|
{ |
|
return new ActionCallInstruction<T0>(target); |
|
} |
|
return new FuncCallInstruction<T0>(target); |
|
} |
|
|
|
if (t.IsEnum) return SlowCreate(target, pi); |
|
switch (t.GetTypeCode()) |
|
{ |
|
case TypeCode.Object: |
|
{ |
|
if (t != typeof(object) && (IndexIsNotReturnType(1, target, pi) || t.IsValueType)) |
|
{ |
|
// if we're on the return type relaxed delegates makes it ok to use object |
|
goto default; |
|
} |
|
return FastCreate<T0, Object>(target, pi); |
|
} |
|
case TypeCode.Int16: return FastCreate<T0, Int16>(target, pi); |
|
case TypeCode.Int32: return FastCreate<T0, Int32>(target, pi); |
|
case TypeCode.Int64: return FastCreate<T0, Int64>(target, pi); |
|
case TypeCode.Boolean: return FastCreate<T0, Boolean>(target, pi); |
|
case TypeCode.Char: return FastCreate<T0, Char>(target, pi); |
|
case TypeCode.Byte: return FastCreate<T0, Byte>(target, pi); |
|
case TypeCode.Decimal: return FastCreate<T0, Decimal>(target, pi); |
|
case TypeCode.DateTime: return FastCreate<T0, DateTime>(target, pi); |
|
case TypeCode.Double: return FastCreate<T0, Double>(target, pi); |
|
case TypeCode.Single: return FastCreate<T0, Single>(target, pi); |
|
case TypeCode.UInt16: return FastCreate<T0, UInt16>(target, pi); |
|
case TypeCode.UInt32: return FastCreate<T0, UInt32>(target, pi); |
|
case TypeCode.UInt64: return FastCreate<T0, UInt64>(target, pi); |
|
case TypeCode.String: return FastCreate<T0, String>(target, pi); |
|
case TypeCode.SByte: return FastCreate<T0, SByte>(target, pi); |
|
default: return SlowCreate(target, pi); |
|
} |
|
} |
|
|
|
private static CallInstruction FastCreate<T0, T1>(MethodInfo target, ParameterInfo[] pi) |
|
{ |
|
Type t = TryGetParameterOrReturnType(target, pi, 2); |
|
if (t == null) |
|
{ |
|
if (target.ReturnType == typeof(void)) |
|
{ |
|
return new ActionCallInstruction<T0, T1>(target); |
|
} |
|
return new FuncCallInstruction<T0, T1>(target); |
|
} |
|
|
|
if (t.IsEnum) return SlowCreate(target, pi); |
|
switch (t.GetTypeCode()) |
|
{ |
|
case TypeCode.Object: |
|
{ |
|
Debug.Assert(pi.Length == 2); |
|
if (t.IsValueType) goto default; |
|
|
|
return new FuncCallInstruction<T0, T1, Object>(target); |
|
} |
|
case TypeCode.Int16: return new FuncCallInstruction<T0, T1, Int16>(target); |
|
case TypeCode.Int32: return new FuncCallInstruction<T0, T1, Int32>(target); |
|
case TypeCode.Int64: return new FuncCallInstruction<T0, T1, Int64>(target); |
|
case TypeCode.Boolean: return new FuncCallInstruction<T0, T1, Boolean>(target); |
|
case TypeCode.Char: return new FuncCallInstruction<T0, T1, Char>(target); |
|
case TypeCode.Byte: return new FuncCallInstruction<T0, T1, Byte>(target); |
|
case TypeCode.Decimal: return new FuncCallInstruction<T0, T1, Decimal>(target); |
|
case TypeCode.DateTime: return new FuncCallInstruction<T0, T1, DateTime>(target); |
|
case TypeCode.Double: return new FuncCallInstruction<T0, T1, Double>(target); |
|
case TypeCode.Single: return new FuncCallInstruction<T0, T1, Single>(target); |
|
case TypeCode.UInt16: return new FuncCallInstruction<T0, T1, UInt16>(target); |
|
case TypeCode.UInt32: return new FuncCallInstruction<T0, T1, UInt32>(target); |
|
case TypeCode.UInt64: return new FuncCallInstruction<T0, T1, UInt64>(target); |
|
case TypeCode.String: return new FuncCallInstruction<T0, T1, String>(target); |
|
case TypeCode.SByte: return new FuncCallInstruction<T0, T1, SByte>(target); |
|
default: return SlowCreate(target, pi); |
Description
There is a difference in how
System.Linq.Expressions.dllis built for iOS-like and other desktop/mobile platforms, which is observable here:runtime/eng/illink.targets
Line 226 in 2aa0c9b
runtime/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj
Line 19 in 2aa0c9b
Up until now this difference was not noticeable, however with enabling support for iOS-like platforms with NativeAOT we started experiencing:
Linq.Expressionstests with NativeAOT on iOS-like platforms: https://github.com/dotnet/runtime/pull/87260/files#diff-4422ce02e9237f4f8e6021526506f87216778e497b33ce676872c1f70e1483c2R54System.Func`3delegate instances take up to 18% of accountable size of a MAUI iOS app with NativeAOT (the cause explained bellow)Additionally, there were several issues reported against MonoAOT, struggling with
Linq.Expressions.Interpreterreported here:Explanation
NativeAOT
The issue was called out by @MichalStrehovsky noting that codepaths in
Linq.Expressionslibrary controlled by:CanCompileToILCanEmitObjectArrayDelegateCanCreateArbitraryDelegatesare not AOT friendly (reported here).
For desktop platforms, NativeAOT fixes this by:
Linq.Expressions.dllruntime/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
Lines 271 to 273 in 2aa0c9b
When it comes to iOS-like platforms, above is not true. When
Linq.Expressionslibrary is built, constant propagation is enabled and control variables get removed during the library build.This further causes above-listed NativeAOT feature switches not to have any effect (fail to trim during app build), causing the AOT compilation to follow unsupported code paths which fail at runtime.
Examples:
Build warnings:
Test crash
runtime/src/tests/nativeaot/SmokeTests/UnitTests/Delegates.cs
Line 54 in 7b7d56f
MonoAOT
As for MonoAOT is concerned, there were several issues reported against the
Linq.Expressions.Interpretersupport. Some issues were fixed, some are still open, but in general following unfriendly AOT codepaths also hurts Mono.Example:
runtime/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs
Line 35 in 2e764d6
runtime/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.cs
Line 86 in 2e764d6
runtime/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/Interpreter/CallInstruction.Generated.cs
Lines 44 to 154 in 2e764d6
includes generating code for a lot of generic delegates and Mono tries its best to support these, but at what cost?
It is true that choosing delegates to implement fast invocation of methods should have better performance, but in case with Mono and delegates over value types, the compiler will generate
GSHAREDVTmethods (generic sharing for value types) which are actually quite slow.On the other hand, NativeAOT does not have generic sharing for value types and generates instead all possible variations (causing the problem reported above 2) with a template MAUI app)
Risk assessment
Pros
Linq.Expressionson iOS-like platforms~30%smaller MAUI template app~2.5%smaller MAUI template app (the difference is way smaller compared to NativeAOT as Mono generates GSHAREDVT for fast invocation)Attempting to JIT compile method (wrapper delegate-invoke)users had to enable mono interpreter in their projects (UseInterpreter=true) to cover-up for missing methods during runtime, which also affects the application sizeCons
GSHAREDVTin these casesProposal
Here is the list of tasks which should resolve all the reported issues around
Linq.Expressionson iOS-like platformsSystem.Linq.Expression.dllbuild for all platforms #88723IsDynamicCodeSupportedfeature to false when using FullAOT macios#18340System.Linq.Expressionstests on iOS-like platforms #89168System.Linq.Expressionsprivate feature switches #89171Thanks to @MichalStrehovsky @vargaz @rolfbjarne for helping to identify these issues.