Browse Source

!R Rework deferred setter for better performance.

pull/2362/head
Dariusz Komosinski 8 years ago
committed by Dariusz Komosinski
parent
commit
24f208bab1
  1. 91
      src/Avalonia.Base/AvaloniaObject.cs
  2. 143
      src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs
  3. 84
      src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs
  4. 153
      src/Avalonia.Base/ValueStore.cs
  5. 82
      tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
  6. 3
      tests/Avalonia.Benchmarks/Base/Properties.cs

91
src/Avalonia.Base/AvaloniaObject.cs

@ -561,6 +561,7 @@ namespace Avalonia
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
return SetAndRaise(
property,
ref field,
@ -568,19 +569,89 @@ namespace Avalonia
=> SetAndRaiseCore(property, ref backing, val, notifyWrapper),
value);
}
/// <summary>
/// Default setter handler that will set backing field and raise notification.
/// </summary>
private sealed class DefaultSetterHandler<T> : DeferredSetterOptimized<T>.ISetterHandler
{
public static readonly DefaultSetterHandler<T> Instance = new DefaultSetterHandler<T>();
public bool Update(AvaloniaObject source, AvaloniaProperty<T> property, ref T backing, T value)
{
var old = backing;
backing = value;
source.RaisePropertyChanged(property, old, value);
return true;
}
}
/// <summary>
/// Default assignment logic for SetAndRaise.
/// Setter handler that will run custom user callback.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
private sealed class CallbackSetterHandler<T> : DeferredSetterOptimized<T>.ISetterHandler
{
private readonly SetAndRaiseCallback<T> _callback;
public CallbackSetterHandler(SetAndRaiseCallback<T> callback)
{
_callback = callback;
}
public bool Update(AvaloniaObject source, AvaloniaProperty<T> property, ref T backing, T value)
{
_callback(value, ref backing, notification => notification());
return true;
}
}
protected bool SetAndRaiseOptimized<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
DeferredSetterOptimized<T> setter = Values.GetDeferredSetter(property);
return setter.SetAndNotify(this, property, DefaultSetterHandler<T>.Instance, ref field, value);
}
protected bool SetAndRaiseOptimized<T>(
AvaloniaProperty<T> property,
ref T field,
SetAndRaiseCallback<T> setterCallback,
T value)
{
VerifyAccess();
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
DeferredSetterOptimized<T> setter = Values.GetDeferredSetter(property);
return setter.SetAndNotify(this, property, new CallbackSetterHandler<T>(setterCallback) , ref field, value);
}
/// <summary>
/// Default assignment logic for SetAndRaise.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
{
var old = field;
field = value;

143
src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Utilities
{
internal sealed class AvaloniaPropertyCollection<TValue>
{
private Entry[] _entries;
public AvaloniaPropertyCollection()
{
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
}
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, out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = default;
return false;
}
value = _entries[index].Value;
return true;
}
public void AddValueInternal(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 SetValueInternal(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
public Dictionary<AvaloniaProperty, TValue> ToDictionary()
{
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
private struct Entry
{
internal int PropertyId;
internal TValue Value;
}
}
}

84
src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs

@ -0,0 +1,84 @@
using System;
namespace Avalonia.Utilities
{
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
internal sealed class DeferredSetterOptimized<TSetRecord>
{
private bool _isNotifying;
private readonly SingleOrQueue<TSetRecord> _pendingValues;
public DeferredSetterOptimized()
{
_pendingValues = new SingleOrQueue<TSetRecord>();
}
public bool SetAndNotify(
AvaloniaObject source,
AvaloniaProperty<TSetRecord> property,
ISetterHandler handler,
ref TSetRecord backing,
TSetRecord value)
{
if (!_isNotifying)
{
bool updated;
using (new NotifyDisposable(this))
{
updated = handler.Update(source, property, ref backing, value);
}
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue());
}
}
}
return updated;
}
_pendingValues.Enqueue(value);
return false;
}
/// <summary>
/// Disposable that marks the property as currently notifying.
/// When disposed, marks the property as done notifying.
/// </summary>
private readonly struct NotifyDisposable : IDisposable
{
private readonly DeferredSetterOptimized<TSetRecord> _setter;
internal NotifyDisposable(DeferredSetterOptimized<TSetRecord> setter)
{
_setter = setter;
_setter._isNotifying = true;
}
public void Dispose()
{
_setter._isNotifying = false;
}
}
public interface ISetterHandler
{
bool Update(
AvaloniaObject source,
AvaloniaProperty<TSetRecord> property,
ref TSetRecord backing,
TSetRecord value);
}
}
}

153
src/Avalonia.Base/ValueStore.cs

@ -7,21 +7,15 @@ namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private struct Entry
{
internal int PropertyId;
internal object Value;
}
private readonly AvaloniaPropertyCollection<object> _propertyValues;
private readonly AvaloniaPropertyCollection<object> _deferredSetters;
private readonly AvaloniaObject _owner;
private Entry[] _entries;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
_propertyValues = new AvaloniaPropertyCollection<object>();
_deferredSetters = new AvaloniaPropertyCollection<object>();
}
public IDisposable AddBinding(
@ -31,7 +25,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -39,13 +33,13 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValueInternal(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValueInternal(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
@ -55,7 +49,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -63,7 +57,7 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
SetValueInternal(property, Validate(property, value));
_propertyValues.SetValueInternal(property, Validate(property, value));
Changed(property, priority, v, value);
return;
}
@ -71,7 +65,7 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValueInternal(property, priorityValue);
}
}
}
@ -84,14 +78,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
AddValueInternal(property, Validate(property, value));
_propertyValues.AddValueInternal(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValueInternal(property, priorityValue);
}
}
@ -110,14 +104,9 @@ namespace Avalonia
public IDictionary<AvaloniaProperty, object> GetSetValues()
{
var dict = new Dictionary<AvaloniaProperty, object>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
return _propertyValues.ToDictionary();
}
public void LogError(AvaloniaProperty property, Exception e)
{
_owner.LogBindingError(property, e);
@ -127,7 +116,7 @@ namespace Avalonia
{
var result = AvaloniaProperty.UnsetValue;
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
@ -137,12 +126,12 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
@ -152,7 +141,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
@ -189,113 +178,29 @@ namespace Avalonia
return value;
}
private DeferredSetter<object> _deferredSetter;
public DeferredSetter<object> Setter
{
get
{
return _deferredSetter ??
(_deferredSetter = new DeferredSetter<object>());
}
}
private bool TryGetValue(AvaloniaProperty property, out object value)
public DeferredSetterOptimized<T> GetDeferredSetter<T>(AvaloniaProperty<T> property)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
if (_deferredSetters.TryGetValue(property, out var deferredSetter))
{
value = null;
return false;
return (DeferredSetterOptimized<T>)deferredSetter;
}
value = _entries[index].Value;
return true;
}
private void AddValueInternal(AvaloniaProperty property, object 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);
}
var newDeferredSetter = new DeferredSetterOptimized<T>();
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_deferredSetters.AddValueInternal(property, newDeferredSetter);
_entries = entries;
return newDeferredSetter;
}
private void SetValueInternal(AvaloniaProperty property, object value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
private DeferredSetter<object> _deferredSetter;
private (int, bool) TryFindEntry(int propertyId)
public DeferredSetter<object> Setter
{
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
get
{
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 _deferredSetter ??
(_deferredSetter = new DeferredSetter<object>());
}
return (0, false);
}
}
}

82
tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs

@ -0,0 +1,82 @@
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class DirectPropertyBenchmark
{
[Benchmark(Baseline = true)]
public void SetAndRaiseOriginal()
{
var obj = new DirectClass();
for (var i = 0; i < 100; ++i)
{
obj.IntValue += 1;
}
}
[Benchmark]
public void SetAndRaiseOptimized()
{
var obj = new DirectClass();
for (var i = 0; i < 100; ++i)
{
obj.IntValueOptimized += 1;
}
}
[Benchmark]
public void SetAndRaiseSimple()
{
var obj = new DirectClass();
for (var i = 0; i < 100; ++i)
{
obj.IntValueSimple += 1;
}
}
class DirectClass : AvaloniaObject
{
private int _intValue;
public static readonly DirectProperty<DirectClass, int> IntValueProperty =
AvaloniaProperty.RegisterDirect<DirectClass, int>(nameof(IntValue),
o => o.IntValue,
(o, v) => o.IntValue = v);
public int IntValue
{
get => _intValue;
set => SetAndRaise(IntValueProperty, ref _intValue, value);
}
public int IntValueOptimized
{
get => _intValue;
set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value);
}
public int IntValueSimple
{
get => _intValue;
set
{
VerifyAccess();
if (_intValue == value)
{
return;
}
var old = _intValue;
_intValue = value;
RaisePropertyChanged(IntValueProperty, old, _intValue);
}
}
}
}
}

3
tests/Avalonia.Benchmarks/Base/Properties.cs

@ -1,5 +1,4 @@
using System;
using System.Reactive.Subjects;
using System.Reactive.Subjects;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base

Loading…
Cancel
Save