-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Add a feature flag to not use GVM in Linq Select #109978
base: main
Are you sure you want to change the base?
Add a feature flag to not use GVM in Linq Select #109978
Conversation
A few questions:
|
@MichalStrehovsky could ILCompiler optimize this pattern for value type a bit broadly (or even conservatively for |
1: It is an opt out of the feature if the performance is unacceptable and the developer is unwilling to rewrite their code to avoid Linq Select. If they are unwilling to make the size vs perf tradeoff for this scenario.
|
I'm suggesting we wouldn't gate it on TPM, and instead guard everything relevant with the feature switch, which when set would cause lots of the specializations to be trimmed away. |
@@ -38,6 +38,7 @@ Copyright (c) .NET Foundation. All rights reserved. | |||
<EnableUnsafeBinaryFormatterSerialization Condition="'$(EnableUnsafeBinaryFormatterSerialization)' == ''">false</EnableUnsafeBinaryFormatterSerialization> | |||
<EnableUnsafeUTF7Encoding Condition="'$(EnableUnsafeUTF7Encoding)' == ''">false</EnableUnsafeUTF7Encoding> | |||
<BuiltInComInteropSupport Condition="'$(BuiltInComInteropSupport)' == ''">false</BuiltInComInteropSupport> | |||
<ValueTypeTrimFriendlySelect Condition="'$(ValueTypeTrimFriendlySelect)' == ''">true</ValueTypeTrimFriendlySelect> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have any measurements how this affects PublishTrimmed
? This enables it if PublishTrimmed
is true, I wonder if it should set defaults based on PublishAot
instead.
I only very briefly spot checked what code is in the SpeedOpt files but if it's stuff like this: runtime/src/libraries/System.Linq/src/System/Linq/Range.SpeedOpt.cs Lines 10 to 113 in d0b4ca6
It would introduce untrimmable methods into compilation - these are all virtual/interface methods. Even if we stub them out, this feels like it's going to be a size regression since we need the method bodies, even if they pretty much do nothing. |
Do you have a more concrete idea? The change in this PR modifies behaviors, the resulting behavior is not identical; we take different codepaths and call different methods. It would require some very advanced analysis to do the equivalent change in the compiler. |
Size statistics from rt-sz: Nice savings for Avalonia. The rest probably already learned the lesson to just steer clear of LINQ if perf is of any concern. Size statisticsPull request #109978
|
It'd mean smaller code for nativeaot / trimmed coreclr apps in exchange for possibly slightly larger for mobile. Plus not having to maintain two biuild flavors of the library, and now another variation on top of it. I'm not excited at having both the build constant and the trimming constant, both to optimize for size, both with similar goals, but both doing it differently.
This is on by default if trimming is enabled? I don't see it mentioned anywhere, but these changes still trade offf throughput and allocation for that size benefit. Some of the ones covered by this PR have been there since the earliest days of LINQ. Also, this can have observable behavioral differences, which I thought we tried to avoid as part of trimming by default. |
Is this behavior or just perf difference? I'm not thrilled about introducing perf differences either, but the generic expansion caused by this generic virtual method can lead to actual failure to compile (#102131 mentioned in the top post) because it becomes physically impossible to compile that much code. Brainstorming alternatives, we could also add a perf analyzer that simply flags all uses of LINQ in code that sets |
Looking at the test results, looks like this change is not correct in this shape, the tests are hitting a stack overflow. |
Maybe a perf analyzer wouldn't be the worst idea in general. LINQ expressions is another thing that performs very differently under AOT and we could use something that would steer people away from it better than a line in native AOT docs. |
I wanted to ask if something could be done to fold codegen-identical type instantiations besides removing optimized iterator implementations. This is not the first report here that concerns a problematic interaction of LINQ + enums with NAOT. |
Behavior. It's generally minor, but for example if you have: IList<T> list = ...;
foreach (var item in list.Skip(3).Take(4).Select(...) { ... } that Select will end up producing an enumerable that will use the |
#103951 would help then, but still would not solve #102131 because we cannot even represent that many methods within the compiler (and we need to represent and compile these methods before we find out they have identical method bodies). |
I see that there's already an ifdef that controls some of the behavior here. It looks like that ifdef may already be enabled for some of the mobile platforms and WASM. Is this a known behavioral difference? |
Also, just to provide some clarity for @keegan-caruso, I think we should definitely have a runtime feature-flag for this behavior. Whether or not it's on by default when running AOT can be a separate decision, but there seems to be a wealth of evidence that this is a potentially blocking implementation for some people and we should provide a way to workaround the problem for large apps. |
I mean, I told you about it, so... yes? :) This is an implementation detail; there are two different interface methods that can be used to achieve the same result, and the implementation is using one or the other. There's no guarantee or documentation about which is used. To my knowledge, we've also never heard anyone having an issue based on it using one versus the other. I raise it, though, because historically folks on our team have been adamant that behavior with and without the linker should be identical, that you should be able to test pre-trimming and not have to test again post-trimming. This change puts in place a possibly observable difference that logically goes against that. This is different from how the difference currently manifests, which is that mobile platforms get one behavior and everything else gets another, and we absolutely say you must test on all platforms you care about as there are platform differences that can manifest and result in testing on one platform not guaranteeing correctness on others. |
The nuance here is that the AppContext switch would be set if PublishTrimmed=true is in the project file - this is irrespective of whether we're doing F5 launch without trimming, or running the trimmed result of dotnet publish. So there wouldn't be a behavioral different between trimmed/untrimmed, just a difference between "I have PublishTrimmed=true in the project file" and "I don't have it". It's a bit of weaseling-out from the rules, but we've been forced to do this for various things in the past (e.g. startup hooks don't work with PublishTrimmed and there's no build-time warning about the behavioral difference, but we also disable them for The difference here is that we're not forced into this - this code works, it's just not great size-wise and the size can be prohibitive. |
Yeah, I think the feature switch does mean that we're technically correct (the best kind of correct) about same behavior during JIT and during AOT -- but I'm definitely open to more discussion on the right defaults. |
Adds a feature flag to allow Linq Select to not use a GVM implementation.
Co-authored-by: Michal Strehovský <MichalStrehovsky@users.noreply.github.com>
- Don't set feature flag by default for trimming - Move option to Enumerable - Fix error in OfType.SpeedOpt
2f6ab0c
to
c1531ec
Compare
c1531ec
to
41d7467
Compare
Adds a feature flag to allow Linq Select to not use a GVM implementation.
The compiled size in Native AOT with a value type GVM scales at an order of n**2 relative to types used in the call. This avoids that growth in size at the cost of a slower implementation of chained Linq calls.
A real-world example of where this caused an inability to compile was in this issue: #102131
Adapting this to something a bit contrived, but easy to measure:
The size of the Linq Namespace measured by sizoscope and when compiled with Native Aot: