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 LINQ handling of iterator.Take(...).Last(...) #112680

Merged
merged 1 commit into from
Feb 19, 2025

Conversation

stephentoub
Copy link
Member

@stephentoub stephentoub commented Feb 19, 2025

When the Take amount is larger than the number of elements in the source Iterator<T>, Last ends up throwing an exception and LastOrDefault ends up returning the default value, rather than returning the last value in the taken region. This is not an issue when the source is a not an Iterator<T>, e.g. it doesn't affect cases where the source is an IList<T> or an arbitrary IEnumerable<T>. This is a regression in .NET 9.

As part of fixing this, I sured up the tests to try to cover more such sequences of operations. In doing so, the tests got a lot slower, so I tracked down and fixed places where we were doing a lot of unnecessary work.

When the Take amount is larger than the number of elements in the source `Iterator<T>`, Last ends up throwing an exception and LastOrDefault ends up returning the default value, rather than returning the last value in the taken region.

As part of fixing this, I sured up the tests to try to cover more such sequences of operations. In doing so, the tests got a lot slower, so I tracked down and fixed places where we were doing a lot of unnecessary work.
@Copilot Copilot bot review requested due to automatic review settings February 19, 2025 03:51
Copy link
Contributor

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

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

PR Overview

This pull request fixes a regression in LINQ behavior for Iterator when using Take(...).Last(...), ensuring that when the requested take amount exceeds the available elements the last element is returned rather than throwing an exception. In addition, the PR consolidates and streamlines test coverage by replacing custom transform helpers with a unified CreateSources method and by refactoring various test cases for consistency and performance.

  • Fixed comparison logic in the SkipTake optimizations.
  • Updated IdentityTransforms to return a more flexible IEnumerable and added extra transforms.
  • Consolidated test helper functions by replacing IdentityTransforms and GuaranteeNotIList with CreateSources.
  • Refactored tests for TakeLast, SkipLast, Single, and other LINQ operators.

Changes

File Description
src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs Adjusted condition in the Take method to properly branch when count equals or exceeds _minIndexInclusive.
src/libraries/System.Linq/tests/EnumerableTests.cs Updated IdentityTransforms with additional transforms and replaced custom empty literal usages.
src/libraries/System.Linq/tests/SelectManyTests.cs Simplified the ParameterizedTests by using an internal loop rather than MemberData.
Other test files Replaced redundant helpers (IdentityTransforms, GuaranteeNotIList) with CreateSources and refactored tests for clarity and performance.

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs:429

  • Changing the condition from '>= _minIndexInclusive' to '> _minIndexInclusive' affects the boundary behavior; please confirm that this change correctly handles the case when count equals _minIndexInclusive without introducing an off-by-one error.
count > _minIndexInclusive

Tip: Leave feedback on Copilot's review comments with the 👎 and 👍 buttons to help improve review quality. Learn more

// another one that has equivalent contents.
var identityTransforms = IdentityTransforms<T>();

// We run the transforms N^2 times, by testing all transforms
Copy link
Member

Choose a reason for hiding this comment

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

I'm puzzled by the purpose this method, it all seems entirely unnecessary. Am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

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

The purpose of the method I deleted because it served no worthwhile purpose? :)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah. I'm legitimately curious about what motivated its inclusion back then.

Copy link
Member Author

Choose a reason for hiding this comment

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

Misguided, I think. The stated intent is to validate that all transformations are equivalent to all other transformations, but that doesn't require N^2 tests, and we also don't need to test the test transformations themselves.

I guess it didn't hurt much when it was fast because there were only a few transformations, but my change causes N to grow from 13 to 1099, which means N^2 grows from 169 to 1,207,801... and that difference is very noticeable :)

@@ -426,9 +426,12 @@ public override Iterator<TSource> Take(int count)
{
if (_source is Iterator<TSource> iterator &&
iterator.GetCount(onlyIfCheap: true) is int count &&
count >= _minIndexInclusive)
count > _minIndexInclusive)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here we miss a fast path if count != -1 but count <= _minIndexInclusive.

Copy link
Member Author

@stephentoub stephentoub Feb 19, 2025

Choose a reason for hiding this comment

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

The purpose of this change is to fix a bug. I don't want to add additional optimizations as part of it. Please feel free to submit a follow-up PR after this goes in.

@stephentoub stephentoub merged commit 3232b44 into dotnet:main Feb 19, 2025
85 of 87 checks passed
@stephentoub
Copy link
Member Author

/backport to release/9.0-staging

@stephentoub stephentoub deleted the fixtakelast branch February 19, 2025 14:38
Copy link
Contributor

Started backporting to release/9.0-staging: https://github.com/dotnet/runtime/actions/runs/13415170353

Copy link
Contributor

@stephentoub backporting to "release/9.0-staging" failed, the patch most likely resulted in conflicts:

$ git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patch

Applying: Fix LINQ handling of iterator.Take(...).Last(...)
Using index info to reconstruct a base tree...
M	src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs
M	src/libraries/System.Linq/tests/AggregateByTests.cs
M	src/libraries/System.Linq/tests/ChunkTests.cs
M	src/libraries/System.Linq/tests/ConcatTests.cs
M	src/libraries/System.Linq/tests/CountTests.cs
M	src/libraries/System.Linq/tests/EnumerableTests.cs
M	src/libraries/System.Linq/tests/SelectManyTests.cs
M	src/libraries/System.Linq/tests/SingleOrDefaultTests.cs
M	src/libraries/System.Linq/tests/SingleTests.cs
M	src/libraries/System.Linq/tests/SkipLastTests.cs
M	src/libraries/System.Linq/tests/SkipTests.cs
M	src/libraries/System.Linq/tests/TakeLastTests.cs
M	src/libraries/System.Linq/tests/TakeTests.cs
M	src/libraries/System.Linq/tests/WhereTests.cs
Falling back to patching base and 3-way merge...
Auto-merging src/libraries/System.Linq/tests/WhereTests.cs
Auto-merging src/libraries/System.Linq/tests/TakeTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/TakeTests.cs
Auto-merging src/libraries/System.Linq/tests/TakeLastTests.cs
Auto-merging src/libraries/System.Linq/tests/SkipTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/SkipTests.cs
Auto-merging src/libraries/System.Linq/tests/SkipLastTests.cs
Auto-merging src/libraries/System.Linq/tests/SingleTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/SingleTests.cs
Auto-merging src/libraries/System.Linq/tests/SingleOrDefaultTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/SingleOrDefaultTests.cs
Auto-merging src/libraries/System.Linq/tests/SelectManyTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/SelectManyTests.cs
Auto-merging src/libraries/System.Linq/tests/EnumerableTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/EnumerableTests.cs
Auto-merging src/libraries/System.Linq/tests/CountTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/CountTests.cs
Auto-merging src/libraries/System.Linq/tests/ConcatTests.cs
Auto-merging src/libraries/System.Linq/tests/ChunkTests.cs
Auto-merging src/libraries/System.Linq/tests/AggregateByTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Linq/tests/AggregateByTests.cs
Auto-merging src/libraries/System.Linq/src/System/Linq/SkipTake.SpeedOpt.cs
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0001 Fix LINQ handling of iterator.Take(...).Last(...)
Error: The process '/usr/bin/git' failed with exit code 128

Please backport manually!

grendello added a commit to grendello/runtime that referenced this pull request Feb 19, 2025
* main: (27 commits)
  Fold null checks against known non-null values (dotnet#109164)
  JIT: Always track the context for late devirt (dotnet#112396)
  JIT: array allocation fixes (dotnet#112676)
  [H/3] Fix test closing connection too fast (dotnet#112691)
  Fix LINQ handling of iterator.Take(...).Last(...) (dotnet#112680)
  [browser][MT] move wasm MT CI legs to extra-platforms (dotnet#112690)
  JIT: Don't use `Compiler::compFloatingPointUsed` to check if FP kills are needed (dotnet#112668)
  [LoongArch64] Fix a typo within PR#112166. (dotnet#112672)
  Fix new EH hang on DebugBreak (dotnet#112640)
  Use encode callback instead of renting a buffer to write to in DSAKeyFormatHelper
  Move some links to other doc (dotnet#112574)
  Reflection-based XmlSerializer - Deserialize empty collections and allow for sub-types in collection items. (dotnet#111723)
  JIT: Use `fgCalledCount` for OSR method entry weight (dotnet#112662)
  Use Avx10.2 Instructions in Floating Point Conversions (dotnet#111775)
  Expose StressLog via CDAC and port StressLogAnalyzer to managed code (dotnet#104999)
  JIT: Use linear block order for MinOpts in LSRA (dotnet#108147)
  Update dependencies from https://github.com/dotnet/arcade build 20250213.2 (dotnet#112625)
  JIT: Clean up and optimize call arg lowering (dotnet#112639)
  Update dependencies from https://github.com/dotnet/emsdk build 20250217.1 (dotnet#112645)
  JIT: Support `FIELD_LIST` for returns (dotnet#112308)
  ...
stephentoub added a commit to stephentoub/runtime that referenced this pull request Feb 19, 2025
When the Take amount is larger than the number of elements in the source `Iterator<T>`, Last ends up throwing an exception and LastOrDefault ends up returning the default value, rather than returning the last value in the taken region.

As part of fixing this, I sured up the tests to try to cover more such sequences of operations. In doing so, the tests got a lot slower, so I tracked down and fixed places where we were doing a lot of unnecessary work.
stephentoub added a commit that referenced this pull request Feb 21, 2025
When the Take amount is larger than the number of elements in the source `Iterator<T>`, Last ends up throwing an exception and LastOrDefault ends up returning the default value, rather than returning the last value in the taken region.

As part of fixing this, I sured up the tests to try to cover more such sequences of operations. In doing so, the tests got a lot slower, so I tracked down and fixed places where we were doing a lot of unnecessary work.
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.

3 participants