perf: Optimize Chart control performance with reduced allocations and improved algorithmic complexity#375
Open
PaulAndersonS wants to merge 1 commit into
Open
Conversation
- Replace O(n²) IndexOf lookups with Dictionary in CategoryAxis.GroupData() - Use HashSet for O(1) Contains checks in category value grouping - Replace LINQ ToList() for single item with simple loop in GetAxisByName - Cache GetType().Name result to avoid repeated reflection in stacking calculations - Replace four LINQ Where/Min/Max queries with single loop in ErrorBarSegment - Replace LINQ Where().Sum() with simple loop in stacking value calculation - Replace LINQ-based index generation with pre-allocated List in GetXValues - Replace LINQ Where/Select/Min/Max with single loop for YRange calculation - Eliminate unnecessary List allocations in GetSdErrorValue - Cache Values.ToList() result outside loops in SBS calculations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or 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
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.
Root Cause of the Issue
Several hot paths in the Chart control used LINQ queries,
List.Contains(),List.IndexOf(), and repeatedGetType().Namereflection calls that introduced unnecessary allocations and O(n²) complexity in data-intensive rendering paths.Description of Change
10 targeted micro-optimizations across 5 files to reduce allocations and improve algorithmic complexity in the Chart control:
CategoryAxis.cs— O(n²)IndexOf→DictionarylookupGroupData()calledList.IndexOf()inside a LINQselectover every data point, making it O(n²). Replaced with a pre-builtDictionary<string, int>for O(1) index lookups, and used pre-allocatedList<double>instead of LINQ materialization.CategoryAxis.cs—List.Contains()→HashSetThe deduplication loop in
GroupData()usedList.Contains()(O(n)) on every iteration. Replaced with aHashSet<string>seeded from existing values for O(1) membership checks.CartesianAxisLayout.cs— LINQToList()for single-item lookup → simpleforloopGetAxisByName()created a full materializedListjust to return the first match. Replaced with an early-returnforloop that avoids all allocation and stops at the first match.CartesianChartArea.cs— RepeatedGetType().Namereflection → cached resultCalculateStackingValues()calledGetType().Name.Contains(...)twice per data point inside nested loops (once perifbranch). Cached the type name and the boolean result before the inner loop to eliminate repeated reflection.ErrorBarSegment.cs— 4× LINQWhere/Min/Max→ single pass loopsComputing
xMin/xMax/yMin/yMaxrequired 4 separate LINQ chains, each iterating the full collection. Replaced with 2forloops that compute both min and max in a single pass per axis, withNaNfallback to0.CartesianChartArea.cs— LINQWhere().Sum()→forloop inGetYValueGetYValue()used a LINQ chain with closure allocation on every call. Replaced with a plainforloop over the pre-typedList<StackingSeriesBase>.CartesianSeries.cs— LINQ-generated index sequences → pre-allocatedList<double>GetXValues()used(from val in list select (xIndexValues++)).ToList()to produce sequential[0, 1, 2, …]index lists, allocating an iterator and boxing closures. Replaced with anew List<double>(count)filled with aforloop, also removing the unusedxIndexValuesvariable.ErrorBarSegment.cs— LINQWhere/Select/Min/MaxforYRange→ single loopThe percentage-mode
YRangecalculation enumerated_topPointCollectionand_bottomPointCollection6 times (.Where,.Select,.Any,.Min,.Max× 2 collections). Replaced with twoforloops that find the overall min/max in a single pass with ahasValidguard.ErrorBarSegment.cs— Eliminate list allocations inGetSdErrorValueGetSdErrorValue()allocated a filteredList, then two moreList<double>(dev,sQDev) just to accumulate a sum of squares. Replaced with two inline loops that computesum,count, andsumSqDevwithout any heap allocations.CartesianChartArea.cs—Values.ToList()inside loops → cache before loopUpdateSBS()andGetTotalWidth()calledSideBySideSeriesPosition.Values.ToList()on every loop iteration, creating a freshListsnapshot each time. Moved the call outside the loop and iterated the cached list.Issues Fixed
No specific issue — proactive performance improvements.
Screenshots
N/A — internal algorithmic changes with no visible behavior difference.