From 39fba5c380cdf06e82780916942bc5b7b633a6ec Mon Sep 17 00:00:00 2001 From: Alberto Aldegheri Date: Sun, 18 Jan 2026 12:47:56 +0100 Subject: [PATCH 1/3] BindableObject property access micro-optimizations --- src/Controls/src/Core/BindableObject.cs | 85 +++++++------------ src/Controls/src/Core/BindableProperty.cs | 6 ++ .../Benchmarks/BindableObjectBenchmarker.cs | 38 +++++++++ 3 files changed, 74 insertions(+), 55 deletions(-) create mode 100644 src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs index f205dbe7e611..9257b1ec4563 100644 --- a/src/Controls/src/Core/BindableObject.cs +++ b/src/Controls/src/Core/BindableObject.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Dispatching; @@ -38,8 +39,8 @@ public BindableObject() } internal ushort _triggerCount = 0; - internal Dictionary _triggerSpecificity = new Dictionary(); - readonly Dictionary _properties = new Dictionary(4); + internal Dictionary _triggerSpecificity = new(); + readonly Dictionary _properties = new(4); bool _applying; WeakReference _inheritedContext; @@ -172,66 +173,24 @@ public object GetValue(BindableProperty property) return context == null ? property.DefaultValue : context.Values.GetValue(); } - internal LocalValueEnumerator GetLocalValueEnumerator() => new LocalValueEnumerator(this); - - internal sealed class LocalValueEnumerator : IEnumerator - { - Dictionary.Enumerator _propertiesEnumerator; - internal LocalValueEnumerator(BindableObject bindableObject) => _propertiesEnumerator = bindableObject._properties.GetEnumerator(); - - object IEnumerator.Current => Current; - public LocalValueEntry Current { get; private set; } - - public bool MoveNext() - { - if (_propertiesEnumerator.MoveNext()) - { - Current = new LocalValueEntry(_propertiesEnumerator.Current.Key, _propertiesEnumerator.Current.Value.Values.GetValue(), _propertiesEnumerator.Current.Value.Attributes); - return true; - } - return false; - } - - public void Dispose() => _propertiesEnumerator.Dispose(); - - void IEnumerator.Reset() - { - ((IEnumerator)_propertiesEnumerator).Reset(); - Current = null; - } - } - - internal sealed class LocalValueEntry - { - internal LocalValueEntry(BindableProperty property, object value, BindableContextAttributes attributes) - { - Property = property; - Value = value; - Attributes = attributes; - } - - public BindableProperty Property { get; } - public object Value { get; } - public BindableContextAttributes Attributes { get; } - } - internal (bool IsSet, T Value)[] GetValues(BindableProperty[] propArray) { - Dictionary properties = _properties; + var properties = _properties; var resultArray = new (bool IsSet, T Value)[propArray.Length]; for (int i = 0; i < propArray.Length; i++) { - if (properties.TryGetValue(propArray[i], out var context)) + ref var result = ref resultArray[i]; + if (properties.TryGetValue(propArray[i].InternalId, out var context)) { var pair = context.Values.GetSpecificityAndValue(); - resultArray[i].IsSet = pair.Key != SetterSpecificity.DefaultValue; - resultArray[i].Value = (T)pair.Value; + result.IsSet = pair.Key != SetterSpecificity.DefaultValue; + result.Value = (T)pair.Value; } else { - resultArray[i].IsSet = false; - resultArray[i].Value = default(T); + result.IsSet = false; + result.Value = default(T); } } @@ -736,7 +695,7 @@ static void BindingContextPropertyChanged(BindableObject bindable, object oldval } [MethodImpl(MethodImplOptions.AggressiveInlining)] - BindablePropertyContext CreateAndAddContext(BindableProperty property) + BindablePropertyContext CreateContext(BindableProperty property) { var defaultValueCreator = property.DefaultValueCreator; var context = new BindablePropertyContext { Property = property }; @@ -745,15 +704,31 @@ BindablePropertyContext CreateAndAddContext(BindableProperty property) if (defaultValueCreator != null) context.Attributes = BindableContextAttributes.IsDefaultValueCreated; - _properties.Add(property, context); return context; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property, out var result) ? result : null; + internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property.InternalId, out var result) ? result : null; [MethodImpl(MethodImplOptions.AggressiveInlining)] - BindablePropertyContext GetOrCreateContext(BindableProperty property) => GetContext(property) ?? CreateAndAddContext(property); + BindablePropertyContext GetOrCreateContext(BindableProperty property) + { +#if NETSTANDARD2_0 + var context = GetContext(property); + if (context is null) + { + context = CreateContext(property); + _properties.Add(property.InternalId, context); + } +#else + ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_properties, property.InternalId, out var exists); + if (!exists) + { + context = CreateContext(property); + } +#endif + return context; + } void RemoveBinding(BindableProperty property, BindablePropertyContext context, SetterSpecificity specificity) { diff --git a/src/Controls/src/Core/BindableProperty.cs b/src/Controls/src/Core/BindableProperty.cs index ed771a2c94c8..43e25867c81a 100644 --- a/src/Controls/src/Core/BindableProperty.cs +++ b/src/Controls/src/Core/BindableProperty.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; +using System.Threading; using Microsoft.Maui.Controls.Xaml; using Microsoft.Maui.Graphics; using Microsoft.Maui.Graphics.Converters; @@ -181,6 +182,9 @@ public sealed class BindableProperty /// public static readonly object UnsetValue = new object(); + private static int _nextInternalId = int.MinValue; + internal readonly int InternalId; + BindableProperty(string propertyName, [DynamicallyAccessedMembers(ReturnTypeMembers)] Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode = BindingMode.OneWay, ValidateValueDelegate validateValue = null, BindingPropertyChangedDelegate propertyChanged = null, BindingPropertyChangingDelegate propertyChanging = null, CoerceValueDelegate coerceValue = null, BindablePropertyBindingChanging bindingChanging = null, bool isReadOnly = false, CreateDefaultValueDelegate defaultValueCreator = null) @@ -191,6 +195,8 @@ public sealed class BindableProperty throw new ArgumentNullException(nameof(returnType)); if (declaringType is null) throw new ArgumentNullException(nameof(declaringType)); + + InternalId = Interlocked.Increment(ref _nextInternalId); // don't use Enum.IsDefined as its redonkulously expensive for what it does if (defaultBindingMode != BindingMode.Default && defaultBindingMode != BindingMode.OneWay && defaultBindingMode != BindingMode.OneWayToSource && defaultBindingMode != BindingMode.TwoWay && defaultBindingMode != BindingMode.OneTime) diff --git a/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs b/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs new file mode 100644 index 000000000000..f768a4944c57 --- /dev/null +++ b/src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.cs @@ -0,0 +1,38 @@ +using System.Linq; +using BenchmarkDotNet.Attributes; +using Microsoft.Maui.Controls; + +namespace Microsoft.Maui.Benchmarks +{ + [MemoryDiagnoser] + public class BindableObjectBenchmarker + { + BindableProperty[] _properties; + + [Params(1, 3, 8, 15, 30, 50)] + public int PropertiesToSet { get; set; } + + [GlobalSetup] + public void Setup() + { + _properties = Enumerable.Range(0, PropertiesToSet) + .Select(i => BindableProperty.Create($"Property{i}", typeof(int), typeof(BindableObject), -1)) + .ToArray(); + } + + private class Bindable : BindableObject {} + + [Benchmark] + public void SetsAndReadsProperties() + { + var bindable = new Bindable(); + + var count = _properties.Length; + for (int i = 0; i < count; i++) + { + bindable.SetValue(_properties[i], i); + _ = bindable.GetValue(_properties[i]); + } + } + } +} \ No newline at end of file From 571287934cbebd43bdef4f813468d48a38bda2a2 Mon Sep 17 00:00:00 2001 From: Alberto Aldegheri Date: Sun, 18 Jan 2026 15:52:59 +0100 Subject: [PATCH 2/3] Fix build --- src/Controls/src/Core/BindableObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs index 9257b1ec4563..175a10390674 100644 --- a/src/Controls/src/Core/BindableObject.cs +++ b/src/Controls/src/Core/BindableObject.cs @@ -713,7 +713,7 @@ BindablePropertyContext CreateContext(BindableProperty property) [MethodImpl(MethodImplOptions.AggressiveInlining)] BindablePropertyContext GetOrCreateContext(BindableProperty property) { -#if NETSTANDARD2_0 +#if NETSTANDARD var context = GetContext(property); if (context is null) { From 5e6e3430161ebd3c3901ee3dd933938101342538 Mon Sep 17 00:00:00 2001 From: Alberto Aldegheri Date: Mon, 19 Jan 2026 10:07:08 +0100 Subject: [PATCH 3/3] Simplify value assignment logic in BindableObject Removed unnecessary else block that sets default values. --- src/Controls/src/Core/BindableObject.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs index 175a10390674..5fd53e0680ca 100644 --- a/src/Controls/src/Core/BindableObject.cs +++ b/src/Controls/src/Core/BindableObject.cs @@ -187,11 +187,6 @@ public object GetValue(BindableProperty property) result.IsSet = pair.Key != SetterSpecificity.DefaultValue; result.Value = (T)pair.Value; } - else - { - result.IsSet = false; - result.Value = default(T); - } } return resultArray;