-
Couldn't load subscription status.
- Fork 5.2k
Description
Description
There is a difference in how System.Linq.Expressions.dll is built for iOS-like and other desktop/mobile platforms, which is observable here:
-
Line 226 in 2aa0c9b
<ILLinkArgs Condition="'$(ILLinkDisableIPConstProp)' == 'true'">$(ILLinkArgs) --disable-opt ipconstprop</ILLinkArgs> -
<ILLinkDisableIPConstProp Condition="'$(TargetPlatformIdentifier)' != 'ios' and '$(TargetPlatformIdentifier)' != 'tvos' and '$(TargetPlatformIdentifier)' != 'maccatalyst'">true</ILLinkDisableIPConstProp> -
Up until now this difference was not noticeable, however with enabling support for iOS-like platforms with NativeAOT we started experiencing:
- Failing
Linq.Expressionstests with NativeAOT on iOS-like platforms: https://github.com/dotnet/runtime/pull/87260/files#diff-4422ce02e9237f4f8e6021526506f87216778e497b33ce676872c1f70e1483c2R54 System.Func`3delegate instances take up to 18% of accountable size of a MAUI iOS app with NativeAOT (the cause explained bellow)
- Failing
-
Additionally, there were several issues reported against MonoAOT, struggling with
Linq.Expressions.Interpreterreported here:- Crash on using Expression.Compile() in a release mode (iOS). #69410
- MAUI iOS Release mode crash: Attempting to JIT compile method '(wrapper delegate-invoke) bool <Module>:invoke_callvirt_bool_SafeHandleZeroOrMinusOneIsInvalid (Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid)' while running in aot-only mode #83212
Explanation
NativeAOT
The issue was called out by @MichalStrehovsky noting that codepaths in Linq.Expressions library controlled by:
CanCompileToILCanEmitObjectArrayDelegateCanCreateArbitraryDelegates
are not AOT friendly (reported here).
For desktop platforms, NativeAOT fixes this by:
- disabling constant propagation when
Linq.Expressions.dll- this prevents above-listed control variables to get trimmed during the build
- introducing feature switches that will substitute the control variables and trim AOT-unfriendly code and provide full AOT experience
- achieved through
runtime/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets
Lines 271 to 273 in 2aa0c9b
<IlcArg Include="--feature:System.Linq.Expressions.CanCompileToIL=false" /> <IlcArg Include="--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false" /> <IlcArg Include="--feature:System.Linq.Expressions.CanCreateArbitraryDelegates=false" />
- achieved through
When it comes to iOS-like platforms, above is not true. When Linq.Expressions library 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:
name(7,8): warning IL2009: System.Linq.Expressions: Could not find method 'System.Boolean get_CanEmitObjectArrayDelegate()' on type 'System.Dynamic.Utils.DelegateHelpers'. [/Users/ivan/repos/runtime-mono-iOS/src/mono/sample/ iOS-NativeAOT/Program.csproj] name(10,8): warning IL2009: System.Linq.Expressions: Could not find method 'System.Boolean get_CanCreateArbitraryDelegates()' on type 'System.Linq.Expressions.Interpreter.CallInstruction'. [/Users/ivan/repos/runtime-mono-iOS/src/ mono/sample/iOS-NativeAOT/Program.csproj] -
Test crash
on a iOS device:TestLinqExpressions.Run(); 2023-06-22 12:51:24.569126+0200 HelloiOS[12307:4919002] Testing LINQ Expressions... 2023-06-22 12:51:24.705363+0200 HelloiOS[12307:4918969] Unhandled Exception: System.PlatformNotSupportedException: Dynamic code generation is not supported on this platform. at System.Reflection.Emit.ReflectionEmitThrower.ThrowPlatformNotSupportedException() + 0x38 at System.Reflection.Emit.DynamicMethod..ctor(String, Type, Type[]) + 0x2c at System.Dynamic.Utils.DelegateHelpers.CreateObjectArrayDelegateRefEmit(Type, Func`2) + 0x450 at System.Dynamic.Utils.DelegateHelpers.CreateObjectArrayDelegate(Type, Func`2) + 0x38 at System.Linq.Expressions.Interpreter.LightLambda.MakeDelegate(Type) + 0xcc at System.Linq.Expressions.Interpreter.LightDelegateCreator.CreateDelegate(IStrongBox[]) + 0x74 at System.Linq.Expressions.Interpreter.LightDelegateCreator.CreateDelegate() + 0x1c at System.Linq.Expressions.Expression`1.Compile() + 0x74 at TestLinqExpressions.Run() + 0x140
MonoAOT
As for MonoAOT is concerned, there were several issues reported against the Linq.Expressions.Interpreter support. Some issues were fixed, some are still open, but in general following unfriendly AOT codepaths also hurts Mono.
Example:
- Following the code path:
Line 35 in 2e764d6
public static CallInstruction Create(MethodInfo info, ParameterInfo[] parameters) Line 86 in 2e764d6
res = FastCreate(info, parameters); - (notice the use of generics over value types)
Lines 44 to 154 in 2e764d6
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);
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 GSHAREDVT methods (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
- Reducing the gap in behaviour between MonoAOT and NativeAOT
- Better support for Linq.Expressions with Mono
- Enabling NativeAOT tests for
Linq.Expressionson iOS-like platforms - Estimated code size improvements:
- NativeAOT
~30%smaller MAUI template app - MonoAOT
~2.5%smaller MAUI template app (the difference is way smaller compared to NativeAOT as Mono generates GSHAREDVT for fast invocation)
- NativeAOT
- Better user experience:
- due to reported
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 size
- due to reported
Cons
- Regression in performance (to be confirmed)
- This has to be measured and evaluated especially because MonoAOT uses
GSHAREDVTin these cases
- This has to be measured and evaluated especially because MonoAOT uses
Proposal
Here is the list of tasks which should resolve all the reported issues around Linq.Expressions on iOS-like platforms
- LambdaExpression.CanCompileToIL should respect IsDynamicCodeSupported #80759
- Respect IsDynamicCodeSupported in more places in Linq.Expressions #88539
- [mono] Add conditional substitution for IsDynamicCodeSupported when targeting ios-like platforms #86971
- [libs][iOS] Unify
System.Linq.Expression.dllbuild for all platforms #88723 - Set the
IsDynamicCodeSupportedfeature to false when using FullAOT macios#18340 - [NativeAOT] Enable
System.Linq.Expressionstests on iOS-like platforms #89168 - [NativeAOT] Refactor
System.Linq.Expressionsprivate feature switches #89171
Thanks to @MichalStrehovsky @vargaz @rolfbjarne for helping to identify these issues.