Browse Source

Refactor value storage to reduce overhead and memory allocations.

pull/8287/head
Dariusz Komosinski 4 years ago
parent
commit
cf0688565a
  1. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  2. 16
      src/Avalonia.Base/AvaloniaObject.cs
  3. 18
      src/Avalonia.Base/StyledElement.cs
  4. 236
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  5. 12
      src/Avalonia.Base/ValueStore.cs
  6. 3
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  7. 351
      tests/Avalonia.Benchmarks/Base/ValueStoreAddRemoveBenchmarks.cs
  8. 33
      tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -20,6 +20,7 @@
<ItemGroup Label="InternalsVisibleTo">
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />

16
src/Avalonia.Base/AvaloniaObject.cs

@ -25,6 +25,7 @@ namespace Avalonia
private List<AvaloniaObject>? _inheritanceChildren;
private ValueStore? _values;
private bool _batchUpdate;
private bool _isInitializing;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@ -126,6 +127,9 @@ namespace Avalonia
{
_values = new ValueStore(this);
_values.IsInitializing = _isInitializing;
_isInitializing = false;
if (_batchUpdate)
_values.BeginBatchUpdate();
}
@ -493,6 +497,18 @@ namespace Avalonia
_batchUpdate = false;
_values?.EndBatchUpdate();
}
internal void SetValueStoreIsInitializing(bool isInitializing)
{
if (_values is null)
{
_isInitializing = isInitializing;
return;
}
_values.IsInitializing = isInitializing;
}
/// <inheritdoc/>
internal void AddInheritanceChild(AvaloniaObject child)

18
src/Avalonia.Base/StyledElement.cs

@ -306,6 +306,11 @@ namespace Avalonia
public virtual void BeginInit()
{
++_initCount;
if (_initCount == 1)
{
SetValueStoreIsInitializing(true);
}
}
/// <inheritdoc/>
@ -316,10 +321,15 @@ namespace Avalonia
throw new InvalidOperationException("BeginInit was not called.");
}
if (--_initCount == 0 && _logicalRoot != null)
if (--_initCount == 0)
{
ApplyStyling();
InitializeIfNeeded();
if (_logicalRoot is not null)
{
ApplyStyling();
InitializeIfNeeded();
}
SetValueStoreIsInitializing(false);
}
}
@ -337,11 +347,13 @@ namespace Avalonia
try
{
BeginBatchUpdate();
SetValueStoreIsInitializing(true);
AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
}
finally
{
EndBatchUpdate();
SetValueStoreIsInitializing(false);
}
_styled = true;

236
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Utilities
@ -8,166 +7,197 @@ namespace Avalonia.Utilities
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
internal struct AvaloniaPropertyValueStore<TValue>
{
// The last item in the list is always int.MaxValue.
private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } };
private Entry[] _entries;
private Entry[]? _entries;
private int _entryCount;
public AvaloniaPropertyValueStore()
{
_entries = s_emptyEntries;
_entries = null;
_entryCount = 0;
IsInitializing = false;
InitialSize = 4;
}
public int Count => _entries.Length - 1;
public TValue this[int index] => _entries[index].Value;
public int Count => _entryCount;
public bool IsInitializing { get; set; }
public int InitialSize { get; set; }
public TValue this[int index] => _entries![index].Value;
private (int, bool) TryFindEntry(int propertyId)
private EntryIndex LookupEntry(int propertyId)
{
if (_entries.Length <= 12)
int checkIndex;
int iLo = 0;
int iHi = _entryCount;
if (iHi <= 0)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId)
return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId)
return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId)
return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId)
return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId)
return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId)
return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId)
return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId)
return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId)
return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId)
return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId)
return (10, _entries[10].PropertyId == propertyId);
return new EntryIndex(0, found: false);
}
else
// Do a binary search to find the value
while (iHi - iLo > 3)
{
int low = 0;
int high = _entries.Length;
int id;
int iPv = (iHi + iLo) / 2;
checkIndex = _entries![iPv].PropertyId;
while (high - low > 3)
if (propertyId == checkIndex)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
return new EntryIndex(iPv, found: true);
}
do
if (propertyId <= checkIndex)
{
iHi = iPv;
}
else
{
id = _entries[low].PropertyId;
iLo = iPv + 1;
}
}
if (id == propertyId)
return (low, true);
// Now we only have three values to search; switch to a linear search
do
{
checkIndex = _entries![iLo].PropertyId;
if (id > propertyId)
break;
if (checkIndex == propertyId)
{
return new EntryIndex(iLo, found: true);
}
++low;
if (checkIndex > propertyId)
{
// we've gone past the targetIndex - return not found
break;
}
while (low < high);
}
return (0, false);
iLo++;
} while (iLo < iHi);
return new EntryIndex(iLo, found: false);
}
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
var entryIndex = LookupEntry(property.Id);
if (!entryIndex.Found)
{
value = default;
return false;
}
value = _entries[index].Value;
value = _entries![entryIndex.Index].Value;
return true;
}
public void AddValue(AvaloniaProperty property, TValue value)
private void InsertEntry(Entry entry, int entryIndex)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
if (_entryCount > 0)
{
if (_entries[i].PropertyId > property.Id)
if (_entryCount == _entries!.Length)
{
if (i > 0)
// We want to have more aggressive resizing when initializing.
var growthFactor = IsInitializing ? 2.0 : 1.2;
var newSize = (int)(_entryCount * growthFactor);
if (newSize == _entryCount)
{
Array.Copy(_entries, 0, entries, 0, i);
newSize++;
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
var destEntries = new Entry[newSize];
Array.Copy(_entries, 0, destEntries, 0, entryIndex);
destEntries[entryIndex] = entry;
Array.Copy(_entries, entryIndex, destEntries, entryIndex + 1, _entryCount - entryIndex);
_entries = destEntries;
}
else
{
Array.Copy(
_entries,
entryIndex,
_entries,
entryIndex + 1,
_entryCount - entryIndex);
_entries[entryIndex] = entry;
}
}
else
{
if (_entries is null)
{
_entries = new Entry[InitialSize];
}
_entries[0] = entry;
}
_entries = entries;
_entryCount++;
}
public void AddValue(AvaloniaProperty property, TValue value)
{
var propertyId = property.Id;
var index = LookupEntry(propertyId);
InsertEntry(new Entry(propertyId, value), index.Index);
}
public void SetValue(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
var propertyId = property.Id;
var entryIndex = LookupEntry(propertyId);
_entries![entryIndex.Index] = new Entry(propertyId, value);
}
public void Remove(AvaloniaProperty property)
{
var (index, found) = TryFindEntry(property.Id);
var entry = LookupEntry(property.Id);
if (found)
{
var newLength = _entries.Length - 1;
// Special case - one element left means that value store is empty so we can just reuse our "empty" array.
if (newLength == 1)
{
_entries = s_emptyEntries;
return;
}
var entries = new Entry[newLength];
if (!entry.Found) return;
Array.Copy(_entries!, entry.Index + 1, _entries!, entry.Index, _entryCount - entry.Index - 1);
int ix = 0;
_entryCount--;
_entries![_entryCount] = default;
}
for (int i = 0; i < _entries.Length; ++i)
{
if (i != index)
{
entries[ix++] = _entries[i];
}
}
private readonly struct EntryIndex
{
public readonly int Index;
public readonly bool Found;
_entries = entries;
public EntryIndex(int index, bool found)
{
Index = index;
Found = found;
}
}
private struct Entry
private readonly struct Entry
{
internal int PropertyId;
internal TValue Value;
public readonly int PropertyId;
public readonly TValue Value;
public Entry(int propertyId, TValue value)
{
PropertyId = propertyId;
Value = value;
}
}
}
}

12
src/Avalonia.Base/ValueStore.cs

@ -24,7 +24,7 @@ namespace Avalonia
internal class ValueStore
{
private readonly AvaloniaObject _owner;
private readonly AvaloniaPropertyValueStore<IValue> _values;
private AvaloniaPropertyValueStore<IValue> _values;
private BatchUpdate? _batchUpdate;
public ValueStore(AvaloniaObject owner)
@ -33,6 +33,12 @@ namespace Avalonia
_values = new AvaloniaPropertyValueStore<IValue>();
}
public bool IsInitializing
{
get => _values.IsInitializing;
set => _values.IsInitializing = value;
}
public void BeginBatchUpdate()
{
_batchUpdate ??= new BatchUpdate(this);
@ -381,7 +387,7 @@ namespace Avalonia
private class BatchUpdate
{
private ValueStore _owner;
private readonly ValueStore _owner;
private List<Notification>? _notifications;
private int _batchUpdateCount;
private int _iterator = -1;
@ -408,7 +414,7 @@ namespace Avalonia
if (--_batchUpdateCount > 0)
return false;
var values = _owner._values;
ref var values = ref _owner._values;
// First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
// but notifications will still not be raised because the owner ValueStore will still have a reference

3
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -5,6 +5,9 @@
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
</PropertyGroup>
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />

351
tests/Avalonia.Benchmarks/Base/ValueStoreAddRemoveBenchmarks.cs

@ -0,0 +1,351 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base;
// TODO: Remove after review together with related benchmark code.
internal sealed class AvaloniaPropertyValueStoreOld<TValue>
{
// The last item in the list is always int.MaxValue.
private static readonly Entry[] s_emptyEntries = { new Entry { PropertyId = int.MaxValue, Value = default! } };
private Entry[] _entries;
public AvaloniaPropertyValueStoreOld()
{
_entries = s_emptyEntries;
}
public int Count => _entries.Length - 1;
public TValue this[int index] => _entries[index].Value;
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId)
return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId)
return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId)
return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId)
return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId)
return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId)
return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId)
return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId)
return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId)
return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId)
return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId)
return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, [MaybeNullWhen(false)] out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = default;
return false;
}
value = _entries[index].Value;
return true;
}
public void AddValue(AvaloniaProperty property, TValue value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_entries = entries;
}
public void SetValue(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
public void Remove(AvaloniaProperty property)
{
var (index, found) = TryFindEntry(property.Id);
if (found)
{
var newLength = _entries.Length - 1;
// Special case - one element left means that value store is empty so we can just reuse our "empty" array.
if (newLength == 1)
{
_entries = s_emptyEntries;
return;
}
var entries = new Entry[newLength];
int ix = 0;
for (int i = 0; i < _entries.Length; ++i)
{
if (i != index)
{
entries[ix++] = _entries[i];
}
}
_entries = entries;
}
}
private struct Entry
{
internal int PropertyId;
internal TValue Value;
}
}
internal class MockProperty : StyledProperty<int>
{
public MockProperty([JetBrains.Annotations.NotNull] string name) : base(name, typeof(object), new StyledPropertyMetadata<int>())
{
}
}
internal static class MockProperties
{
public static readonly AvaloniaProperty[] LinearProperties;
public static readonly AvaloniaProperty[] ShuffledProperties;
static MockProperties()
{
LinearProperties = new AvaloniaProperty[32];
ShuffledProperties = new AvaloniaProperty[32];
for (int i = 0; i < LinearProperties.Length; i++)
{
LinearProperties[i] = ShuffledProperties[i] = new MockProperty($"Property#{i}");
}
Shuffle(ShuffledProperties, 42);
}
private static void Shuffle<T> (T[] array, int seed)
{
var rng = new Random(seed);
int n = array.Length;
while (n > 1)
{
int k = rng.Next(n--);
T temp = array[n];
array[n] = array[k];
array[k] = temp;
}
}
}
[MemoryDiagnoser]
public class ValueStoreLookupBenchmarks
{
[Params(2, 6, 10, 20, 30)]
public int PropertyCount;
[Params(false, true)]
public bool UseShuffledProperties;
public AvaloniaProperty[] Properties => UseShuffledProperties ? MockProperties.ShuffledProperties : MockProperties.LinearProperties;
private AvaloniaPropertyValueStore<object> _store;
private AvaloniaPropertyValueStoreOld<object> _oldStore;
[GlobalSetup]
public void GlobalSetup()
{
_store = new AvaloniaPropertyValueStore<object>();
_oldStore = new AvaloniaPropertyValueStoreOld<object>();
for (int i = 0; i < PropertyCount; i++)
{
_store.AddValue(Properties[i], null);
_oldStore.AddValue(Properties[i], null);
}
}
[Benchmark]
public void LookupProperties()
{
for (int i = 0; i < PropertyCount; i++)
{
_store.TryGetValue(Properties[i], out _);
}
}
[Benchmark] public void LookupProperties_Old()
{
for (int i = 0; i < PropertyCount; i++)
{
_oldStore.TryGetValue(Properties[i], out _);
}
}
}
[MemoryDiagnoser]
public class ValueStoreAddRemoveBenchmarks
{
[Params(2, 6, 10, 20, 30)]
public int PropertyCount;
[Params(false, true)]
public bool UseShuffledProperties;
public AvaloniaProperty[] Properties => UseShuffledProperties ? MockProperties.ShuffledProperties : MockProperties.LinearProperties;
[Benchmark]
[Arguments(false)]
[Arguments(true)]
public void AddValue(bool isInitializing)
{
var store = new AvaloniaPropertyValueStore<object> { IsInitializing = isInitializing };
for (int i = 0; i < PropertyCount; i++)
{
store.AddValue(Properties[i], null);
}
}
[Benchmark]
[Arguments(false)]
[Arguments(true)]
public void AddAndRemoveValue(bool isInitializing)
{
var store = new AvaloniaPropertyValueStore<object> { IsInitializing = isInitializing };
for (int i = 0; i < PropertyCount; i++)
{
store.AddValue(Properties[i], null);
}
for (int i = PropertyCount - 1; i >= 0; i--)
{
store.Remove(Properties[i]);
}
}
[Benchmark]
[Arguments(false)]
[Arguments(true)]
public void AddAndRemoveValueInterleaved(bool isInitializing)
{
var store = new AvaloniaPropertyValueStore<object> { IsInitializing = isInitializing };
for (int i = 0; i < PropertyCount; i++)
{
store.AddValue(Properties[i], null);
store.Remove(Properties[i]);
}
}
[Benchmark]
public void AddValue_Old()
{
var store = new AvaloniaPropertyValueStoreOld<object>();
for (int i = 0; i < PropertyCount; i++)
{
store.AddValue(Properties[i], null);
}
}
[Benchmark]
public void AddAndRemoveValue_Old()
{
var store = new AvaloniaPropertyValueStoreOld<object>();
for (int i = 0; i < PropertyCount; i++)
{
store.AddValue(Properties[i], null);
}
for (int i = PropertyCount - 1; i >= 0; i--)
{
store.Remove(Properties[i]);
}
}
[Benchmark]
public void AddAndRemoveValueInterleaved_Old()
{
var store = new AvaloniaPropertyValueStoreOld<object>();
for (int i = 0; i < PropertyCount; i++)
{
store.AddValue(Properties[i], null);
store.Remove(Properties[i]);
}
}
}

33
tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs

@ -39,6 +39,39 @@ namespace Avalonia.Benchmarks.Layout
_root.LayoutManager.ExecuteLayoutPass();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateControl()
{
var control = new Control();
_root.Child = control;
_root.LayoutManager.ExecuteLayoutPass();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateDecorator()
{
var control = new Decorator();
_root.Child = control;
_root.LayoutManager.ExecuteLayoutPass();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateScrollViewer()
{
var control = new ScrollViewer();
_root.Child = control;
_root.LayoutManager.ExecuteLayoutPass();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateButton()

Loading…
Cancel
Save