Use std::mutex instead of never-shared std::shared_mutex in ShadowNodeFamily#57550
Use std::mutex instead of never-shared std::shared_mutex in ShadowNodeFamily#57550amillez wants to merge 1 commit into
Conversation
…eFamily ShadowNodeFamily::mutex_ is declared std::shared_mutex but has not been acquired in shared mode since February 2020. All three call sites take it exclusively via unique_lock, and there is no shared_lock in the class. Of the 17 classes in ReactCommon that declare a std::shared_mutex, this is the only one that never takes a shared_lock. Reduces sizeof(ShadowNodeFamily) from 384 to 272 bytes on arm64 (-112, -29%). There is one family per host element, so this scales with tree size. Changelog: [GENERAL] [CHANGED] - Replace never-shared std::shared_mutex with std::mutex in ShadowNodeFamily
|
Hi @amillez! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
Summary:
ShadowNodeFamily::mutex_is declaredstd::shared_mutex, but it is never acquired in shared mode. All three call sites take it exclusively viaunique_lock, and there is noshared_lockanywhere in the class.Worth saying up front, since the diff reads like a downgrade: this doesn't remove any read concurrency, because there hasn't been any since February 2020.
History
git log -L148,148:packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.hThe mutex came from
StateCoordinator, which used both modes:shared_lockfor the reader,unique_lockfor the writer.d418750359b("Merge StateCoordinator into ShadowNodeFamily", Feb 3 2020) carried it over unchanged.91540d74b15("Fabric: Fixing a retain cycle in between State and ShadowNodeFamily", Feb 9 2020) rewrote the reader as part of a retain cycle fix, and the lock changed along with it:afc3c97111c(Apr 2020) then addedgetMostRecentStateIfObsoletewithunique_lock, matching the neighbouring reader.How it compares to the rest of ReactCommon
Of the 17 classes that declare a
std::shared_mutex, this is the only one that never takes ashared_lock; the other 16 use both modes. That seemed worth flagging, and blame suggests this one used to be in the other group. After this change, all 16 remaining users take both modes.Why
std::mutexrather than restoring theshared_lockHappy to do it the other way if you'd prefer. My reasoning:
The critical section is
return mostRecentState_;, a singleshared_ptrcopy. Lock acquisition probably dominates the body, and acquiring ashared_lockcosts more thanmutex::lock. The lock is also per element rather than global, so contention on any one family should already be low. If that holds,std::mutexis both smaller and slightly cheaper here, whereas restoring theshared_lockwould keep the memory cost for little practical gain.The class already holds a
std::mutex(nativePropsMutex), so this is consistent with its existing style, and<mutex>is already included.Result
shared_mutexstd::mutexsizeof(ShadowNodeFamily)(arm64)112 bytes per family, 29% smaller. There's one family per host element, so it scales with tree size. The primitive saving is 100 bytes on Android/bionic and 104 on Apple/libc++, so it isn't platform specific.
This is not a fix for #55998, but it does reduce the per instance cost of the objects that issue reports accumulating.
ABI
ShadowNodeFamily.hships as a prefab header, so third party native modules compile against it. Changingsizeofis a C++ ABI break, which means this should targetmainfor a minor rather than a patch.If I've missed something
If the exclusive reads are intentional for a reason I haven't found, I'd appreciate knowing, and I'll close this.
Changelog:
[GENERAL] [CHANGED] - Replace never-shared std::shared_mutex with std::mutex in ShadowNodeFamily
Test Plan:
The compiler checks the main claim:
std::mutexhas nolock_shared(), sostd::shared_lock<std::mutex>doesn't compile. If anything took a shared lock on this mutex, this change wouldn't build.libreactnativethat transitively includeShadowNodeFamily.h, before and after the change, using the project's owncompile_commands.json(NDK 27,aarch64-none-linux-android24). Identical results.unique_lock<shared_mutex>andunique_lock<mutex>are both exclusive, non recursive, and impose the same happens-before, so with no shared acquisitions onlysizeofand timing differ. Neither type guarantees fairness.mutex_isprivate, and the twofriends (ShadowNode,State) don't reference it..cppneeds no change: all three sites use CTAD (std::unique_lock lock(mutex_)), which deducesunique_lock<std::mutex>automatically.sizeofprobe compiled against the real header using the project's own build flags.Reproduce the measurement
I'd be happy to run any other tests, just let me know what would help.