8 changed files with 561 additions and 109 deletions
@ -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]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue