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

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

Closed
7 tasks done
Tracked by #80905
ivanpovazan opened this issue Jun 22, 2023 · 36 comments
Closed
7 tasks done
Tracked by #80905
Assignees
Milestone

Comments

@ivanpovazan
Copy link
Member

ivanpovazan commented Jun 22, 2023

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.

@ghost
Copy link

ghost commented Jun 22, 2023

Tagging subscribers to 'os-ios': @steveisok, @akoeplinger, @kotlarmilos
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

There is a difference in how 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.

Author: ivanpovazan
Assignees: ivanpovazan
Labels:

area-Codegen-AOT-mono, os-ios, area-NativeAOT-coreclr

Milestone: 8.0.0

@ivanpovazan
Copy link
Member Author

@ivanpovazan
Copy link
Member Author

/cc: @SamMonoRT @eerhardt

@lambdageek
Copy link
Member

Thanks Ivan. Can you or Michal explain the constant propagation issue? Is this some kind of IL trimmer bug - we substitute constants for feature switches all the time. Why is it a problem here?

My understanding is that the existing delegate-based approach will work with Mono AOT now.
What is the cost for NativeAOT to implement universal shared generics?

@filipnavara
Copy link
Member

What is the cost for NativeAOT to implement universal shared generics?

I was asking about this multiple times already, and the answer was always that's it's intentionally not planned. .NET Native used to implement it (with different codegen backend), but any remains of the code were removed over time.

I don't think that Linq.Expressions would have any effect on the decision as the library itself is in archived state.

@ivanpovazan
Copy link
Member Author

Thanks Ivan. Can you or Michal explain the constant propagation issue? Is this some kind of IL trimmer bug - we substitute constants for feature switches all the time. Why is it a problem here?

The problem is that two of the three mentioned control variables are configured as constants:

  1. internal static bool CanEmitObjectArrayDelegate => true;
  2. The exception is:
    public static bool CanCompileToIL => RuntimeFeature.IsDynamicCodeSupported;

From my understanding, when the constant propagation is enabled during Linq.Expressions library build for iOS-like platforms, the first two will get removed and all the code paths they control, as they are constants. Once that happens, the feature switches ILC compiler depends on:

<IlcArg Include="--feature:System.Linq.Expressions.CanCompileToIL=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanCreateArbitraryDelegates=false" />
become obscure, as these variables will not be present in the Linq.Expressions assembly for iOS-like platforms (this can be seen in the warning messages I pasted in the description).

Replacing them with a non-constant RuntimeFeature.IsDynamicCodeSupported would do the trick, as it would prevent them from being removed during the library build. What gets trimmed and AOTed further (during the app build) would then be properly controlled with DynamicCodeSupport feature switch (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported documented here).

@lambdageek
Copy link
Member

  1. internal static bool CanEmitObjectArrayDelegate => true;
  1. The exception is:
    public static bool CanCompileToIL => RuntimeFeature.IsDynamicCodeSupported;

Ah, I see. It's not a trimmer bug.
Would it be better if these three values should be their own top-level feature switches instead of constants in the source. and the SDKs should set them as appropriate if the trimmer isn't running.

@ghost
Copy link

ghost commented Jun 22, 2023

Tagging subscribers to this area: @cston
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

There is a difference in how 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.

Author: ivanpovazan
Assignees: ivanpovazan
Labels:

area-System.Linq.Expressions, os-ios

Milestone: 8.0.0

@MichalStrehovsky
Copy link
Member

What is the cost for NativeAOT to implement universal shared generics?

Universal shared generics wouldn't help with the size here - even on .NET Native, we only used universal shared code for the absolutely most dynamic scenarios (MakeGenericXXX APIs and cases of deep generic recursion). We'd not do it for cases like in Linq.Expressions because universal shared code is slow and the perf characteristics are hard to reason about for users - it was a feature we built only because we needed to be as compatible as possible. For Native AOT we don't try to be as compatible as possible because people always have a choice not to deploy with Native AOT - we wouldn't sacrifice predictable perf for this. .NET Native had this logic in Linq.Expressions ifdeffed out, despite having universal shared code.

Ah, I see. It's not a trimmer bug.

We could also view it as a trimmer bug though - when we're running library level trimming (the way we do when we build the dotnet/runtime repo) and the library contains an embedded substitution manifest for something, maybe ILLinker should be smart enough to know that the substitution could replace the IL at app build time and maybe it shouldn't inline the value it sees in IL right now. The problem is really that the library trimming is generating a broken assembly (the warning we see at app build time warning IL2009: System.Linq.Expressions: Could not find method 'System.Boolean get_CanEmitObjectArrayDelegate()' on type 'System.Dynamic.Utils.DelegateHelpers' is a sign that the library trimming generated a broken assembly - the substitution wants to substitute something we inlined at library build). Cc @dotnet/illink for thoughts.

@vitek-karas
Copy link
Member

maybe ILLinker should be smart enough to know that the substitution could replace the IL at app build time and maybe it shouldn't inline the value it sees in IL right now.

As far as I know we want both behaviors. For example, for IntPtr.Size we want to inline the value at library build time, so that we can remove x86 code from x64 assembly. But in this case we would want to avoid inlining it.

We would have to add some kind of hint to differentiate the two cases. For example we could add support for reading the NoInlining attribute from attribute.xml - and add it in library build to this property (and make sure linker will comply).

@MichalStrehovsky
Copy link
Member

For the IntPtr.Size case, the substitution is unconditional - we can inline at library build time because the value cannot change. I'm thinking about only skipping it for things that have a conditional substitution conditioned on some feature. Would that be still problematic? It feels like that's something we should be never inlining during libs build.

@vitek-karas
Copy link
Member

You're right - but that would probably mean the feature switch definition is wrong in a way - library builds should basically never define any feature switch values - for exactly this reason.

@vitek-karas
Copy link
Member

I see - the problem is that the code hardcodes a return value, the feature switch doesn't kick in at all (and linker doesn't consider substitutions which didn't apply due to feature switch values). Why don't we read this from AppContext just like other feature switches? It could still default to true.
The additional benefit would be that one would get the same implementation of Linq.Expression in an AOT app regardless if it's running on CoreCLR ot NativeAOT runtime (Debug versus Release and so on).

@marek-safar
Copy link
Contributor

Why not to unify CanCreateArbitraryDelegates and CanEmitObjectArrayDelegate to return RuntimeFeature.IsDynamicCodeSupported and delete

<IlcArg Include="--feature:System.Linq.Expressions.CanCompileToIL=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanCreateArbitraryDelegates=false" />
?

@rolfbjarne
Copy link
Member

Why not to unify CanCreateArbitraryDelegates and CanEmitObjectArrayDelegate to return RuntimeFeature.IsDynamicCodeSupported and delete

<IlcArg Include="--feature:System.Linq.Expressions.CanCompileToIL=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanCreateArbitraryDelegates=false" />

?

Doing this for CanCreateArbitraryDelegates is in progress / being tested here: #86758

@MichalStrehovsky
Copy link
Member

CanEmitObjectArrayDelegate is going to crash on mono because it doesn't implement the private CoreLib API the else paths rely on.

We can certainly make it not constant to work around the trimming limitations but there is no point to make this user overridable with appcontext because there's only one right answer per runtime configuration right now.

@vitek-karas
Copy link
Member

because there's only one right answer per runtime configuration right now

I don't understand this.

For example, IsDynamicCodeSupported on CoreCLR is similar - the runtime always supports it, so it should return true. But it is implemented via AppContext. The reason is that if we're running a PublishAot=true app on CoreCLR, we want consistent behavior, so we return IsDynamicCodeSupported == false even though the runtime (CoreCLR) is perfectly capable of supporting.

How is CanEmitObjectArrayDelegate any different. On runtimes where it can be true, it should be read from AppContext so that it can be turned off based on feature switches.

@MichalStrehovsky
Copy link
Member

It relies on a private API that only exists in NativeAOT CoreLib right now.

@eerhardt
Copy link
Member

FYI - a related discussion here is #81803 (comment). It's been on my backlog to get that fixed in .NET 8.

@MichalStrehovsky
Copy link
Member

FYI - a related discussion here is #81803 (comment). It's been on my backlog to get that fixed in .NET 8.

I don't see fixing that as likely to help in this situation - we're unlikely to move the Reflection.Emit part to corelib as part of this fix (due to Linq.Expressions being archived). So we'd still need something to tell whether to call secret CoreLib APIs or do reflection emit. We have multiple options:

  • Disable IPConstprop during library build for iDevices. We already disable it elsewhere (to work around this issue). It likely increases the size of the assembly. Apps that use trimming will not notice because we'll trim with ipconstprop at app build time. Apps that do not trim will notice
    <!--
    Disable constant propagation so that methods referenced from ILLink.Substitutions.xml don't get inlined
    with a wrong value at library build time and the substitution can still be selected at publish time.
    For iOS/tvOS/Catalyst we prefer smaller size by default, so keep constprop enabled to get rid of the expression compiler.
    -->
    <ILLinkDisableIPConstProp Condition="'$(TargetPlatformIdentifier)' != 'ios' and '$(TargetPlatformIdentifier)' != 'tvos' and '$(TargetPlatformIdentifier)' != 'maccatalyst'">true</ILLinkDisableIPConstProp>
  • Rewrite this in a way that ILLinker cannot understand it:
    internal static bool CanEmitObjectArrayDelegate => true;
    (e.g. turn it to string.Empty.Length == 0). The CoreLib secret API lightup code will not be trimmed during a trimmed app build because ILLinker doesn't understand it at library build time, and doesn't understand it at app build time either. Probably fixable by adding a substitution if we care.
  • Fix the ILLinker bug where at library build it does substitutions for things that could look different at app trim time.

@rolfbjarne
Copy link
Member

  • Apps that do not trim will notice

For Apple devices, we don't care about size when not trimming (we never have).

@MichalStrehovsky
Copy link
Member

For Apple devices, we don't care about size when not trimming (we never have).

In that case I would probably recommend removing the special build of Linq.Expressions for iOS-like platforms completely and use the same assembly everywhere. It should be possible to get the same results with feature switches and trimming.

From what I see, we currently do two special things or iOS-like:

  • Enable IPConstprop.
  • Add a substitiution in ILLink.Substitutions.IsInterpreting.LibraryBuild.xml to get rid of the Reflection.Emit backend.

Instead, we could expose the option to remove Ref.Emit backend publicly (doc on how to do that here: https://github.com/dotnet/runtime/blob/e685ff9e0e1a57480df05abda6be135b72c8cbe6/docs/workflow/trimming/feature-switches.md) and have the Xamarin SDK turn the public flag on to remove it when trimming.

@eerhardt
Copy link
Member

Fix the ILLinker bug where at library build it does substitutions for things that could look different at app trim time.

Note that would also allow us to fix #80398.

@ivanpovazan
Copy link
Member Author

@MichalStrehovsky

CanEmitObjectArrayDelegate is going to crash on mono because it doesn't implement the private CoreLib API the else paths rely on.

Is the private CoreLib API related to using bound delegates? (from the discussion here: #78889 (comment))

@MichalStrehovsky
Copy link
Member

@MichalStrehovsky

CanEmitObjectArrayDelegate is going to crash on mono because it doesn't implement the private CoreLib API the else paths rely on.

Is the private CoreLib API related to using bound delegates? (from the discussion here: #78889 (comment))

The private API is to support creating an instance of a "magic" kind of delegate - given the type of the delegate to create delegateType (e.g. Func<int, int>) and an existing delegate in the shape of Func<object?[], object?>, create an instance of delegateType that when invoked, will put all it's argument in an object array and call the other delegate. I don't think it's related to the bound delegate issue - that looks like a more general issue not specific to Linq.Expressions.

@ivanpovazan
Copy link
Member Author

ivanpovazan commented Jun 30, 2023

@vargaz @LeVladIonescu is the functionality from Michal's previous comment:

The private API is to support creating an instance of a "magic" kind of delegate - given the type of the delegate to create delegateType (e.g. Func<int, int>) and an existing delegate in the shape of Func<object?[], object?>, create an instance of delegateType that when invoked, will put all it's argument in an object array and call the other delegate. I don't think it's related to the bound delegate issue - that looks like a more general issue not specific to Linq.Expressions.

covered with: #83329 ?

@vargaz
Copy link
Contributor

vargaz commented Jun 30, 2023

Don't think so, thats a different issue.

@ivanpovazan
Copy link
Member Author

ivanpovazan commented Jul 10, 2023

Considering the risks of changing the ILLink constant propagation to take into account definitions from the substitution file and changing the MonoAOT behaviour to match NativeAOT, would it make sense to instead:

  1. Introduce three new "private" feature switches:

    • CanCreateArbitraryDelegates
    • CanEmitObjectArrayDelegate
    • CanCompileToIL
  2. Change the source code of Linq.Expressions library to utilize 1) where:

    private static bool CanCreateArbitraryDelegates => RuntimeFeature.CanCreateArbitraryDelegates;
    // ...
    private static bool CanEmitObjectArrayDelegate => RuntimeFeature.CanEmitObjectArrayDelegate;
    // ...
    private static bool CanCompileToIL => RuntimeFeature.CanCompileToIL;
  3. Set default values for the new feature switches from 1) and define the linker substitutions accordingly:

    Feature switch MonoAOT NativeAOT
    CanCreateArbitraryDelegates true false
    CanEmitObjectArrayDelegate true false
    CanCompileToIL false false

This way we would:

  • Prevent constant propagation during Linq.Expressions library build
  • Enable NativeAOT to have identical behaviour regarding support for Linq.Expressions on all platforms
  • MonoAOT would have the same behaviour and performance as before
  • Linq.Expressions.dll built for iOS-like platforms would be the same for all supported runtimes

As a follow-up we will investigate and work on enabling MonoAOT to have the support for false values for all three feature switches (and the codepaths false values imply), so the above can be eventually replaced with IsDynamicCodeSupported if it turns out to be feasible and does not introduce any regressions.

Would this make sense?

@eerhardt
Copy link
Member

Disable IPConstprop during library build for iDevices. We already disable it elsewhere (to work around this issue). It likely increases the size of the assembly. Apps that use trimming will not notice because we'll trim with ipconstprop at app build time. Apps that do not trim will notice

What are the cons of following this approach? It would align the "iDevice" build with the rest of the platforms (all other builds have ILLinkDisableIPConstProp=true).

I understand that it will result in a larger untrimmed assembly. But we should measure how much size are we actually talking about. Is it just a few KBs? And my understanding is that all "iDevice" apps will end up being trimmed at the app level anyway - so the resulting app sizes wouldn't be affected.

@MichalStrehovsky
Copy link
Member

But we should measure how much size are we actually talking about. Is it just a few KBs?

It will be a size regression when not trimming because we'll no longer remove Ref.Emit-based expressions at library build time, but per #87924 (comment) it should not be a concern. We'll still trim it at app build time when trimming the app.

@ivanpovazan
Copy link
Member Author

ivanpovazan commented Jul 11, 2023

I have measured the Linq.Expressions.dll size:

  1. with the current state of the main

  2. with disabling constant propagation for library build via the following diff1:

    diff --git a/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj b/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj
    index 0d9a6e1c1c2..12152cb237d 100644
    --- a/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj
    +++ b/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj
    @@ -16,7 +16,7 @@
           with a wrong value at library build time and the substitution can still be selected at publish time.
           For iOS/tvOS/Catalyst we prefer smaller size by default, so keep constprop enabled to get rid of the expression compiler.
         -->
    -    <ILLinkDisableIPConstProp Condition="'$(TargetPlatformIdentifier)' != 'ios' and '$(TargetPlatformIdentifier)' != 'tvos' and '$(TargetPlatformIdentifier)' != 'maccatalyst'">true</ILLinkDisableIPConstProp>
    +    <ILLinkDisableIPConstProp>true</ILLinkDisableIPConstProp>
       </PropertyGroup>
       <ItemGroup>
         <ILLinkSubstitutionsXmls Include="$(ILLinkDirectory)ILLink.Substitutions.xml" />
  3. with disabling constant propagation for library build and disabling removal of Ref.Emit-based expressions via the following diff2:

    diff --git a/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj b/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj
    index 0d9a6e1c1c2..6bc375fa637 100644
    --- a/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj
    +++ b/src/libraries/System.Linq.Expressions/src/System.Linq.Expressions.csproj
    @@ -10,13 +10,13 @@
       <PropertyGroup>
         <TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
         <IsInterpreting Condition="'$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos' or '$(TargetPlatformIdentifier)' == 'maccatalyst'">true</IsInterpreting>
    -    <ILLinkSubstitutionsLibraryBuildXml Condition="'$(IsInterpreting)' == 'true'">ILLink\ILLink.Substitutions.IsInterpreting.LibraryBuild.xml</ILLinkSubstitutionsLibraryBuildXml>
    +    <!-- <ILLinkSubstitutionsLibraryBuildXml Condition="'$(IsInterpreting)' == 'true'">ILLink\ILLink.Substitutions.IsInterpreting.LibraryBuild.xml</ILLinkSubstitutionsLibraryBuildXml> -->
         <!--
           Disable constant propagation so that methods referenced from ILLink.Substitutions.xml don't get inlined
           with a wrong value at library build time and the substitution can still be selected at publish time.
           For iOS/tvOS/Catalyst we prefer smaller size by default, so keep constprop enabled to get rid of the expression compiler.
         -->
    -    <ILLinkDisableIPConstProp Condition="'$(TargetPlatformIdentifier)' != 'ios' and '$(TargetPlatformIdentifier)' != 'tvos' and '$(TargetPlatformIdentifier)' != 'maccatalyst'">true</ILLinkDisableIPConstProp>
    +    <ILLinkDisableIPConstProp>true</ILLinkDisableIPConstProp>
       </PropertyGroup>
       <ItemGroup>
         <ILLinkSubstitutionsXmls Include="$(ILLinkDirectory)ILLink.Substitutions.xml" />

Results

System.Linq.Expressions.dll Size (b) diff (b) diff (%)
main 448512 NaN NaN
main+diff1 449024 512 0,11%
main+diff2 561664 113152 25,09%

NOTE: The diffs presented above are just for performing measurements


  1. Should we try to open a PR investigating if this will have any other implication?
  2. It is important to note that if we take the diff2 approach it will become a size regression since Set the IsDynamicCodeSupported feature to false when using FullAOT xamarin/xamarin-macios#18340 is not yet implemented, and IsDynamicCodeSupported is not set for Mono, meaning further that the assembly will not get trimmed during publish.

@eerhardt
Copy link
Member

I think diff1 seems like a good approach to try (along with updating the comment directly above the changed line). That is inline with what I was thinking.

@ivanpovazan
Copy link
Member Author

@MichalStrehovsky as #88539 got merged in, should we open an issue to change how the private NativeAOT feature switches are handled:

<IlcArg Include="--feature:System.Linq.Expressions.CanCompileToIL=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false" />
<IlcArg Include="--feature:System.Linq.Expressions.CanCreateArbitraryDelegates=false" />

There are two problems with this:

  1. [bigger-issue] In Xamarin+NativeAOT build scenario even when [libs][iOS] Unify System.Linq.Expression.dll build for all platforms #88723 gets merged in, the library will still not be in the right shape once it reaches ILC during publish. The reason for that is that ILLink is being run before ILC, so its constant propagation during app deployment will again remove the conditional variables that NativeAOT expects to find in the assembly.
    • To solve that, can these feature switches be somehow exposed via project property in NativeAOT build .targets so that Xamarin SDK can pass them properly to ILLink. It would also help if these are promoted into actual feature switches handled by SDK, then these workarounds would not be necessary.
  2. [smaller-issue] The feature switches for CanCompileToIL and CanCreateArbitraryDelegates have become redundant as these are now controlled via IsDynamicCodeSupported and should/could be removed

@MichalStrehovsky
Copy link
Member

[smaller-issue] The feature switches for CanCompileToIL and CanCreateArbitraryDelegates have become redundant as these are now controlled via IsDynamicCodeSupported and should/could be removed

Yep, looks like these --feature lines can now be deleted.

[bigger-issue] In Xamarin+NativeAOT build scenario

I think this one would be fixable by replacing the --feature line with something along the lines of:

<ItemGroup>
    <RuntimeHostConfigurationOption Include="System.Linq.Expressions.CanEmitObjectArrayDelegate"
                                    Value="false"
                                    Trim="true" />
</ItemGroup>

I think if we put this into the NativeAOT targets, it would affect both ILLink and NativeAOT. I wouldn't pull this into a feature switch one can control externally - there's only one correct value for this and there's little reason to make it configurable.

If this works, could you submit a PR?

@ivanpovazan
Copy link
Member Author

Sure, I will try it out and ping you.

@ivanpovazan ivanpovazan changed the title Fix how Linq.Expressions.dll is built for iOS-like platforms Fix how System.Linq.Expressions.dll is supported on iOS-like platforms Jul 19, 2023
@ivanpovazan
Copy link
Member Author

I am closing this issue as completed as all the identified work has been done and adequate tracking issues were created.

@dotnet dotnet locked as resolved and limited conversation to collaborators Sep 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

10 participants