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

Replace OPTIMIZE_FOR_SIZE with feature switch #111743

Merged
merged 10 commits into from
Jan 30, 2025

Conversation

MichalStrehovsky
Copy link
Member

When I was looking at this last time, I noticed the virtual method overrides and though it's not solvable with feature switches. But looks like the virtual methods are not actually called outside !OPTIMIZE_FOR_SIZE code so we might still be able to trim things correctly if it's a feature switch (we can remove unused virtual methods).

The only problem is maintainability - how do we remember that we should only call the virtuals under a !IsSizeOptimized check. The ifdef was a nice forcing function enforced by the compiler.

Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-linq
See info in area-owners.md if you want to be subscribed.

@MichalStrehovsky
Copy link
Member Author

Oh nice:

Size statistics

Pull request #111743

Project Size before Size after Difference
avalonia.app-windows 19096064 18085888 -1010176
hello-minimal-windows 857600 857600 0
hello-windows 1102848 1102848 0
kestrel-minimal-windows 4898816 4804096 -94720
reflection-windows 1748992 1748992 0
webapiaot-windows 9154048 8944640 -209408
winrt-component-full-windows 5728768 5728256 -512
winrt-component-minimal-windows 1760768 1760768 0

@stephentoub
Copy link
Member

I would love it if we could remove the compilation constant, have a single build, and just achieve desired size reduction in certain environments via the feature switch.

@MichalStrehovsky
Copy link
Member Author

I would love it if we could remove the compilation constant, have a single build, and just achieve desired size reduction in certain environments via the feature switch.

Do you have any opinion on how to achieve the "The only problem is maintainability - how do we remember that we should only call the virtuals under a !IsSizeOptimized check." part? I was thinking about leaving the .SpeedOpt.cs files around to keep the virtuals separated at least in files. It won't help much, but it's at least something.

@MichalStrehovsky
Copy link
Member Author

Do you have any opinion on how to achieve the "The only problem is maintainability - how do we remember that we should only call the virtuals under a !IsSizeOptimized check."

Hmm, I guess this is the spot where we could use the generalized feature guards. @sbomer - did we implement that part? (I'm not able to find it right now, so my guess is no, but wanted to check.)

@sbomer
Copy link
Member

sbomer commented Jan 23, 2025

@sbomer - did we implement that part? (I'm not able to find it right now, so my guess is no, but wanted to check.)

Nope, only for RequiresUnreferencedCode/RequiresDynamicCode/RequiresAssemblyFiles.

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky MichalStrehovsky marked this pull request as ready for review January 24, 2025 13:21
@MichalStrehovsky MichalStrehovsky changed the title See if we can replace OPTIMIZE_FOR_SIZE with feature switch Replace OPTIMIZE_FOR_SIZE with feature switch Jan 24, 2025
@MichalStrehovsky
Copy link
Member Author

I marked this ready for review. Not all tests pass. It turns out we cannot fully replace this with a feature switch.

This part is the problem:

private sealed partial class RangeIterator : IList<int>, IReadOnlyList<int>

We cannot express "this class only implements the interface if a feature switch is defined". And this is a popular interface.

I'll try to measure how much that matters in a separate PR (#111794). The extra interface will only really matter for AOT.

We can adjusts the test to expect this to pass, that's the least problem.

@ivanpovazan is it easy enough to measure the size impact of this on iOS? This is replacing the special build of System.Linq with a common assembly that is same across platforms. There's now a feature switch to enable size savings.

To enable the feature switch, one currently needs to add:

  <ItemGroup>
    <RuntimeHostConfigurationOption Include="System.Linq.Enumerable.IsSizeOptimized"
                                        Value="true"
                                        Trim="true" />
  </ItemGroup>

The expectation is that we would introduce a property to wrap it, and default the property on iOS/Android/etc. to true.

@stephentoub
Copy link
Member

Do you have any opinion on how to achieve the "The only problem is maintainability - how do we remember that we should only call the virtuals under a !IsSizeOptimized check." part? I was thinking about leaving the .SpeedOpt.cs files around to keep the virtuals separated at least in files. It won't help much, but it's at least something.

We could have an analyzer just for that library. Better would be to build out the unimplemented parts of feature guards and then use it here. We could explore having a separate build of the library that we test but don't ship and that implements the guarded functionality to throw. But probably most simply, we could rely on size regression tests to point out any place we make a mistake and then fix it, as we do with other performance-focused issues.

@ivanpovazan
Copy link
Member

@ivanpovazan is it easy enough to measure the size impact of this on iOS? This is replacing the special build of System.Linq with a common assembly that is same across platforms. There's now a feature switch to enable size savings.

Unfortunately it is tricky. Especially since we would like to measure the size impact on MAUI iOS apps and net10 builds are currently unstable.

Would it be possible to apply these changes on top of net9 branch (not sure if there are any dependencies from the point we branched to net10) ? I could then use the official net9 workloads with local build of your changes to see the impact.

@MichalStrehovsky
Copy link
Member Author

Would it be possible to apply these changes on top of net9 branch (not sure if there are any dependencies from the point we branched to net10) ? I could then use the official net9 workloads with local build of your changes to see the impact.

Yeah, this applies pretty cleanly to 9.0 and I don't think there's prerequisites. Here's a branch: https://github.com/MichalStrehovsky/runtime/pull/new/sizeoptswitch-net9

The important bit when measuring is that we need to set the feature switch, or this will be a regression for sure.

Here are the numbers for the couple test apps with native AOT using OPTIMIZE_FOR_SIZE. It is better with the ifdef, but not by much. I'd expect a similar picture for Mono AOT.

Size statistics

Pull request #111794

Project Size before Size after Difference
avalonia.app-windows 19096064 18072576 -1023488
hello-minimal-windows 857600 857600 0
hello-windows 1102848 1102848 0
kestrel-minimal-windows 4898816 4804096 -94720
reflection-windows 1748992 1748992 0
webapiaot-windows 9154048 8943104 -210944
winrt-component-full-windows 5728256 5728256 0
winrt-component-minimal-windows 1760256 1760256 0

@MichalStrehovsky
Copy link
Member Author

Here are the numbers for the couple test apps with native AOT using OPTIMIZE_FOR_SIZE. It is better with the ifdef, but not by much.

(Need to compare with the table in my previous comment, not the "size before". The "size before" is main branch.)

@MichalStrehovsky
Copy link
Member Author

But probably most simply, we could rely on size regression tests to point out any place we make a mistake and then fix it, as we do with other performance-focused issues.

Yeah, I guess that would work. It will require some skill to root cause but if enough people remember that some virtuals are special (and we should probably keep them special in the .SpeedOpt.cs files), it should be workable.

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@MichalStrehovsky
Copy link
Member Author

@stephentoub @dotnet/ilc-contrib this is ready for review. I tested IL-level trimming with a test app and I'm getting the same results as with native AOT (no new methods seem to be added compared to just using the ifdef, modulo the methods that implement the popular interfaces mentioned in #111743 (comment)).

We can react if there's something unexpected on iDevices later. I don't expect it based on the above testing that I am equipped to do.

I'm also planning to introduce a property to control this and default it to enabled for places that used the ifdef until now:

(@ivanpovazan are these the right places? Did I forget any? Anyone else we should loop in and might know?)

Before I add those, I'd like to have a consensus on the property name. Does anyone have suggestions? Or any objections to UseSizeOptimizedLinq? (Cc @eerhardt for ideas too)

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the MSBuild property, I think UseSizeOptimizedLinq sounds good and fits our existing naming patterns.

@ivanpovazan
Copy link
Member

ivanpovazan commented Jan 28, 2025

Yeah, this applies pretty cleanly to 9.0 and I don't think there's prerequisites. Here's a branch: https://github.com/MichalStrehovsky/runtime/pull/new/sizeoptswitch-net9

Thank you, here are the results for:

  • NativeAOT with MAUI iOS template app:
MAUI ios template app 9.0.100 release local build of
sizeoptswitch-net9
local build of sizeoptswitch-net9 with System.Linq.Enumerable.IsSizeOptimized enabled local build of release/9.0
zipped bundled (MB) 5,85 5,99 5,87 5,87
  • Mono with MAUI iOS template app:
MAUI ios template app 9.0.100 release local build of
sizeoptswitch-net9
local build of sizeoptswitch-net9 with System.Linq.Enumerable.IsSizeOptimized enabled local build of release/9.0
zipped bundled (MB) 15,45 15,83 15,61 15,61

@MichalStrehovsky It seems the optimization only cuts-off the regression introduced, is that expected?

PS I will measure Mono as a follow-up.

@ivanpovazan
Copy link
Member

ivanpovazan commented Jan 28, 2025

@MichalStrehovsky:

(@ivanpovazan are these the right places? Did I forget any? Anyone else we should loop in and might know?)

Regarding: iOS/Android

  • The places you linked for setting the feature switch are correct.

Regarding: wasm/wasi

/cc: @pavelsavara @lewing

@MichalStrehovsky
Copy link
Member Author

@MichalStrehovsky It seems the optimization only cuts-off the regression introduced, is that expected?

Yep, this just replaces ifdef with a configurable feature switch. There were valid questions in #109978 (that introduces a feature switch for LINQ) why we should have both ifdefs and feature switches. So this is a preparatory change for that.

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-nativeaot-outerloop

@MichalStrehovsky
Copy link
Member Author

/azp run runtime-extra-platforms

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

1 similar comment
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

<!-- DesignTimeBuild requires all the TargetFramework Derived Properties to not be present in the first property group. -->
<PropertyGroup>
<TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
<OptimizeForSize Condition="'$(TargetPlatformIdentifier)' == 'browser' or '$(TargetPlatformIdentifier)' == 'android' or '$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos'">true</OptimizeForSize>
Copy link
Member

@pavelsavara pavelsavara Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptimizeForSize was set true for some targets but it's not anymore.
WasmFeatures.props set the default for wasm workload, but default runtime pack also needs to be optimized for size - when people don't use workload in Blazor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptimizeForSize was set true for some targets but it's not anymore.

Yes, that is the purpose of this PR. To only have one build of this library (#111743 (comment))

WasmFeatures.props set the default for wasm workload, but default runtime pack also needs to be optimized for size - when people don't use workload in Blazor.

Could you please point me to which .targets we need to update? Is it the Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets linked from #111743 (comment)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(To be clear, the OptimizeForSize property controls whether we build System.Linq assembly in a not-fully-compatible manner (see the tests I'm touching where it behaves differently) that has better size characteristics. This PR deletes that build and changes it to always build the compatible Linq implementation. It introduces a feature switch that allows switching to the not-fully-compatible mode. The default for the feature switch is disabled. It can be enabled where the tradeoff is worth it (I have PRs out enabling it in places, linked above). The advantage is that now if a customer runs into the incompatibility, they can just set a property and unblock themselves.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Folded the BlazorWebAssembly.targets change into dotnet/sdk#46375.

WasmFeatures.props change is already part of the PR here.

MichalStrehovsky added a commit to dotnet/sdk that referenced this pull request Jan 29, 2025
@MichalStrehovsky MichalStrehovsky merged commit adf123c into dotnet:main Jan 30, 2025
149 of 157 checks passed
@MichalStrehovsky MichalStrehovsky deleted the sizeoptswitch branch January 30, 2025 08:36
grendello added a commit to grendello/runtime that referenced this pull request Jan 30, 2025
* main: (31 commits)
  More native AOT Pri-1 test tree bring up (dotnet#111994)
  Fix BigInteger outerloop test (dotnet#111841)
  JIT: Run 3-opt once across all regions (dotnet#111989)
  JIT: Check for profile consistency throughout JIT backend (dotnet#111684)
  [JIT] Add legacy extended EVEX encoding and EVEX.ND/NF feature to x64 emitter backend (dotnet#108796)
  [iOS][globalization] Fix IndexOf on empty strings on iOS to return -1 (dotnet#111898)
  System.Speech: Use intellisense xml from dotnet-api-docs (dotnet#111983)
  [mono][mini] Disable inlining if we encounter class initialization failure (dotnet#111754)
  [main] Update dependencies from dotnet/roslyn (dotnet#111946)
  Update dependencies from https://github.com/dotnet/arcade build 20250129.2 (dotnet#111996)
  Try changing the ICustomQueryInterface implementation to always return NotHandled instead of Failed to defer back to the ComWrappers impl. (dotnet#111978)
  Combined dependency update (dotnet#111852)
  Replace OPTIMIZE_FOR_SIZE with feature switch (dotnet#111743)
  Fix failed assertion 'FPbased == FPbased2' (dotnet#111787)
  Add remark to `ConditionalSelect` (dotnet#111945)
  JIT: fix try region cloning when try is nested in a handler (dotnet#111975)
  Use IRootFunctions in Tensor.StdDev (dotnet#110641)
  Remove zlib dependencies from Docker containers (dotnet#111939)
  Avoid `Unsafe.As` for `Memory<T>` and `ReadOnlyMemory<T>` conversion (dotnet#111023)
  Cleanup membarrier portability (dotnet#111943)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants