-
Notifications
You must be signed in to change notification settings - Fork 704
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
Reduce func allocations #3919
Merged
Merged
Reduce func allocations #3919
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Removes the lambda func allocations caused by captured outer variables.
It should be enough to add null-forgiving operator somewhere in the LINQ query to make the final result non-null. No need to shove the nullability further down the line to complicate things. :)
…h loop and rented array
…nditional operators
No need to first put the dimensions in a list and then sum the list when you can just add to sum while looping through dimensions.
…filter func allocation
GetCellMap would not benefit much from array pool because IntersectionDefinition is not struct. This change eliminates majority of the rest of Func<,> allocations. As a bonus IntersectionDefinition[] allocations dropped nicely.
tig
approved these changes
Feb 25, 2025
tig
added a commit
that referenced
this pull request
Feb 28, 2025
* Moved scripts * testing versions * Rune extensions micro-optimizations (#3910) * Add benchmarks for potentially optimizable RuneExtensions * Add new RuneExtensions.DecodeSurrogatePair benchmark implementation Avoids intermediate heap array allocations which is especially nice when the rune is not surrogate pair because then array heap allocations are completely avoided. * Enable nullable reference types in RuneExtensions * Make RuneExtensions.MaxUnicodeCodePoint readonly Makes sure no one can accidentally change the value. Ideally would be const value. * Optimize RuneExtensions.DecodeSurrogatePair * Remove duplicate Rune.GetUnicodeCategory call * Add new RuneExtensions.IsSurrogatePair benchmark implementation Avoids intermediate heap allocations by using stack allocated buffer. * Optimize RuneExtensions.IsSurrogatePair * Add RuneExtensions.GetEncodingLength tests * Optimize RuneExtensions.GetEncodingLength * Optimize RuneExtensions.Encode * Print encoding name in benchmark results * Rename variable to better match return description * Add RuneExtensions.EncodeSurrogatePair benchmark --------- Co-authored-by: Tig <[email protected]> * Reduce func allocations (#3919) * Replace Region.Contains LINQ lambdas with foreach loop Removes the lambda func allocations caused by captured outer variables. * Replace LineCanvas.Has LINQ lambda with foreach loop * Fix LineCanvas.GetMap intersects array nullability It should be enough to add null-forgiving operator somewhere in the LINQ query to make the final result non-null. No need to shove the nullability further down the line to complicate things. :) * Replace LineCanvas.All LINQ lambda with foreach loop * Replace Region.Intersect LINQ lambdas and list allocation with foreach loop and rented array * Use stackalloc buffer in Region.Intersect when max 8 rectangles * Fix LineCanvas.GetCellMap intersects array nullability * Remove leftover LineCanvas.GetRuneForIntersects null-conditional operators * Remove leftover IntersectionRuneResolver.GetRuneForIntersects null-conditional operators * PosAlign.CalculateMinDimension: calculate sum during loop No need to first put the dimensions in a list and then sum the list when you can just add to sum while looping through dimensions. * PosAlign.CalculateMinDimension: Remove intermediate list and related filter func allocation * TextFormatter.GetRuneWidth: Remove intermediate list and related sum func allocation * ReadOnlySpan refactor preparation for GetCellMap rewrite * LineCanvas.GetCellMap: Reuse intersection list outside nested loops GetCellMap would not benefit much from array pool because IntersectionDefinition is not struct. This change eliminates majority of the rest of Func<,> allocations. As a bonus IntersectionDefinition[] allocations dropped nicely. * Refactor local method UseRounded * Wrap too long list of method parameters * Region: Consistent location for #nullable enable --------- Co-authored-by: Tig <[email protected]> * Fixes #3918 and #3913 - `Accepting` behavior (#3921) * Fixed #3905, #3918 * Tweaked Generic * Label code cleanup * Clean up. * Clean up. * Clean up2. * Fixes #3839, #3922 - CM Glyphs not working (#3923) * fixed * Moved Glyphs to ThemeScope * Removed test code * Fixed nav (#3926) * Fixes #3881. PositionCursor broke with recent ConsoleDriver changes. (#3927) * Reduce IntersectionType[] allocations (#3924) * Eliminate LineCanvas.Has params array allocation Inline ReadOnlySpan arguments do not incur heap allocation compared to regular arrays. * Allocate once LineCanvas.Exactly corner intersection arrays --------- Co-authored-by: Tig <[email protected]> * API doc updates (#3928) --------- Co-authored-by: Tonttu <[email protected]> Co-authored-by: BDisp <[email protected]>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes
Func<,>
allocations originating from LINQ lambda outer variable captures by replacing them with equivalent loops.** Measured in most likely skewed scenario described in the "Data collection scenario" section.
Proposed Changes/Todos
LineCanvas
to reduce majority of Func allocationsLineCanvas.All(...)
LineCanvas.GetCellMap(...)
LineCanvas.Has(...)
Region
to reduce majority of Func allocationsRegion.Contains(...)
Region.Intersect(...)
PosAlign.CalculateMinDimension(...)
to reduce majority of Func allocationsTextFormatter.GetRuneWidth(...)
to reduce majority of Func allocationsPull Request checklist
CTRL-K-D
to automatically reformat your files before committing.dotnet test
before commit///
style comments)Disclaimer
I like LINQ and the convenience it offers and use it most of time over loops when appropriate. Unfortunately when used in application hot paths the heap allocations can get out of hand due to hidden allocations from captured outer variables.
Data collection scenario
Run in Windows Terminal v1.21.10351.0 120x30 Command Prompt tab.
This does not necessarily represent a typical application use and probably skews the statistics but at least reveals some allocation issues when using similar list navigation.
Ideally there would be some sort of automation that emulates more typical application.
Object allocation statistics
Data collected in Release build using Visual Studio Performance Profiler .NET Object Allocation Tracking.
Before
After
As a bonus, also list and enumerator allocations decreased slightly.

The main focus
Func<,>
dropped way down the allocation list.There are still some unnecessary Func<,> allocations but the PR is already getting quite bloated. Also some of them are not worth the effort unless of course profiling other parts of the UICatalog tells otherwise. I nope'd out quite quickly after seeing
DimAuto.Calculate(...)
. 😆Refactors
LineCanvas.All
LINQ All replaced with equivalent foreach loop.
LineCanvas.GetCellMap
Replaced array allocation inside nested loops + LINQ lambdas with List buffer outside the nested. If IntersectionDefinition would be struct then stackalloc/ArrayPool would have made more sense.
Maybe the biggest change due to ReadOnlySpan replacing arrays in the methods further down the line, which forced to replace LINQ lambdas from those methods as well.
GetRuneForIntersects(...)
being the biggest "victim" of this change. Tried to keep it readable with local methods.LineCanvas.Has
Similar change as in
LineCanvas.All(...)
.Region.Contains
Replaced LINQ Any with foreach loop.
Region.Intersect
Replaced list allocation and LINQ lambdas with foreach loop utilizing stackalloc buffer or pooled array depending on the amount of region rectangles. The original rectangles list is cleared and reused instead of creating new list every time. The list can still of course internally create new array if capacity is too small.
PosAlign.CalculateMinDimension
Group list and sum list allocations with multiple loops replaced with single loop combining the group check and summing.
TextFormatter.GetRuneWidth
Removed intermediate list allocation and just looped through the StringRuneEnumerator and summing the widths.