Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add a feature flag to not use GVM in Linq Select#109978

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Conversation

@keegan-caruso
Copy link
Contributor

@keegan-carusokeegan-caruso commentedNov 19, 2024
edited
Loading

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:

GetEnumValue<GeneratedEnum0>();GetEnumValue<GeneratedEnumN>();staticT?GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)]T>()whereT:struct,Enum{varrawValue="foo,bar";if(string.IsNullOrEmpty(rawValue))returnnull;vartype=typeof(T);if(type.GetCustomAttributes<FlagsAttribute>().Any()){return(T)(object)rawValue!.Split(',').Select(x=>Enum.TryParse<T>(x,true,outvarresult)?result:(T?)null).Where(x=>!x.Equals(null)).Select(x=>(int)(object)x!).Sum();}elsereturnEnum.TryParse<T>(rawValue,true,outvarresult)?result:null;}classFlagsAttribute:Attribute{}enumGeneratedEnum0{};…enumGeneratedEnumN{};

The size of the Linq Namespace measured by sizoscope and when compiled with Native Aot:

NSize NET 9.0Size with feature flag enabled
101016.6 kb77.7 kb
255.2 mb190.4 kb
5019.7 mb378.3 kb
7543.4 mb566.2 kb
10076.3 mb754.1 kb
1000Failed to compile7475.2 kb

am11 reacted with eyes emoji
@ghostghost added the area-System.Linq labelNov 19, 2024
@dotnet-policy-servicedotnet-policy-servicebot added the community-contributionIndicates that the PR has been added by a community member labelNov 19, 2024
@stephentoub
Copy link
Member

A few questions:

  1. Who do we expect to set this to false?
  2. This is specific to Select because it's the only one of the virtuals on the base Iterator type that's generic? There have separately been concerns about the size impact of all of these specializations, GVM or not.
  3. We already have the OptimizeForSize build constant:https://github.com/dotnet/runtime/pull/109978/files#diff-bcb77d7db7721bb5508d93dc432d9a40d920e6b248c98c9ad14a3640bbe6fa2bR11. I'm not a huge fan of having yet another flavor. Could we get rid of that existing one, so that there's just one build of LINQ, and then use a switch like this to control things at publish / execution time?

@am11
Copy link
Member

The size of the Linq Namespace measured by sizoscope and when compiled with Native Aot:

NSize .NET 9.0Size with Feature Flag Enabled
101016.6 kb77.7 kb
255.2 mb190.4 kb
5019.7 mb378.3 kb
7543.4 mb566.2 kb
10076.3 mb754.1 kb
1000Failed to compile7475.2 kb

@MichalStrehovsky could ILCompiler optimize this pattern for value type a bit broadly (or even conservatively for.Select() verbatim) without introducing the feature switch?

@keegan-caruso
Copy link
ContributorAuthor

A few questions:

  1. Who do we expect to set this to false?
  2. This is specific to Select because it's the only one of the virtuals on the base Iterator type that's generic? There have separately been concerns about the size impact of all of these specializations, GVM or not.
  3. We already have the OptimizeForSize build constant:https://github.com/dotnet/runtime/pull/109978/files#diff-bcb77d7db7721bb5508d93dc432d9a40d920e6b248c98c9ad14a3640bbe6fa2bR11. I'm not a huge fan of having yet another flavor. Could we get rid of that existing one, so that there's just one build of LINQ, and then use a switch like this to control things at publish / execution time?

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.

  1. Yes, only virtual that is also a generic method. I updated the description to add an example of the size difference we see with this change; it can be significant.

  2. I guess it is a question if we can conditionally set the value of the feature flag from TargetPlatformIdentifier and if it should be controllable at publish time.

@stephentoub
Copy link
Member

I guess it is a question if we can conditionally set the value of the feature flag from TargetPlatformIdentifier and if it should be controllable at publish time.

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.

<EnableUnsafeBinaryFormatterSerializationCondition="'$(EnableUnsafeBinaryFormatterSerialization)' == ''">false</EnableUnsafeBinaryFormatterSerialization>
<EnableUnsafeUTF7EncodingCondition="'$(EnableUnsafeUTF7Encoding)' == ''">false</EnableUnsafeUTF7Encoding>
<BuiltInComInteropSupportCondition="'$(BuiltInComInteropSupport)' == ''">false</BuiltInComInteropSupport>
<ValueTypeTrimFriendlySelectCondition="'$(ValueTypeTrimFriendlySelect)' == ''">true</ValueTypeTrimFriendlySelect>

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 affectsPublishTrimmed? This enables it ifPublishTrimmed is true, I wonder if it should set defaults based onPublishAot instead.

@MichalStrehovsky
Copy link
Member

I guess it is a question if we can conditionally set the value of the feature flag from TargetPlatformIdentifier and if it should be controllable at publish time.

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.

I only very briefly spot checked what code is in the SpeedOpt files but if it's stuff like this:

privatesealedpartialclassRangeIterator:IList<int>,IReadOnlyList<int>
{
publicoverrideIEnumerable<TResult>Select<TResult>(Func<int,TResult>selector)
{
returnnewRangeSelectIterator<TResult>(_start,_end,selector);
}
publicoverrideint[]ToArray()
{
intstart=_start;
int[]array=newint[_end-start];
FillIncrementing(array,start);
returnarray;
}
publicoverrideList<int>ToList()
{
(intstart,intend)=(_start,_end);
List<int>list=newList<int>(end-start);
FillIncrementing(SetCountAndGetSpan(list,end-start),start);
returnlist;
}
publicvoidCopyTo(int[]array,intarrayIndex)=>
FillIncrementing(array.AsSpan(arrayIndex,_end-_start),_start);
publicoverrideintGetCount(boolonlyIfCheap)=>_end-_start;
publicintCount=>_end-_start;
publicoverrideIterator<int>?Skip(intcount)
{
if(count>=_end-_start)
{
returnnull;
}
returnnewRangeIterator(_start+count,_end-_start-count);
}
publicoverrideIterator<int>Take(intcount)
{
intcurCount=_end-_start;
if(count>=curCount)
{
returnthis;
}
returnnewRangeIterator(_start,count);
}
publicoverrideintTryGetElementAt(intindex,outboolfound)
{
if((uint)index<(uint)(_end-_start))
{
found=true;
return_start+index;
}
found=false;
return0;
}
publicoverrideintTryGetFirst(outboolfound)
{
found=true;
return_start;
}
publicoverrideintTryGetLast(outboolfound)
{
found=true;
return_end-1;
}
publicboolContains(intitem)=>
(uint)(item-_start)<(uint)(_end-_start);
publicintIndexOf(intitem)=>
Contains(item)?item-_start:-1;
publicintthis[intindex]
{
get
{
if((uint)index>=(uint)(_end-_start))
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index);
}
return_start+index;
}
set=>ThrowHelper.ThrowNotSupportedException();
}
publicboolIsReadOnly=>true;
voidICollection<int>.Add(intitem)=>ThrowHelper.ThrowNotSupportedException();
voidICollection<int>.Clear()=>ThrowHelper.ThrowNotSupportedException();
voidIList<int>.Insert(intindex,intitem)=>ThrowHelper.ThrowNotSupportedException();
boolICollection<int>.Remove(intitem)=>ThrowHelper.ThrowNotSupportedException_Boolean();
voidIList<int>.RemoveAt(intindex)=>ThrowHelper.ThrowNotSupportedException();
}
}

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.

@MichalStrehovsky
Copy link
Member

@MichalStrehovsky could ILCompiler optimize this pattern for value type a bit broadly (or even conservatively for.Select() verbatim) without introducing the feature switch?

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.

@MichalStrehovsky
Copy link
Member

Size statistics fromrt-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 statistics

Pull request#109978

ProjectSize beforeSize afterDifference
avalonia.app-linux 22175368 20849944-1325424
avalonia.app-windows 19117568 18252800-864768
hello-linux 1352352 13523520
hello-minimal-linux 1081896 10818960
hello-minimal-windows 858112 8581120
hello-windows 1103360 11033600
kestrel-minimal-linux 5474240 54742400
kestrel-minimal-windows 4908544 4909056512
reflection-linux 2063440 20634400
reflection-windows 1750016 17500160
webapiaot-linux 10120480 101204800
webapiaot-windows 9157632 9158144512
winrt-component-full-windows 5602304 5600768-1536
winrt-component-minimal-windows 1747456 17474560

@stephentoub
Copy link
Member

stephentoub commentedNov 20, 2024
edited
Loading

this feels like it's going to be a size regression since we need the method bodies, even if they pretty much do nothing.

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 enables it if PublishTrimmed is true, I wonder if it should set defaults based on PublishAot instead.

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.

@MichalStrehovsky
Copy link
Member

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 setsIsAotCompatible as a perf problem so that people know to stay away from it. It's basically how we solved these issues in the past, but without the analyzer, just deleting LINQ use in e.g. ASP.NET.

@MichalStrehovsky
Copy link
Member

Looking at the test results, looks like this change is not correct in this shape, the tests are hitting a stack overflow.

@MichalStrehovsky
Copy link
Member

Brainstorming alternatives, we could also add a perf analyzer that simply flags all uses of LINQ in code that setsIsAotCompatible as a perf problem so that people know to stay away from it. It's basically how we solved these issues in the past, but without the analyzer, just deleting LINQ use in e.g. ASP.NET.

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.

keegan-caruso reacted with eyes emoji

@neon-sunset
Copy link
Contributor

neon-sunset commentedNov 20, 2024
edited
Loading

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.

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.

IlcFoldIdenticalMethodBodies=true appears to remove about 260 KB from LINQ namespace for N = 10.

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.

@am11am11 added the size-reductionIssues impacting final app size primary for size sensitive workloads labelNov 20, 2024
@stephentoub
Copy link
Member

stephentoub commentedNov 20, 2024
edited
Loading

Is this behavior or just perf difference?

Behavior. It's generally minor, but for example if you have:

IList<T>list= ...;foreach(variteminlist.Skip(3).Take(4).Select(...){ ...}

that Select will end up producing an enumerable that will use theIList<T>'s indexer, but with this PR, it would end up enumerating it via GetEnumerator/MoveNext/Current.

keegan-caruso reacted with thumbs up emojiMichalStrehovsky and am11 reacted with eyes emoji

@MichalStrehovsky
Copy link
Member

IlcFoldIdenticalMethodBodies=true appears to remove about 260 KB from LINQ namespace for N = 10.

#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).

@agocke
Copy link
Member

Behavior. It's generally minor, but for example if you have:

IList<T>list= ...;foreach(variteminlist.Skip(3).Take(4).Select(...){ ...}

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?

@agocke
Copy link
Member

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.

@stephentoub
Copy link
Member

stephentoub commentedNov 22, 2024
edited
Loading

Is this a known behavioral difference?

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.

@MichalStrehovsky
Copy link
Member

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.

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 fordotnet build, not justpublish, so it doesn't break the rule).

The difference here is that we're not forced into this - this codeworks, it's just not great size-wise and the size can be prohibitive.

@agocke
Copy link
Member

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.

@keegan-carusokeegan-caruso changed the titleDon't use a GVM in Linq Select with NAOT by defaultAdd a feature flag to not use GVM in Linq SelectNov 27, 2024
Adds a feature flag to allow Linq Select to not usea 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
@keegan-carusokeegan-carusoforce-pushed thenativeaot-remove-gvm-select branch from2f6ab0c toc1531ecCompareNovember 27, 2024 19:49
@MichalStrehovsky
Copy link
Member

Unless there's any objections, I'd like to fold this new codepath under the same feature switch that we introduced in#111743. The latest commit does that. This can all just be "size-optimized" without going into details of the mechanism.

Some EXE size numbers for the N=11 sample in the top post:

bytes
main3,287,552
main (UseSizeOptimizedLinq)2,689,024
PR (UseSizeOptimizedLinq)1,792,512

Additional numbers are in rt-sz measurements atMichalStrehovsky/rt-sz#106 (comment). That one measures UseSizeOptimizedLinq for both baseline size (before) and PR size (after).

So this is making UseSizeOptimizedLinq even more size optimized.

One of the reasons I'd like to fold this into the existing switch is that there's actually interactions between the existing size optimization and this size optimization - as you can see in05d43cf I'm deleting a test that was testing a difference in behavior of the new switch added here. Turns out the difference in behavior only exists if the new switch (added in this PR) is enabled, but the existing switch is not enabled. If both are enabled (or both are disabled) the difference in behavior doesn't exist.

stephentoub, am11, and keegan-caruso reacted with thumbs up emoji

@MichalStrehovskyMichalStrehovsky merged commit481eab6 intodotnet:mainFeb 6, 2025
86 checks passed
grendello added a commit to grendello/runtime that referenced this pull requestFeb 6, 2025
* main: (23 commits)  add important remarks to NrbfDecoder (dotnet#111286)  docs: fix spelling grammar and missing words in clr-code-guide.md (dotnet#112222)  Consider type declaration order in MethodImpls (dotnet#111998)  Add a feature flag to not use GVM in Linq Select (dotnet#109978)  [cDAC] Implement ISOSDacInterface::GetMethodDescPtrFromIp (dotnet#110755)  Restructure JSImport/JSExport generators to share more code and utilize more Microsoft.Interop.SourceGeneration shared code (dotnet#107769)  Add more detailed explanations to control-flow RegexOpcode values (dotnet#112170)  Add repo-specific condition to labeling workflows (dotnet#112169)  Fix bad assembly when a nested exported type is marked via link.xml (dotnet#107945)  Make `CalculateAssemblyAction` virtual. (dotnet#112154)  JIT: Enable reusing profile-aware DFS trees between phases (dotnet#112198)  Add support for LDAPTLS_CACERTDIR \ TrustedCertificateDirectory (dotnet#111877)  JIT: Support custom `ClassLayout` instances with GC pointers in them (dotnet#112064)  Factor positive lookaheads better into find optimizations (dotnet#112107)  Add ImmutableCollectionsMarshal.AsMemory (dotnet#112177)  [mono] ILStrip write directly to the output filestream (dotnet#112142)  Allow the NativeAOT runtime pack to be specified as the ILC runtime package (dotnet#111876)  JIT: some reworking for conditional escape analysis (dotnet#112194)  Replace HELPER_METHOD_FRAME with DynamicHelperFrame in patchpoints (dotnet#112025)  [Android] Decouple runtime initialization and entry point execution for Android sample (dotnet#111742)  ...
@keegan-carusokeegan-caruso deleted the nativeaot-remove-gvm-select branchFebruary 6, 2025 21:30
@github-actionsgithub-actionsbot locked and limited conversation to collaboratorsMar 9, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.

Reviewers

@sbomersbomersbomer left review comments

@MichalStrehovskyMichalStrehovskyMichalStrehovsky left review comments

@stephentoubstephentoubstephentoub approved these changes

Labels

area-System.Linqcommunity-contributionIndicates that the PR has been added by a community membersize-reductionIssues impacting final app size primary for size sensitive workloads

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

7 participants

@keegan-caruso@stephentoub@am11@MichalStrehovsky@neon-sunset@agocke@sbomer

[8]ページ先頭

©2009-2025 Movatter.jp