Replace OU library and volatile atomics with C++ standard equivalents - #90
Replace OU library and volatile atomics with C++ standard equivalents#90Kwizatz wants to merge 1 commit into
Conversation
5eb9838 to
a36dc3a
Compare
Remove the OU (Objects Unlimited) library dependency and replace all volatile-based atomics with C++ std::atomic, using only standard C++ threading and synchronization primitives throughout. Atomics migration (threading_atomics_provs.h, threadingutils.h): - Replace volatile + custom CAS/exchange intrinsics with std::atomic - Replace OU CAtomicsUnsafeReinit/CAtomicPtr with std::atomic<T> - Replace custom cellatomic/atomicord32/atomicptr typedefs with std::atomic equivalents - Update all consumers: threaded_solver_ldlt.h, coop_matrix_types.h, fastl*.cpp/impl.h, fastvecscale, step.cpp, quickstep.cpp, threading_impl_templates.h, resource_control.h, objects.h, util.* OU removal (odeou.h, odeou.cpp, odeinit.cpp, CMakeLists.txt): - Replace OU CEnumUnsortedElementArray/CEnumSortedElementArray with standalone C++ templates in odeou.h - Replace OU CSimpleFlags with a minimal bitflag class in odeou.h - Remove OU initialization/customization bridge from odeou.cpp - Remove dOU_ENABLED, dATOMICS_ENABLED, dTLS_ENABLED guards from odeinit.cpp - Remove ODE_WITH_OU CMake option and OU source/include references - Remove unused odeou.h includes from collision_kernel.cpp, misc.cpp, joints/amotor.cpp TLS rewrite (odetls.h, odetls.cpp): - Replace OU CThreadLocalStorage with direct slot-based struct - Use Windows FLS (Fiber-Local Storage) API on Windows for reliable per-thread cleanup (works around MinGW thread_local destructor bug) - Use thread_local with RAII wrapper on POSIX platforms Threading tests (tests/threading.cpp, tests/main.cpp): - Add 26 unit tests covering threading lifecycle, multi-threaded stepping, threaded-vs-single-threaded consistency, TLS allocation, concurrent collision, and island parallelism - Suppress non-fatal LCP solver messages during test runs All 245 tests pass (219 existing + 26 new).
a36dc3a to
52c4520
Compare
|
I did not check the actual changes yet, but, from the description alone, here is my initial commentary. OU stands for "Oder's Utilities" (and not what your CoPilot suggested ;)). I know it because Oder is the nickname of mine. The goal of this library was to provide some useful platform-dependent features not directly available in the C++ at that time, as well as a few handy general purpose utilities, in a form of C++ classes and functions. The library is lightweight and mostly header-only. Now, if atomics are widely available natively in the language, of course, it is better to use the native implementation as it is more flexible, more robust, and it is maintained. But removing the OU just for the sake of removing the OU — it might be not so wise idea. ODE is a physics simulation library and, ideologically, it should be focused on physics and the math for it. If it needs something platform-dependent which is not natively available in the language, it is better to keep that abstracted and isolated in a sub-library. Always, if you are making a breaking change you should consider what problem are you solving, what will the the gain, and if it is worth breaking things for it. With the TLS replacement (whatever that replacement could be) I can hardly imagine what could be the gains. Like, TLS always requires initialization on all all the major platforms and the initialization can be failing — you can't save on that. The language-provided thread_local... — I'm not sure it could be used directly in the cases and in the way ODE needs is. And just replacing one platform-dependent implementation with another platform-dependent implementation — that should not be worth breaking things at all. Yes, the current implementation is lacking thread memory release on Windows, but with the way it is used in ODE that's not a problem at all. As for the generic utilities, like CSimpleFlags/CAtomicFlags and the enumarrays... If you don't need them it does not automatically mean I don't need them. ;) |
| std::atomic<uint32_t> *GetStatisticsIterationCountStorage() const { dSASSERT(sizeof(uint32_t) == membersize(dWorldQuickStepIterationCount_DynamicAdjustmentStatistics, iteration_count)); return reinterpret_cast<std::atomic<uint32_t>*>(&m_statistics->iteration_count); } | ||
| std::atomic<uint32_t> *GetStatisticsPrematureExitsStorage() const { dSASSERT(sizeof(uint32_t) == membersize(dWorldQuickStepIterationCount_DynamicAdjustmentStatistics, premature_exits)); return reinterpret_cast<std::atomic<uint32_t>*>(&m_statistics->premature_exits); } | ||
| std::atomic<uint32_t> *GetStatisticsProlongedExecutionsStorage() const { dSASSERT(sizeof(uint32_t) == membersize(dWorldQuickStepIterationCount_DynamicAdjustmentStatistics, prolonged_execs)); return reinterpret_cast<std::atomic<uint32_t>*>(&m_statistics->prolonged_execs); } | ||
| std::atomic<uint32_t> *GetStatisticsFullExtraExecutionsStorage() const { dSASSERT(sizeof(uint32_t) == membersize(dWorldQuickStepIterationCount_DynamicAdjustmentStatistics, full_extra_execs)); return reinterpret_cast<std::atomic<uint32_t>*>(&m_statistics->full_extra_execs); } |
There was a problem hiding this comment.
So, there is a problem with this.
You can't cast pointer to plain integral value to a pointer to std::atomic<> as the former is a plain type and the latter is a class. Even if on some platforms the binary representation of the class may match the plain value this is not required in general.
And this is a bad code and bad behavior in general as there is plain integer and there is no std::atomic there. You may do the reinterpret_cast like this only if you know that there ACTUALLY IS the class instance there. But here, there is not.
| std::atomic<uint32_t> m_tagsTaken; | ||
| std::atomic<uint32_t> m_gravityTaken; | ||
| std::atomic<uint32_t> m_inertiaBodyIndex; |
There was a problem hiding this comment.
Since now you have introduced class instances as fields of the structure you may no longer allocate plain memory and call Initialize() on it. You have to execute in-place constructor and in-place destructor for the structure.
| unsigned int m_m; | ||
| unsigned int m_mfb; | ||
| volatile atomicord32 m_valid_findices; | ||
| std::atomic<uint32_t> m_valid_findices; |
There was a problem hiding this comment.
Since now you have introduced class instances as fields of the structure you may no longer allocate plain memory and call Initialize() on it. You have to execute in-place constructor and in-place destructor for the structure.
| std::atomic<uint32_t> m_ji_J; | ||
| std::atomic<uint32_t> m_ji_jb; | ||
| std::atomic<uint32_t> m_bi; | ||
| std::atomic<uint32_t> m_Jrhsi; |
There was a problem hiding this comment.
Since now you have introduced class instances as fields of the structure you may no longer allocate plain memory and call Initialize() on it. You have to execute in-place constructor and in-place destructor for the structure.
| std::atomic<uint32_t> *m_bi_links_or_mi_levels; | ||
| std::atomic<uint32_t> *m_mi_links; | ||
| dReal m_LCP_iteration_premature_exit_delta; | ||
| dCallReleaseeID m_LCP_IterationSyncReleasee; | ||
| unsigned int m_LCP_IterationAllowedThreads; | ||
| dCallReleaseeID m_LCP_fcStartReleasee; | ||
| volatile atomicord32 m_ji_4a; | ||
| volatile atomicord32 m_mi_iMJ; | ||
| volatile atomicord32 m_bi_forceMaxAdj; | ||
| volatile atomicord32 m_bi_fc; | ||
| volatile atomicord32 m_LCP_fcPrepareThreadsRemaining; | ||
| std::atomic<uint32_t> m_ji_4a; | ||
| std::atomic<uint32_t> m_mi_iMJ; | ||
| std::atomic<uint32_t> m_bi_forceMaxAdj; | ||
| std::atomic<uint32_t> m_bi_fc; | ||
| std::atomic<uint32_t> m_LCP_fcPrepareThreadsRemaining; | ||
| unsigned int m_LCP_fcCompleteThreadsTotal; | ||
| volatile atomicord32 m_mi_Ad; | ||
| std::atomic<uint32_t> m_mi_Ad; | ||
| unsigned int m_LCP_iteration; | ||
| unsigned int m_LCP_extra_num_iterations; | ||
| unsigned int m_LCP_iterationThreadsTotal; | ||
| volatile atomicord32 m_LCP_iterationThreadsRemaining; | ||
| std::atomic<uint32_t> m_LCP_iterationThreadsRemaining; | ||
| dCallReleaseeID m_LCP_iterationNextReleasee; | ||
| volatile atomicord32 m_SOR_reorderHeadTaken; | ||
| volatile atomicord32 m_SOR_reorderTailTaken; | ||
| volatile atomicord32 m_SOR_bi_zeroHeadTaken; | ||
| volatile atomicord32 m_SOR_bi_zeroTailTaken; | ||
| volatile atomicord32 m_SOR_mi_zeroHeadTaken; | ||
| volatile atomicord32 m_SOR_mi_zeroTailTaken; | ||
| volatile atomicord32 m_SOR_reorderThreadsRemaining; | ||
| volatile atomicord32 m_cf_4b; | ||
| volatile atomicord32 m_ji_4b; | ||
| std::atomic<uint32_t> m_SOR_reorderHeadTaken; | ||
| std::atomic<uint32_t> m_SOR_reorderTailTaken; | ||
| std::atomic<uint32_t> m_SOR_bi_zeroHeadTaken; | ||
| std::atomic<uint32_t> m_SOR_bi_zeroTailTaken; | ||
| std::atomic<uint32_t> m_SOR_mi_zeroHeadTaken; | ||
| std::atomic<uint32_t> m_SOR_mi_zeroTailTaken; | ||
| std::atomic<uint32_t> m_SOR_reorderThreadsRemaining; | ||
| std::atomic<uint32_t> m_cf_4b; | ||
| std::atomic<uint32_t> m_ji_4b; |
There was a problem hiding this comment.
Since now you have introduced class instances as fields of the structure you may no longer allocate plain memory and call Initialize() on it. You have to execute in-place constructor and in-place destructor for the structure.
| unsigned lo = 0, hi = static_cast<unsigned>(EnumMax); | ||
| while (lo < hi) | ||
| { | ||
| unsigned mid = lo + (hi - lo) / 2; | ||
| if (data[mid] < value) lo = mid + 1; | ||
| else hi = mid; | ||
| } |
There was a problem hiding this comment.
Why would not you use std::lower_bound() here if you are redesigning this class?
| template<typename EnumType, EnumType EnumMax, typename ElementType> | ||
| struct CEnumSortedElementArray |
There was a problem hiding this comment.
Here, the same commentaries, as in the unsorted case, apply.
Also, the original implementation used to validate that the array elements are ordered with respect to the comparison operator (i. e., that the array can safely be used for binary search) . And this also was one of the the major features of the class that has been lost.
| OTI_TRIMESH_TRIMESH_COLLIDER_CACHE, | ||
|
|
||
| OTI__MAX, | ||
| struct OdeTlsSlot |
There was a problem hiding this comment.
Why would not you follow the naming convention of the related class in the same file? This is the Windows convention, btw, when classes start with "C".
| if (!tls) | ||
| { | ||
| tls = new OdeTlsData(); | ||
| FlsSetValue(g_flsIndex, tls); |
There was a problem hiding this comment.
This function can fail. The API call failures have to be checked and handled.
| if (slots[i].trimeshCache) | ||
| { | ||
| COdeTls::FreeTrimeshCollidersCache(slots[i].trimeshCache); | ||
| slots[i].trimeshCache = nullptr; | ||
| } |
There was a problem hiding this comment.
This code appears four times within this .cpp file. Why would not you move it to be a method of OdeTlsSlot?
|
Also, here is a patch to add missed std::memory_order_relaxed parameters in your changes. 0001-Changed-Adding-missed-std-memory_order_relaxed-param.patch |
| ~OdeTlsDataWrapper() { data.cleanup(); } | ||
| }; | ||
|
|
||
| static thread_local OdeTlsDataWrapper g_odeTlsWrapper; |
There was a problem hiding this comment.
Since TLS key allocation is always a function that can potentially fail, here the 'thread_local' will be initialized from the CRT startup code on dlopen() (in the dynamic library case) or form the main application startup code (in the static library case). As a result, instead of clear failure location in COdeTls::Initialize() the library user will have a failure from dlopen() or a failed CRT startup of their program in general. Even though a very unlikely, this is a quality decrease.
| } | ||
|
|
||
| // Legacy aliases — both self-threaded and multi-threaded paths now use std::atomic | ||
| typedef dxStdAtomicsProvider dxFakeAtomicsProvider; |
There was a problem hiding this comment.
Since you have changed the plain type to std::atomic, you are now unable to support simple arithmetic without the atomicity. But, at least, you could create another variant of the same atomic provider with all the operations' memory order reduced to std::memory_order_relaxed.
|
Also, pay attention that you were able to make this refactoring partly because I used custom typedefs of atomicord32 and atomicptr. Now, if you replace these typedefs with generic uint32_t and void * you will lose some information and it will be harder to find the respective values and perform one more refactoring like this. Always, if a type has some custom meaning behind it, it is better to use a typedef name, rather than the generic int/void *. |
Remove the OU (Objects Unlimited) library dependency and replace all volatile-based atomics with C++ std::atomic, using only standard C++ threading and synchronization primitives throughout.
Atomics migration (threading_atomics_provs.h, threadingutils.h):
OU removal (odeou.h, odeou.cpp, odeinit.cpp, CMakeLists.txt):
TLS rewrite (odetls.h, odetls.cpp):
Threading tests (tests/threading.cpp, tests/main.cpp):
All 245 tests pass (219 existing + 26 new).