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

Reduce func allocations #3919

Merged
merged 18 commits into from
Feb 25, 2025
Merged

Conversation

TheTonttu
Copy link
Contributor

@TheTonttu TheTonttu commented Feb 22, 2025

Fixes

  • Improves Intermediate heap allocations #2725
  • Eliminates major sources of 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

  • Refactor LineCanvas to reduce majority of Func allocations
    • LineCanvas.All(...)
    • LineCanvas.GetCellMap(...)
    • LineCanvas.Has(...)
  • Refactor Region to reduce majority of Func allocations
    • Region.Contains(...)
    • Region.Intersect(...)
  • Refactor PosAlign.CalculateMinDimension(...) to reduce majority of Func allocations
  • Refactor TextFormatter.GetRuneWidth(...) to reduce majority of Func allocations

Pull Request checklist

  • I've named my PR in the form of "Fixes #issue. Terse description."
  • My code follows the style guidelines of Terminal.Gui - if you use Visual Studio, hit CTRL-K-D to automatically reformat your files before committing.
  • My code follows the Terminal.Gui library design guidelines
  • I ran dotnet test before commit
  • I have made corresponding changes to the API documentation (using /// style comments)
  • My changes generate no new warnings
  • I have checked my code and corrected any poor grammar or misspellings
  • I conducted basic QA to assure all features are working

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.

  1. Start UICatalog.
  2. Navigate down the Categories list using arrow key down.
    • Pause for a while (~1 sec) between key presses so that the Scenarios view has time to load properly.
  3. Once at the bottom of the list navigate up using the arrow key up in similar manner until back at the top of the Categories list.
  4. Quit application.
  5. ???
  6. Profit

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

1_before_func-allocations

After

As a bonus, also list and enumerator allocations decreased slightly.
2_after_1_top_allocations

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(...). 😆

2_after_2_func-allocations

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.

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. :)
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.
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.
@TheTonttu TheTonttu requested a review from tig as a code owner February 22, 2025 23:59
@tig
Copy link
Collaborator

tig commented Feb 25, 2025

With:

image

Without:

image

2 sec improvement. :-)

@tig tig merged commit 35522cc into gui-cs:v2_develop Feb 25, 2025
4 checks passed
@TheTonttu TheTonttu deleted the reduce-func-allocations branch February 25, 2025 18:07
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
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants