Skip to content

Improve TypedBinding performance by ~29%#33656

Merged
StephaneDelcroix merged 3 commits intomainfrom
dev/stdelc/typedbindingimprovements
Feb 12, 2026
Merged

Improve TypedBinding performance by ~29%#33656
StephaneDelcroix merged 3 commits intomainfrom
dev/stdelc/typedbindingimprovements

Conversation

@StephaneDelcroix
Copy link
Copy Markdown
Contributor

@StephaneDelcroix StephaneDelcroix commented Jan 21, 2026

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

This PR improves TypedBinding and SourceGeneratedBinding performance for value propagation scenarios (binding set once, values updated many times).

Optimizations Applied

  1. Cache Apply() delegate - Avoids lambda allocation on every property change
  2. Cache binding mode - Avoids GetRealizedMode() call on every Apply
  3. Skip Subscribe() loop - Only subscribes to INPC once per binding lifetime
  4. Cache default value - Avoids repeated GetDefaultValue() calls
  5. Skip TryConvert - When TProperty matches property.ReturnType
  6. Cache isTSource - Type check result (source type doesn't change)

Benchmark Results

Before (net10.0 baseline):

Method Mean Ratio Allocated
SetValue 20.77 ns 1.00 40 B
Binding 52.03 ns 2.50 128 B
TypedBinding 47.47 ns 2.28 128 B
SourceGeneratedBinding 45.45 ns 2.19 128 B

After (this PR):

Method Mean Ratio Allocated
SetValue 20.64 ns 1.00 40 B
Binding 52.00 ns 2.52 128 B
TypedBinding 32.90 ns 1.59 64 B
SourceGeneratedBinding 34.10 ns 1.65 64 B

Summary:

Metric Before After Improvement
TypedBinding 47.47 ns (2.28x) 32.90 ns (1.59x) 31% faster
SourceGeneratedBinding 45.45 ns (2.19x) 34.10 ns (1.65x) 25% faster
Memory per operation 128 B 64 B 50% less

New Benchmark

Adds BindingComparisonBenchmarker to Core.Benchmarks for comparing SetValue, Binding, TypedBinding, and SourceGeneratedBinding performance.

cd src/Core/tests/Benchmarks
dotnet run -c Release -- --filter "*BindingComparison*"

Optimizations applied:
- Cache Apply() delegate to avoid lambda allocation on every property change
- Cache binding mode after first calculation
- Skip Subscribe() loop if already subscribed
- Cache default value to avoid repeated GetDefaultValue() calls
- Skip TryConvert when TProperty matches property.ReturnType
- Cache isTSource type check result
- Cache dispatcher reference to avoid cast on every call

Also adds BindingComparisonBenchmarker for measuring SetValue vs Binding vs TypedBinding vs SourceGeneratedBinding performance.

Results (100 value updates after binding once):
- SourceGeneratedBinding: 5.05 µs → 3.65 µs (28% faster, 1.57x of SetValue)
- TypedBinding: 5.19 µs → 3.80 µs (27% faster, 1.64x of SetValue)
- Memory: 12.5 KB → 6.25 KB (50% reduction)
Copilot AI review requested due to automatic review settings January 21, 2026 20:36
@StephaneDelcroix StephaneDelcroix added this to the .NET 11.0-preview1 milestone Jan 21, 2026
@StephaneDelcroix StephaneDelcroix added the legacy-area-perf Startup / Runtime performance label Jan 21, 2026
@StephaneDelcroix StephaneDelcroix changed the title Improve TypedBinding performance by ~27% [PERF] Improve TypedBinding performance by ~27% Jan 21, 2026
@StephaneDelcroix StephaneDelcroix removed the legacy-area-perf Startup / Runtime performance label Jan 21, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes TypedBinding and SourceGeneratedBinding performance for value propagation scenarios (where bindings are set once and values are updated many times). The optimizations include caching delegates, binding modes, default values, type checks, and dispatchers to reduce allocations and repeated computations.

Changes:

  • Added new benchmark BindingComparisonBenchmarker to measure binding performance
  • Implemented 7 performance optimizations in TypedBinding.cs with cached fields
  • Reported 27-28% performance improvement and 50% memory reduction based on benchmark results

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/Core/tests/Benchmarks/Benchmarks/BindingComparisonBenchmarker.cs New benchmark comparing SetValue, Binding, TypedBinding, and SourceGeneratedBinding performance across 100 iterations
src/Controls/src/Core/TypedBinding.cs Performance optimizations including cached mode, default values, type checks, dispatcher, Apply delegate, and skip-convert flag with corresponding state management in Unapply

Comment on lines +309 to +319
// Use cached type check after first apply (source type doesn't change)
var isTSource = _isTSource;
if (!_hasDefaultValue)
{
isTSource = sourceObject is TSource;
_isTSource = isTSource;
if (!isTSource && sourceObject is not null)
{
BindingDiagnostics.SendBindingFailure(this, "Binding", $"Mismatch between the specified x:DataType ({typeof(TSource)}) and the current binding context ({sourceObject.GetType()}).");
}
}
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cached type check _isTSource is never reset in Unapply() (line 292-296 resets other cached values but not _isTSource). This causes incorrect behavior when ApplyCore is called with a different source object.

For example, line 520 calls ApplyCore(null, ...) directly without calling Unapply() first. In this case, the condition if (!_hasDefaultValue) on line 311 evaluates to false (since default value was already cached), so the type check is skipped entirely and the stale _isTSource value is used. This will cause incorrect behavior when the source changes from a valid TSource object to null.

Copilot uses AI. Check for mistakes.
Comment on lines +583 to +590
// Cache dispatcher to avoid cast + property access on every call
if (!_dispatcherCached)
{
_cachedDispatcher = (sender as BindableObject)?.Dispatcher;
_dispatcherCached = true;
}

_cachedDispatcher.DispatchIfRequired(_applyAction);
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dispatcher caching implementation is conceptually incorrect. The dispatcher should come from the binding target (as seen in BindingExpression.cs line 731 where it uses the target's dispatcher), but this code caches it from the source object.

In typical binding scenarios, the source is a view model (POCO with INotifyPropertyChanged) and the target is a BindableObject (UI element). Since the source is usually not a BindableObject, the cached dispatcher will be null in most cases. While DispatchIfRequired handles null via EnsureDispatcher, this defeats the optimization purpose since EnsureDispatcher is called on every property change anyway.

Additionally, when the PropertyChangedProxy is resubscribed to a different source object (via the Part setter at line 547-559), the cached dispatcher is never updated, which could lead to using an incorrect dispatcher if the sources have different dispatchers or BindableObject status.

Copilot uses AI. Check for mistakes.
_cachedMode = BindingMode.Default;
_hasDefaultValue = false;
_cachedDefaultValue = null;
_skipConvert = false;
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field _isTSource should be reset in Unapply() along with the other cached values. Add _isTSource = false; to properly reset the cached type check result.

Suggested change
_skipConvert = false;
_skipConvert = false;
_isTSource = false;

Copilot uses AI. Check for mistakes.
- Reset _isTSource in Unapply() to handle BindingContext type changes
- Remove ineffective dispatcher caching (sender is usually ViewModel, not BindableObject)
- Add clarifying comment about dispatcher null handling
Copy link
Copy Markdown
Contributor Author

@StephaneDelcroix StephaneDelcroix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the review comments:

Comment 1 & 3 (_isTSource not reset): This was already fixed in commit 7a8b3c2. The Unapply() method now includes _isTSource = false; at line 296.

Comment 2 (Dispatcher caching): Marked as outdated - this was removed in the same commit after identifying that the source is typically a ViewModel (not BindableObject), making the cached dispatcher null in most cases.

All three review comments have been addressed in the current code.

@StephaneDelcroix StephaneDelcroix changed the base branch from net10.0 to main January 22, 2026 12:20
@StephaneDelcroix StephaneDelcroix merged commit acc2476 into main Feb 12, 2026
24 of 28 checks passed
@StephaneDelcroix StephaneDelcroix deleted the dev/stdelc/typedbindingimprovements branch February 12, 2026 21:46
StephaneDelcroix added a commit that referenced this pull request Feb 27, 2026
Squashed from 26 commits (dev/srozsival/binding-improvements-worker4):
- Remove ForSingleNestingLevel factory method
- Add new TypedBinding constructor API: TypedBinding(getter, setter, handlersCount, handlers)
- Add IPropertyChangeHandler abstraction for handler strategy pattern
- Add LegacyPropertyChangeHandler (backward compat with old Tuple-based API)
- Add PropertyChangeHandler (new efficient handler using INotifyPropertyChanged)
- Unify TypedBinding and TypedBinding2 designs into single TypedBinding class
- Update BindingSourceGen to emit new TypedBinding constructor calls
- Update CompiledBindingMarkup source generator for new API
- Ban old ForSingleNestingLevel usage in BannedSymbols.txt
- Add TypedBinding2UnitTests.cs (~2000 lines of new tests)
- Update benchmarks to compare TypedBinding variants

Note: Rebased onto net11.0 as a squash to avoid conflicts with intermediate
states. net11.0 already has the ~29% ApplyCore perf improvements (PR #33656);
this PR adds the new flexible handler API on top.

Co-authored-by: Simon Rozsival <simon@rozsival.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TamilarasanSF4853 pushed a commit to TamilarasanSF4853/maui that referenced this pull request Mar 2, 2026
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

## Description

This PR improves TypedBinding and SourceGeneratedBinding performance for
value propagation scenarios (binding set once, values updated many
times).

### Optimizations Applied

1. **Cache Apply() delegate** - Avoids lambda allocation on every
property change
2. **Cache binding mode** - Avoids `GetRealizedMode()` call on every
Apply
3. **Skip Subscribe() loop** - Only subscribes to INPC once per binding
lifetime
4. **Cache default value** - Avoids repeated `GetDefaultValue()` calls
5. **Skip TryConvert** - When `TProperty` matches `property.ReturnType`
6. **Cache isTSource** - Type check result (source type doesn't change)

### Benchmark Results

**Before (net10.0 baseline):**

| Method                 | Mean     | Ratio | Allocated |
|----------------------- |---------:|------:|----------:|
| SetValue               | 20.77 ns |  1.00 |      40 B |
| Binding                | 52.03 ns |  2.50 |     128 B |
| TypedBinding           | 47.47 ns |  2.28 |     128 B |
| SourceGeneratedBinding | 45.45 ns |  2.19 |     128 B |

**After (this PR):**

| Method                 | Mean     | Ratio | Allocated |
|----------------------- |---------:|------:|----------:|
| SetValue               | 20.64 ns |  1.00 |      40 B |
| Binding                | 52.00 ns |  2.52 |     128 B |
| TypedBinding           | 32.90 ns |  1.59 |      64 B |
| SourceGeneratedBinding | 34.10 ns |  1.65 |      64 B |

**Summary:**

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| TypedBinding | 47.47 ns (2.28x) | **32.90 ns (1.59x)** | **31%
faster** |
| SourceGeneratedBinding | 45.45 ns (2.19x) | **34.10 ns (1.65x)** |
**25% faster** |
| Memory per operation | 128 B | **64 B** | **50% less** |

### New Benchmark

Adds `BindingComparisonBenchmarker` to `Core.Benchmarks` for comparing
SetValue, Binding, TypedBinding, and SourceGeneratedBinding performance.

```bash
cd src/Core/tests/Benchmarks
dotnet run -c Release -- --filter "*BindingComparison*"
```
@github-actions github-actions bot locked and limited conversation to collaborators Mar 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants