-
-
Couldn't load subscription status.
- Fork 1.1k
Reduce allocations within ReactiveObject and ReactiveRecord #4195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce allocations within ReactiveObject and ReactiveRecord #4195
Conversation
There was a problem hiding this 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 withifchecks - Implemented thread-safe lazy initialization using
Interlocked.CompareExchangeand 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 |
|
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. |
|
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.
|
|
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 |
|
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. |
|
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. |
What kind of change does this PR introduce?
Performance improvement.
What is the current behavior?
Currently
ReactiveObjectandReactiveRecorduseLazy<T>to lazily initializePropertyChanging,PropertyChanged,Changing,Changed,ThrownExceptions. This requires allocation ofLazy<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 fromReactiveObjectand I had to reimplementINotifyProertyChangedmanually.What is the new behavior?
Lazy<T>was replaced withInterlocked.CompareExchange, events initialization was performed with a simpleifstatement. It's a simplified version of whatSystem.Threading.LazyInitializerdoes.What might this PR break?
Concurrent first access to
PropertyChangingorPropertyChangedevents or first access toChanging,Changed,ThrownExceptionsproperties.Please check if the PR fulfills these requirements
Other information: