Skip to content

Make the SkipLast polyfill streaming - #692

Merged
SimonCropp merged 2 commits into
mainfrom
perf/skiplast-streaming
Oct 12, 2026
Merged

SimonCropp merged 2 commits into
mainfrom
perf/skiplast-streaming

Conversation

@SimonCropp

@SimonCropp SimonCropp commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

The SkipLast polyfill (.NET Framework / netstandard2.0) was target.Reverse().Skip(count).Reverse(). That pulls the entire source and buffers it twice before the first item is yielded, so source.SkipLast(3).Take(3) over 1,000,000 items enumerated all 1,000,000.

It now uses the existing TakeRangeFromEndIterator helper (range 0..^count), which is what the BCL does:

  • when the count is known without enumerating (ICollection<T> / ICollection), it yields the first Count - count items and stops;
  • otherwise it streams through a queue that holds back at most count items.

count <= 0 returns target.Skip(0) as the BCL does. A null source still throws ArgumentNullException (parameter name source, as before) when SkipLast is called, and execution is still deferred.

Results are identical. The difference is in when the source is pulled: items are yielded as they become available, where before the whole source was consumed on the first MoveNext.

Measurements

net48, Release, x64, Stopwatch, best of 5, microseconds per call:

Scenario Before After
SkipLast(3).Take(3) over a 1,000,000 item iterator 22,821 0.14
SkipLast(3) over a List<int> of 1,000,000, fully enumerated 15,923 11,422
SkipLast(3) over a 1,000,000 item iterator, fully enumerated 20,203 12,169 to 25,785 (shared machine, two runs)
SkipLast(3) over a List<int> of 20 0.47 0.32
SkipLast(3) over a 20 item iterator 0.63 0.66

Full enumeration costs about the same time, without the two full-size buffers (the queue holds 3 items here).

Tests

Added to PolyfillTests_IEnumerable.cs:

  • results for skip counts from int.MinValue to int.MaxValue against an array, List<T>, Queue<T>, HashSet<T> and an iterator, for lengths 0, 1, 2 and 10, including enumerating the result twice;
  • reference types with nulls;
  • streaming: SkipLast(3).Take(3) over a 1,000,000 item iterator pulls at most 7 items;
  • deferred execution: nothing is pulled until enumeration, and list changes after the call are seen;
  • a null source throws when called, for positive and zero counts.

The deferred-execution test skips its list assertion on .NET 5 (checked at run time): the real SkipLast there fixes its bounds over a list when it is called, which .NET 6 changed.

SkipLast was Reverse().Skip(count).Reverse(), which buffered the whole source twice before yielding anything. Use the queue based range iterator, as the BCL does.
@SimonCropp SimonCropp added this to the 11.4.5 milestone Oct 12, 2026
@SimonCropp
SimonCropp merged commit 660e517 into main Oct 12, 2026
5 checks passed
@SimonCropp
SimonCropp deleted the perf/skiplast-streaming branch October 12, 2026 12:07
This was referenced Oct 12, 2026
This was referenced Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant