Browse Source

Merge pull request #2362 from MarchingCube/perf-setandraise

[perf] Improve performance of AvaloniaObject SetAndRaise method.
pull/2766/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
ee5ca02e7c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 72
      src/Avalonia.Base/AvaloniaObject.cs
  2. 9
      src/Avalonia.Base/IPriorityValueOwner.cs
  3. 34
      src/Avalonia.Base/PriorityValue.cs
  4. 150
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  5. 206
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  6. 152
      src/Avalonia.Base/ValueStore.cs
  7. 2
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  8. 2
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  9. 65
      tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
  10. 3
      tests/Avalonia.Benchmarks/Base/Properties.cs

72
src/Avalonia.Base/AvaloniaObject.cs

@ -466,7 +466,7 @@ namespace Avalonia
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged(
protected internal void RaisePropertyChanged(
AvaloniaProperty property,
object oldValue,
object newValue,
@ -508,45 +508,6 @@ namespace Avalonia
}
}
/// <summary>
/// A callback type for encapsulating complex logic for setting direct properties.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="value">The value to which to set the property.</param>
/// <param name="field">The backing field for the property.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </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="setterCallback">A callback called to actually set the value to the backing field.</param>
/// <param name="value">The value.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(
AvaloniaProperty<T> property,
ref T field,
SetAndRaiseCallback<T> setterCallback,
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return Values.Setter.SetAndNotify(
property,
ref field,
(object update, ref T backing, Action<Action> notify) =>
{
setterCallback((T)update, ref backing, notify);
return true;
},
value);
}
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
@ -561,32 +522,15 @@ namespace Avalonia
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
return SetAndRaise(
property,
ref field,
(T val, ref T backing, Action<Action> notifyWrapper)
=> SetAndRaiseCore(property, ref backing, val, notifyWrapper),
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;
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
return true;
return setter.SetAndNotify(this, property, ref field, value);
}
/// <summary>

9
src/Avalonia.Base/IPriorityValueOwner.cs

@ -29,6 +29,13 @@ namespace Avalonia
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
/// <summary>
/// Returns deferred setter for given non-direct property.
/// </summary>
/// <param name="property">Property.</param>
/// <returns>Deferred setter for given property.</returns>
DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
/// <summary>
/// Logs a binding error.
/// </summary>
@ -40,7 +47,5 @@ namespace Avalonia
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();
DeferredSetter<object> Setter { get; }
}
}

34
src/Avalonia.Base/PriorityValue.cs

@ -30,7 +30,9 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
@ -50,6 +52,7 @@ namespace Avalonia
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
_setAndNotifyCallback = SetAndNotify;
}
/// <summary>
@ -242,22 +245,22 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
Owner.Setter.SetAndNotify(Property,
ref _value,
UpdateCore,
(value, priority));
}
var newValue = (value, priority);
if (newValue == _value)
{
return;
}
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
if (_setter == null)
{
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
}
private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,
Action<Action> notify)
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;
@ -286,7 +289,7 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
Owner?.Changed(Property, ValuePriority, old, Value);
}
if (notification != null)
@ -305,7 +308,6 @@ namespace Avalonia
val,
val?.GetType());
}
return true;
}
}
}

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

@ -0,0 +1,150 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
namespace Avalonia.Utilities
{
/// <summary>
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
{
private Entry[] _entries;
public AvaloniaPropertyValueStore()
{
// 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 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 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;
}
}
}

206
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -1,168 +1,122 @@
using System;
using System.Collections.Generic;
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Utilities
{
/// <summary>
/// Callback invoked when deferred setter wants to set a value.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
/// <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>
class DeferredSetter<TSetRecord>
internal sealed class DeferredSetter<TSetRecord>
{
private struct NotifyDisposable : IDisposable
private readonly SingleOrQueue<TSetRecord> _pendingValues;
private bool _isNotifying;
public DeferredSetter()
{
private readonly SettingStatus status;
_pendingValues = new SingleOrQueue<TSetRecord>();
}
internal NotifyDisposable(SettingStatus status)
{
this.status = status;
status.Notifying = true;
}
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
{
var old = backing;
public void Dispose()
{
status.Notifying = false;
}
backing = value;
source.RaisePropertyChanged(property, old, value);
}
/// <summary>
/// Information on current setting/notification status of a property.
/// </summary>
private class SettingStatus
public bool SetAndNotify(
AvaloniaObject source,
AvaloniaProperty<TSetRecord> property,
ref TSetRecord backing,
TSetRecord value)
{
public bool Notifying { get; set; }
private SingleOrQueue<TSetRecord> pendingValues;
public SingleOrQueue<TSetRecord> PendingValues
if (!_isNotifying)
{
get
using (new NotifyDisposable(this))
{
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
SetAndRaisePropertyChanged(source, property, ref backing, value);
}
}
}
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
}
}
}
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
return true;
}
return status;
_pendingValues.Enqueue(value);
return false;
}
/// <summary>
/// Mark the property as currently notifying.
/// </summary>
/// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(AvaloniaProperty property)
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value)
where TValue : TSetRecord
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));
SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
}
if (!_isNotifying)
{
using (new NotifyDisposable(this))
{
setAndNotifyCallback(property, ref backing, value);
}
/// <summary>
/// Check if the property is currently notifying listeners.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(AvaloniaProperty property)
=> SetRecords.TryGetValue(property, out var value) && value.Notifying;
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
}
}
}
/// <summary>
/// Add a pending assignment for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param>
private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));
return true;
}
GetOrCreateStatus(property).PendingValues.Enqueue(value);
}
_pendingValues.Enqueue(value);
/// <summary>
/// Checks if there are any pending assignments for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(AvaloniaProperty property)
{
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
return false;
}
/// <summary>
/// Gets the first pending assignment for the property.
/// Disposable that marks the property as currently notifying.
/// When disposed, marks the property as done notifying.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
private readonly struct NotifyDisposable : IDisposable
{
return GetOrCreateStatus(property).PendingValues.Dequeue();
}
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
private readonly DeferredSetter<TSetRecord> _setter;
/// <summary>
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
/// </summary>
/// <param name="property">The property to set.</param>
/// <param name="backing">The backing field for the property</param>
/// <param name="setterCallback">
/// A callback that actually sets the property.
/// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
AvaloniaProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
if (!IsNotifying(property))
internal NotifyDisposable(DeferredSetter<TSetRecord> setter)
{
bool updated = false;
if (!object.Equals(value, backing))
{
updated = setterCallback(value, ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
while (HasPendingSet(property))
{
updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
return updated;
_setter = setter;
_setter._isNotifying = true;
}
else if(!object.Equals(value, backing))
public void Dispose()
{
AddPendingSet(property, value);
_setter._isNotifying = false;
}
return false;
}
}
}

152
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 AvaloniaPropertyValueStore<object> _propertyValues;
private readonly AvaloniaPropertyValueStore<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 AvaloniaPropertyValueStore<object>();
_deferredSetters = new AvaloniaPropertyValueStore<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.SetValue(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(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.SetValue(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.SetValue(property, priorityValue);
}
}
}
@ -84,14 +78,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
AddValueInternal(property, Validate(property, value));
_propertyValues.AddValue(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(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,28 @@ namespace Avalonia
return value;
}
private DeferredSetter<object> _deferredSetter;
public DeferredSetter<object> Setter
private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
{
get
if (_deferredSetters.TryGetValue(property, out var deferredSetter))
{
return _deferredSetter ??
(_deferredSetter = new DeferredSetter<object>());
return (DeferredSetter<T>)deferredSetter;
}
}
private bool TryGetValue(AvaloniaProperty property, out object value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = null;
return false;
}
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 DeferredSetter<T>();
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_deferredSetters.AddValue(property, newDeferredSetter);
_entries = entries;
return newDeferredSetter;
}
private void SetValueInternal(AvaloniaProperty property, object value)
public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
return GetDeferredSetter<object>(property);
}
private (int, bool) TryFindEntry(int propertyId)
public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
{
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);
return GetDeferredSetter<T>(property);
}
}
}

2
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests
private static Mock<IPriorityValueOwner> GetMockOwner()
{
var owner = new Mock<IPriorityValueOwner>();
owner.SetupGet(o => o.Setter).Returns(new DeferredSetter<object>());
owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny<AvaloniaProperty>())).Returns(new DeferredSetter<object>());
return owner;
}
}

2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -18,7 +18,7 @@
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.10.14" />
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />

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

@ -0,0 +1,65 @@
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 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 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