From 59c415d0463868c0ae061d741f37ad26810d714f Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 26 Jan 2020 20:35:26 +0100 Subject: [PATCH 1/4] Remove list allocations from the general case of Selector.Match --- src/Avalonia.Styling/Styling/Selector.cs | 29 +++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index af209ea970..b8a67e6d96 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -41,7 +41,9 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - var inputs = new List>(); + List> inputs = null; + IObservable singleInput = null; + var selector = this; var alwaysThisType = true; var hitCombinator = false; @@ -66,16 +68,37 @@ namespace Avalonia.Styling } else if (match.Result == SelectorMatchResult.Sometimes) { - inputs.Add(match.Activator); + if (inputs != null) + { + inputs.Add(match.Activator); + } + else + { + if (singleInput == null) + { + singleInput = match.Activator; + } + else + { + inputs = new List>(); + + inputs.Add(singleInput); + inputs.Add(match.Activator); + } + } } selector = selector.MovePrevious(); } - if (inputs.Count > 0) + if (inputs != null) { return new SelectorMatch(StyleActivator.And(inputs)); } + else if (singleInput != null) + { + return new SelectorMatch(singleInput); + } else { return alwaysThisType && !hitCombinator ? From 5873057817f5b2dd76d6b1f4f3e00c524d4ecb78 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 26 Jan 2020 20:53:11 +0100 Subject: [PATCH 2/4] Add sanity check. --- src/Avalonia.Styling/Styling/Selector.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index b8a67e6d96..52c6ae4027 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; namespace Avalonia.Styling { @@ -68,6 +69,8 @@ namespace Avalonia.Styling } else if (match.Result == SelectorMatchResult.Sometimes) { + Debug.Assert(match.Activator != null); + if (inputs != null) { inputs.Add(match.Activator); From 46781ad9082ccea3aab7950095238003269cbaad Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 26 Jan 2020 20:58:41 +0100 Subject: [PATCH 3/4] Add basic benchmarks for Is and Class selectors. --- .../Styling/SelectorBenchmark.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs diff --git a/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs new file mode 100644 index 0000000000..0ac96c1103 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs @@ -0,0 +1,52 @@ +using Avalonia.Controls; +using Avalonia.Styling; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Styling +{ + [MemoryDiagnoser] + public class SelectorBenchmark + { + private readonly Control _notMatchingControl; + private readonly Calendar _matchingControl; + private readonly Selector _isCalendarSelector; + private readonly Selector _classSelector; + + public SelectorBenchmark() + { + _notMatchingControl = new Control(); + _matchingControl = new Calendar(); + + const string className = "selector-class"; + + _matchingControl.Classes.Add(className); + + _isCalendarSelector = Selectors.Is(null); + _classSelector = Selectors.Class(null, className); + } + + [Benchmark] + public SelectorMatch IsSelector_NoMatch() + { + return _isCalendarSelector.Match(_notMatchingControl); + } + + [Benchmark] + public SelectorMatch IsSelector_Match() + { + return _isCalendarSelector.Match(_matchingControl); + } + + [Benchmark] + public SelectorMatch ClassSelector_NoMatch() + { + return _classSelector.Match(_notMatchingControl); + } + + [Benchmark] + public SelectorMatch ClassSelector_Match() + { + return _classSelector.Match(_matchingControl); + } + } +} From beea785f30f6010ba7ffd3f2ce5d9f7d785dcadc Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 27 Jan 2020 23:13:10 +0100 Subject: [PATCH 4/4] Implement simple struct wrapping a single value or List. --- .../Utilities/ValueSingleOrList.cs | 94 +++++++++++++++++++ src/Avalonia.Styling/Styling/Selector.cs | 33 ++----- 2 files changed, 102 insertions(+), 25 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/ValueSingleOrList.cs diff --git a/src/Avalonia.Base/Utilities/ValueSingleOrList.cs b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs new file mode 100644 index 0000000000..dc32cedb76 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ValueSingleOrList.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + /// + /// A list like struct optimized for holding zero or one items. + /// + /// The type of items held in the list. + /// + /// Once more than value has been added to this storage it will switch to using internally. + /// + public ref struct ValueSingleOrList + { + private bool _isSingleSet; + + /// + /// Single contained value. Only valid if is set. + /// + public T Single { get; private set; } + + /// + /// List of values. + /// + public List List { get; private set; } + + /// + /// If this struct is backed by a list. + /// + public bool HasList => List != null; + + /// + /// If this struct contains only single value and storage was not promoted to a list. + /// + public bool IsSingle => List == null && _isSingleSet; + + /// + /// Adds a value. + /// + /// Value to add. + public void Add(T value) + { + if (List != null) + { + List.Add(value); + } + else + { + if (!_isSingleSet) + { + Single = value; + + _isSingleSet = true; + } + else + { + List = new List(); + + List.Add(Single); + List.Add(value); + + Single = default; + } + } + } + + /// + /// Removes a value. + /// + /// Value to remove. + public bool Remove(T value) + { + if (List != null) + { + return List.Remove(value); + } + + if (!_isSingleSet) + { + return false; + } + + if (EqualityComparer.Default.Equals(Single, value)) + { + Single = default; + + _isSingleSet = false; + + return true; + } + + return false; + } + } +} diff --git a/src/Avalonia.Styling/Styling/Selector.cs b/src/Avalonia.Styling/Styling/Selector.cs index 52c6ae4027..7d4e92baeb 100644 --- a/src/Avalonia.Styling/Styling/Selector.cs +++ b/src/Avalonia.Styling/Styling/Selector.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Utilities; namespace Avalonia.Styling { @@ -42,8 +43,7 @@ namespace Avalonia.Styling /// A . public SelectorMatch Match(IStyleable control, bool subscribe = true) { - List> inputs = null; - IObservable singleInput = null; + ValueSingleOrList> inputs = default; var selector = this; var alwaysThisType = true; @@ -71,40 +71,23 @@ namespace Avalonia.Styling { Debug.Assert(match.Activator != null); - if (inputs != null) - { - inputs.Add(match.Activator); - } - else - { - if (singleInput == null) - { - singleInput = match.Activator; - } - else - { - inputs = new List>(); - - inputs.Add(singleInput); - inputs.Add(match.Activator); - } - } + inputs.Add(match.Activator); } selector = selector.MovePrevious(); } - if (inputs != null) + if (inputs.HasList) { - return new SelectorMatch(StyleActivator.And(inputs)); + return new SelectorMatch(StyleActivator.And(inputs.List)); } - else if (singleInput != null) + else if (inputs.IsSingle) { - return new SelectorMatch(singleInput); + return new SelectorMatch(inputs.Single); } else { - return alwaysThisType && !hitCombinator ? + return alwaysThisType && !hitCombinator ? SelectorMatch.AlwaysThisType : SelectorMatch.AlwaysThisInstance; }