fix(Storage): remove static-init-order dependency from Storage construction#1073
fix(Storage): remove static-init-order dependency from Storage construction#1073IvanaGyro wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the Storage initialization mechanism by replacing the legacy Storage_init_interface class and its global instance with a static array named storage_init_fns. This change simplifies the initialization logic and removes redundant code across multiple files, including updates to Tensor_impl and various backend implementations. The reviewer recommends using the inline keyword for the storage_init_fns array in the header file to ensure a single instance across translation units and prevent potential code bloat.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| static StorageInitFn storage_init_fns[N_Type] = { | ||
| nullptr, SIInit_cd, SIInit_cf, SIInit_d, SIInit_f, SIInit_i64, | ||
| SIInit_u64, SIInit_i32, SIInit_u32, SIInit_i16, SIInit_u16, SIInit_b}; |
There was a problem hiding this comment.
Using static for storage_init_fns at namespace scope in a header file will create a separate copy of the array in each translation unit that includes this header. While this may work, it can lead to unnecessary code bloat.
Since the project targets C++20, you can use inline to ensure there is only one instance of this array across the entire program. This is the modern and correct way to define a global variable in a header file since C++17.
| static StorageInitFn storage_init_fns[N_Type] = { | |
| nullptr, SIInit_cd, SIInit_cf, SIInit_d, SIInit_f, SIInit_i64, | |
| SIInit_u64, SIInit_i32, SIInit_u32, SIInit_i16, SIInit_u16, SIInit_b}; | |
| inline StorageInitFn storage_init_fns[N_Type] = { | |
| nullptr, SIInit_cd, SIInit_cf, SIInit_d, SIInit_f, SIInit_i64, | |
| SIInit_u64, SIInit_i32, SIInit_u32, SIInit_i16, SIInit_u16, SIInit_b}; |
There was a problem hiding this comment.
The solution was already posted at #673 (comment)
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1073 +/- ##
==========================================
+ Coverage 72.11% 72.61% +0.49%
==========================================
Files 225 226 +1
Lines 28207 28103 -104
Branches 71 71
==========================================
+ Hits 20341 20406 +65
+ Misses 7845 7676 -169
Partials 21 21
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 19 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
|
This is an improvement over There should be two construction paths. For code where template <CytnxType T>
boost::intrusive_ptr<StorageImplementation<T>>
make_storage(cytnx_uint64 size, int device, bool init_zero = true) {
auto out = boost::intrusive_ptr<StorageImplementation<T>>(
new StorageImplementation<T>());
out->Init(size, device, init_zero);
return out;
}Eventually A runtime-dtype factory is still needed for the public namespace internal {
template <std::size_t... Is>
boost::intrusive_ptr<Storage_base>
make_storage_impl(unsigned int dtype, cytnx_uint64 size, int device,
bool init_zero, std::index_sequence<Is...>) {
boost::intrusive_ptr<Storage_base> out;
const bool matched = (... || [&] {
using T = std::variant_alternative_t<Is + 1, Type_list>; // skip void
if (dtype != Type_class::cy_typeid_v<T>) return false;
out = make_storage<T>(size, device, init_zero);
return true;
}());
cytnx_error_msg(!matched, "[Storage] invalid dtype %u.%s", dtype, "\n");
return out;
}
} // namespace internal
inline boost::intrusive_ptr<Storage_base>
make_storage(unsigned int dtype, cytnx_uint64 size, int device,
bool init_zero = true) {
constexpr std::size_t count = std::variant_size_v<Type_list> - 1;
return internal::make_storage_impl(
dtype, size, device, init_zero, std::make_index_sequence<count>{});
}Then this->_impl = make_storage(dtype, size, device, init_zero);This removes:
The fold above is one possible implementation; a compile-time-generated Until |
| @@ -976,57 +935,57 @@ namespace cytnx { | |||
| } | |||
|
|
|||
| void _from_vector(const std::vector<cytnx_complex128> &vin, const int device = -1) { | |||
There was a problem hiding this comment.
templatize these function, just let
p = new StorageImeplementation<T>(); // and Init
this->_impl = boost::intrusive_ptr(p)
|
I prefer to make this PR simple. There is no many function use this init function. I will preserve function table for the code that need large scope refactoring to use typed constructor directly. The places that are easy to templatize will be refactored. |
Collapse the eleven per-dtype _from_vector overloads (and the unused generic fallback that only raised a runtime error) into a single function template that constructs StorageImplementation<T> directly instead of routing through the runtime storage_init_fns dispatch table. Callers of _from_vector already know the element type T, so the integer-dtype lookup threw the type information away only to recover it. The template is constrained with a static_assert on CytnxType<T>, turning an unsupported element type into a compile-time error at the call site rather than the previous runtime "[FATAL] ERROR unsupport type". The bit-packed std::vector<bool> case keeps its element-wise _cpy_bool copy via an if constexpr branch; every other dtype uses a contiguous memcpy from vin.data(). Co-Authored-By: Claude <noreply@anthropic.com>
Unify the eleven SIInit_cd/SIInit_cf/... free functions and the storage_init_fns[N_Type] pointer table into a single init_storage(dtype) factory. The dispatch is generated by folding over Type_list (skipping void at index 0), mirroring as_storage_variant_impl, so the supported dtype set has one source of truth instead of a hand-maintained enumeration that must be kept in lockstep with Type_list. This also drops the StorageInitFn typedef. Runtime-dtype callers (Storage::Init, Storage::_Loadbinary and the load path in Storage.cpp, and cuMovemem_gpu) now call init_storage(dtype). Unlike the old table, which stored a nullptr at the Type.Void slot that would be called on an out-of-range access, the fold raises cytnx_error_msg on any dtype without a StorageImplementation, Void included. Add <utility> and <variant> includes for std::index_sequence and the std::variant_alternative_t / std::variant_size_v the fold relies on. Co-Authored-By: Claude <noreply@anthropic.com>
Guard the behavior of init_storage(dtype), the replacement for the Storage_init_interface/__SII table that #673 reported segfaulting on. Construct a Storage of every supported dtype (the exact repro from the issue for the default Double case, plus a round-tripped representative value -- including negatives, fractional values, complex, and both Bool states -- for every other dtype) and check size()/dtype()/value against independently known expectations rather than one Cytnx path against another. Verified this test also passes unmodified against the pre-#1073 code (Storage_init_interface/__SII, checked out at f64b2f5): the segfault was a cross-translation-unit static-initialization-order dependency that only manifests in a fresh C++ process, and pytest already runs inside a fully loaded cytnx extension module where every global object's constructor has necessarily already run. So this test cannot reproduce the original crash and only guards the dispatch behavior of the new code, which the module docstring states explicitly. A C++ test that can reproduce the original ordering-dependent failure is tracked in a follow-up issue, deferred until #1080 (test-suite reorganization) merges. Co-Authored-By: Claude <noreply@anthropic.com>
Problem
Closes #673.
Storage::Init(inline, in the header) dispatched dtype construction throughStorage_init_interface::USIInit[dtype], astaticfunction-pointer table populated by the constructor of the globalStorage_init_interface __SIIobject defined insrc/backend/Storage.cpp. BecauseStorage::Initonly touches thestatictable member — not__SIIitself — a translation unit that instantiatesStorage(size)without also referencing anything else fromStorage.cpp(e.g.operator<<) has no guarantee that__SII's constructor has run beforeUSIInit[dtype]is read: a static-initialization-order fiasco. When it hasn't run,USIInit[dtype]is still zero-initialized and Cytnx calls a null function pointer, segfaulting.Fix
Storage_init_interface,__SII, and theUSIInittable entirely — there is no more implicit global-initializer side effect to depend on.init_storage(dtype), a plain function that folds overType_list(skippingvoid) to find and construct theStorageImplementation<T>matching the requested dtype, mirroring the existingas_storage_variant_implpattern already used inStorage.cpp. This keeps the supported dtype set single-sourced fromType_listinstead of a hand-populated table.Storage::Init,Storage::_Loadbinary/the file-load path, and the GPU move/copy paths incuMovemem_gpu.cuto callinit_storage(dtype).Storage::_from_vectorso callers that already know the element typeTconstructStorageImplementation<T>directly, instead of also routing through the (removed) runtime dtype table.Testing
ctestondebug-openblas-cpuanddebug-mkl-cpu: all Storage suites pass (288/288); one pre-existing, unrelated ASan failure inBlockUniTensorTest.CombineBondForceDoesNotRewriteUserHeldBondreproduces identically on the pre-change base.pytest(Storage / dtype / mixed-dtype-arithmetic / rank-zero suites) passes against the rebuilt Python bindings.debug-openblas-cuda: compiles and links (cuMovemem_gpu.cuchange) — compile-check only, no GPU available in this environment.pre-commit run clang-format(pinned v14) clean.