Skip to content

Fix how System.Linq.Expressions.dll is supported on iOS-like platforms #87924

@ivanpovazan

Description

@ivanpovazan

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:

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

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:

    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:
    • public static CallInstruction Create(MethodInfo info, ParameterInfo[] parameters)
    • 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);
      (notice the use of generics over value types)

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.Expressions on 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)
  • 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

Cons

  • Regression in performance (to be confirmed)
    • This has to be measured and evaluated especially because MonoAOT uses GSHAREDVT in these cases

Proposal

Here is the list of tasks which should resolve all the reported issues around Linq.Expressions on iOS-like platforms


Thanks to @MichalStrehovsky @vargaz @rolfbjarne for helping to identify these issues.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions