- 
                Notifications
    You must be signed in to change notification settings 
- 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:
- CanCompileToIL
- CanEmitObjectArrayDelegate
- CanCreateArbitraryDelegates
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.