Skip to content

perf: enumerate result pages without boxing - #7

Closed
msallin wants to merge 1 commit into
developfrom
perf/result-struct-enumerators
Closed

perf: enumerate result pages without boxing#7
msallin wants to merge 1 commit into
developfrom
perf/result-struct-enumerators

Conversation

@msallin

@msallin msallin commented Sep 6, 2026

Copy link
Copy Markdown
Member

SvgResult and PngResult implement IReadOnlyList<T> by forwarding GetEnumerator() to the list behind their Pages property. Because Pages is typed as the interface, the list hands its enumerator back as an IEnumerator<T>, so the struct is boxed on every foreach. Measured: 80 bytes for one pass over an SvgResult and a PngResult, 40 each.

A struct enumerator, which foreach binds to in preference to the interface, takes that to zero. The interface implementations remain, now explicit, so LINQ and IEnumerable callers are unaffected.

Adds one public type, PageEnumerator<T>, shared by both results rather than duplicated as two nested structs.

Compatibility

Source-compatible: foreach, LINQ, Count and the indexer all keep compiling, and result.GetEnumerator() still converts to IEnumerator<T> where a caller assigned it to one.

Binary-breaking: the public IEnumerator<T> GetEnumerator() becomes an explicit interface implementation. This needs a version bump rather than a patch release, so it should be scheduled rather than merged alongside the other allocation fixes.

Tests

  • EnumeratingResultPagesDoesNotAllocate asserts zero allocations across one pass over both result types. Confirmed failing at exactly 80 bytes against the previous implementation.
  • ResultPagesEnumerateInIndexOrderThroughBothPaths pins that the struct enumerator agrees with the indexer, and that the interface path LINQ uses still yields the same pages.

58/58 tests pass. Builds clean on net8, net9 and net10 with zero warnings.

SvgResult and PngResult forwarded GetEnumerator to the list behind their Pages
property. The list hands its enumerator back as an IEnumerator<T>, so every
foreach boxed the struct: 80 bytes measured for one pass over an SVG result and
a PNG result.

A struct enumerator, which foreach binds to in preference to the interface,
removes that. The interface implementations stay for LINQ and IEnumerable
callers.
@msallin
msallin force-pushed the perf/result-struct-enumerators branch from e24d3f8 to 44d24bf Compare September 6, 2026 19:16
@msallin

msallin commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Reopened upstream against the parent repository: evolvedlight#50

@msallin msallin closed this Sep 6, 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