Skip to content

Conversation

@jl0pd
Copy link
Contributor

@jl0pd jl0pd commented Oct 25, 2025

What kind of change does this PR introduce?

Performance improvement.

What is the current behavior?

Currently ReactiveObject and ReactiveRecord use Lazy<T> to lazily initialize PropertyChanging, PropertyChanged, Changing, Changed, ThrownExceptions. This requires allocation of Lazy<T> itself and allocation of closure. This produces unnecessary overhead of allocating 11 objects during construction instead of 1. This was main problem when I was writing log reader, where each entry initially inherited from ReactiveObject and I had to reimplement INotifyProertyChanged manually.

What is the new behavior?

Lazy<T> was replaced with Interlocked.CompareExchange, events initialization was performed with a simple if statement. It's a simplified version of what System.Threading.LazyInitializer does.

What might this PR break?

Concurrent first access to PropertyChanging or PropertyChanged events or first access to Changing, Changed, ThrownExceptions properties.

Please check if the PR fulfills these requirements

  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

Other information:

@glennawatson glennawatson requested a review from Copilot October 25, 2025 04:10
Copy link

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 reduces object allocations in ReactiveObject and ReactiveRecord by replacing Lazy<T> initialization with direct Interlocked.CompareExchange for thread-safe lazy initialization. This optimization eliminates unnecessary allocations of Lazy<T> objects and closures during construction.

Key Changes:

  • Replaced five Lazy<T> fields with simpler boolean flags and direct initialization patterns
  • Converted event subscription tracking from Lazy<Unit> to simple boolean flags with if checks
  • Implemented thread-safe lazy initialization using Interlocked.CompareExchange and C# 13 field-backed properties

Reviewed Changes

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

File Description
src/ReactiveUI/ReactiveObject/ReactiveObject.cs Removed Lazy<T> fields and closures, replacing with boolean flags and Interlocked.CompareExchange patterns for observable properties
src/ReactiveUI/ReactiveObject/ReactiveRecord.cs Applied identical optimization pattern to record type, removing Lazy<T> allocations

@glennawatson
Copy link
Contributor

Thanks for the PR.

This is one of those that seems good on the surface but would require a bit of digging into if its a good change or not. Threading wise I'll have to check if the null field comparing is atomic, the Interlocked.CompareExchange definitely is a atomic operation, and I guess in theory if you done the exchange it should be one and only one instance.

It'd just be exploring if these mechanics work as intended since you're not changing the public API at all.

I'll get back to you on it once I've experimenting around.

@glennawatson
Copy link
Contributor

glennawatson commented Oct 25, 2025

Also worth noting we do a project ReactiveMarbles where we been testing and experimenting in general

https://github.com/reactivemarbles/Mvvm/blob/main/src/ReactiveMarbles.Mvvm/RxObject.cs

We plan on bringing across a lot of the Marbles stuff across back to RxUI at some stage, but they are offered as nuget packages well. I did a lot of alloc/perf improvements with the newer Marbles stuff.

GitHub
A light weight core package for Reactive Marbles to introduce MVVM abstractions - reactivemarbles/Mvvm

@jl0pd
Copy link
Contributor Author

jl0pd commented Oct 25, 2025

Thanks for taking a look at my PR.

After reading I'll have to check if the null field comparing is atomic I've started to doubt myself and re-read source of LazyInitializer. In second commit I've added Volatile.Read at first field access in same way as it's done in LazyInitializer, without volatile read on seconds access. This should prevent potential issues

@glennawatson
Copy link
Contributor

glennawatson commented Oct 25, 2025

I think it should be ok, likely not atomic, but either the way exists or it doesn't, and as you noted it was multiple creation anyway.

Another option is to use the new Lock class in .NET 9 but given we still target .NET8/net462 etc the fast path advantages in the new Lock would be lost in the older platforms.

@glennawatson glennawatson merged commit 346e3bb into reactiveui:main Oct 25, 2025
2 of 4 checks passed
@glennawatson
Copy link
Contributor

Thanks

Merged your change in. Testing looks good.

We got some issues with our unit tests lately, recently converted from XUnit to NUnit, and there just seems to be some race conditions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants