零开销抽象

This commit is contained in:
2026-08-07 16:51:40 +08:00
parent 1e903dfe1e
commit 1ddd2799f2
8 changed files with 164 additions and 46 deletions
+1 -1
View File
@@ -121,7 +121,7 @@ This preserves normal C++ member semantics, keeps raw object access available wh
`Type_Descriptor<T>` describes the **type**. `Property_Object<T>` adds state and behavior to an **instance**.
The schema contains the registered property tuple, object defaults and the default synchronization plan. A `Property_Object<T>` resolves that plan for each instance and owns its lock topology and mutex storage.
The schema contains the registered property tuple, object defaults and the default synchronization plan. `Property_Object<T>` shares one resolved default lock topology per type and keeps only instance synchronization state that is actually required: real mutex storage for locking policies and a compact override layout only when an instance explicitly supplies `Property_Synchronization`.
This distinction matters for performance and architecture: structural description is type-level information; managed synchronization is instance-level state.
+1 -1
View File
@@ -121,7 +121,7 @@ struct Device : Property_Object<Device> {
`Type_Descriptor<T>` 描述的是**类型**`Property_Object<T>` 管理的是**实例**。
Schema 保存注册属性列表、对象默认 Attribute 和默认同步计划。`Property_Object<T>` 在每个实例上解析同步计划,并持有自己的锁拓扑和 mutex 存储
Schema 保存注册属性列表、对象默认 Attribute 和默认同步计划。`Property_Object<T>` 对同一类型共享一份解析后的默认 lock topology;实例只保留真正需要的同步状态:真实锁策略需要 mutex storage,只有显式传入 `Property_Synchronization` 的实例才额外持有紧凑的覆盖布局
这个边界必须长期保持:结构描述属于类型级;同步状态属于实例级。
@@ -42,6 +42,43 @@ template <class Policy>
concept Synchronization_Policy = requires {
typename Policy::mutex_type;
} && Shared_Lockable<typename Policy::mutex_type>;
namespace detail {
template <class Mutex, bool Stores_Mutexes = !std::same_as<Mutex, Null_Shared_Mutex>>
class Mutex_Storage;
template <class Mutex>
class Mutex_Storage<Mutex, true> {
std::unique_ptr<Mutex[]> mutexes_;
public:
Mutex_Storage() = default;
explicit Mutex_Storage(std::size_t count) : mutexes_(count ? std::make_unique<Mutex[]>(count) : nullptr) {}
void reset(std::size_t count) {
mutexes_ = count ? std::make_unique<Mutex[]>(count) : nullptr;
}
Mutex& get(std::size_t index) noexcept {
return mutexes_[index];
}
Mutex& get(std::size_t index) const noexcept {
return mutexes_[index];
}
};
template <class Mutex>
class Mutex_Storage<Mutex, false> {
static Mutex& mutex() noexcept {
static Mutex value;
return value;
}
public:
Mutex_Storage() = default;
explicit Mutex_Storage(std::size_t) {}
void reset(std::size_t) {}
Mutex& get(std::size_t) noexcept {
return mutex();
}
Mutex& get(std::size_t) const noexcept {
return mutex();
}
};
}
enum class Managed_Access_Mode {
internal,
external,
@@ -120,6 +157,8 @@ public:
using object_type = Derived;
using mutex_type = typename Lock_Policy::mutex_type;
private:
static constexpr bool uses_real_mutexes = !std::same_as<mutex_type, Null_Shared_Mutex>;
struct Empty_Guard_Locks {};
struct Dynamic_Lock_Targets {
std::vector<std::size_t> slots;
std::vector<std::size_t> unsynchronized_properties;
@@ -131,23 +170,43 @@ private:
std::array<std::size_t, Capacity> unsynchronized_properties{};
std::size_t unsynchronized_count{};
};
std::vector<std::size_t> lock_slots_;
std::size_t lock_count_{};
std::unique_ptr<mutex_type[]> locks_;
std::unique_ptr<std::size_t[]> custom_lock_layout_;
[[no_unique_address]] detail::Mutex_Storage<mutex_type> mutex_storage_;
static const auto& default_resolved_synchronization() {
static const auto resolved = resolve_synchronization_plan(type_descriptor<Derived>(), type_descriptor<Derived>().synchronization_plan());
return resolved;
}
template <class Schema>
void initialize(const Schema& schema, const Synchronization_Plan& plan) {
auto resolved = resolve_synchronization_plan(schema, plan);
lock_slots_.assign(resolved.lock_slots.begin(), resolved.lock_slots.end());
lock_count_ = resolved.lock_count;
locks_ = lock_count_ ? std::make_unique<mutex_type[]>(lock_count_) : nullptr;
auto layout = std::make_unique<std::size_t[]>(Schema::property_count + 1);
layout[0] = resolved.lock_count;
std::copy(resolved.lock_slots.begin(), resolved.lock_slots.end(), layout.get() + 1);
custom_lock_layout_ = std::move(layout);
mutex_storage_.reset(resolved.lock_count);
}
void copy_synchronization_from(const Property_Object& other) {
lock_slots_ = other.lock_slots_;
lock_count_ = other.lock_count_;
locks_ = lock_count_ ? std::make_unique<mutex_type[]>(lock_count_) : nullptr;
using Schema = type_descriptor_schema_t<Derived>;
if (other.custom_lock_layout_) {
auto layout = std::make_unique<std::size_t[]>(Schema::property_count + 1);
std::copy_n(other.custom_lock_layout_.get(), Schema::property_count + 1, layout.get());
custom_lock_layout_ = std::move(layout);
} else {
custom_lock_layout_.reset();
}
mutex_storage_.reset(lock_count());
}
std::size_t lock_count() const noexcept {
return custom_lock_layout_ ? custom_lock_layout_[0] : default_resolved_synchronization().lock_count;
}
std::size_t slot(std::size_t index) const noexcept {
return lock_slots_[index];
return custom_lock_layout_ ? custom_lock_layout_[index + 1] : default_resolved_synchronization().slot(index);
}
mutex_type& mutex(std::size_t index) noexcept {
return mutex_storage_.get(index);
}
mutex_type& mutex(std::size_t index) const noexcept {
return mutex_storage_.get(index);
}
template <std::size_t Capacity>
static void sort_unique_prefix(std::array<std::size_t, Capacity>& values, std::size_t& count) {
@@ -303,25 +362,36 @@ private:
auto read_one() const {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(property_read_allowed_v<Mode, Schema, Index>);
using Value = typename Schema::template property_type<Index>::value_type;
using Property = typename Schema::template property_type<Index>;
using Value = typename Property::value_type;
if constexpr (!uses_real_mutexes && !Property::accessor_type::synchronized_view_read) {
return Value(read_unlocked<Index>(*this));
}
auto lock_slot = slot(Index);
Single_Read_View<Mode> view{*this, Index, lock_slot};
if constexpr (!uses_real_mutexes) {
return Value(read_unlocked<Index>(view));
}
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
return Value(read_unlocked<Index>(view));
}
std::shared_lock lock{locks_[lock_slot]};
std::shared_lock lock{mutex(lock_slot)};
return Value(read_unlocked<Index>(view));
}
template <Managed_Access_Mode Mode, std::size_t Index, class Value>
void write_one(Value&& value) {
using Schema = type_descriptor_schema_t<Derived>;
static_assert(property_write_allowed_v<Mode, Schema, Index>);
if constexpr (!uses_real_mutexes) {
write_unlocked<Index>(std::forward<Value>(value));
return;
}
auto lock_slot = slot(Index);
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
write_unlocked<Index>(std::forward<Value>(value));
return;
}
std::unique_lock lock{locks_[lock_slot]};
std::unique_lock lock{mutex(lock_slot)};
write_unlocked<Index>(std::forward<Value>(value));
}
public:
@@ -330,7 +400,7 @@ public:
const Property_Object* owner_{};
std::vector<std::size_t> slots_;
std::vector<std::size_t> unsynchronized_properties_;
std::vector<std::shared_lock<mutex_type>> locks_;
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::vector<std::shared_lock<mutex_type>>, Empty_Guard_Locks> locks_;
bool holds(std::size_t index) const {
auto lock_slot = owner_->slot(index);
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
@@ -340,9 +410,11 @@ public:
}
friend class Property_Object;
Read_Guard(const Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) {
locks_.reserve(slots_.size());
for (auto lock_slot : slots_) {
locks_.emplace_back(owner_->locks_[lock_slot]);
if constexpr (uses_real_mutexes) {
locks_.reserve(slots_.size());
for (auto lock_slot : slots_) {
locks_.emplace_back(owner_->mutex(lock_slot));
}
}
}
public:
@@ -376,7 +448,7 @@ public:
Property_Object* owner_{};
std::vector<std::size_t> slots_;
std::vector<std::size_t> unsynchronized_properties_;
std::vector<std::unique_lock<mutex_type>> locks_;
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::vector<std::unique_lock<mutex_type>>, Empty_Guard_Locks> locks_;
bool holds(std::size_t index) const {
auto lock_slot = owner_->slot(index);
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
@@ -386,9 +458,11 @@ public:
}
friend class Property_Object;
Write_Guard(Property_Object& owner, Dynamic_Lock_Targets targets) : owner_(&owner), slots_(std::move(targets.slots)), unsynchronized_properties_(std::move(targets.unsynchronized_properties)) {
locks_.reserve(slots_.size());
for (auto lock_slot : slots_) {
locks_.emplace_back(owner_->locks_[lock_slot]);
if constexpr (uses_real_mutexes) {
locks_.reserve(slots_.size());
for (auto lock_slot : slots_) {
locks_.emplace_back(owner_->mutex(lock_slot));
}
}
}
public:
@@ -445,7 +519,7 @@ public:
class Static_Read_Guard {
const Property_Object* owner_{};
Static_Lock_Targets<Capacity> targets_;
std::array<std::shared_lock<mutex_type>, Capacity> locks_{};
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::array<std::shared_lock<mutex_type>, Capacity>, Empty_Guard_Locks> locks_{};
bool holds(std::size_t index) const {
auto lock_slot = owner_->slot(index);
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
@@ -455,8 +529,10 @@ public:
}
friend class Property_Object;
Static_Read_Guard(const Property_Object& owner, Static_Lock_Targets<Capacity> targets) : owner_(&owner), targets_(std::move(targets)) {
for (std::size_t index = 0; index < targets_.slot_count; ++index) {
locks_[index] = std::shared_lock<mutex_type>{owner_->locks_[targets_.slots[index]]};
if constexpr (uses_real_mutexes) {
for (std::size_t index = 0; index < targets_.slot_count; ++index) {
locks_[index] = std::shared_lock<mutex_type>{owner_->mutex(targets_.slots[index])};
}
}
}
public:
@@ -489,7 +565,7 @@ public:
class Static_Write_Guard {
Property_Object* owner_{};
Static_Lock_Targets<Capacity> targets_;
std::array<std::unique_lock<mutex_type>, Capacity> locks_{};
[[no_unique_address]] std::conditional_t<uses_real_mutexes, std::array<std::unique_lock<mutex_type>, Capacity>, Empty_Guard_Locks> locks_{};
bool holds(std::size_t index) const {
auto lock_slot = owner_->slot(index);
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
@@ -499,8 +575,10 @@ public:
}
friend class Property_Object;
Static_Write_Guard(Property_Object& owner, Static_Lock_Targets<Capacity> targets) : owner_(&owner), targets_(std::move(targets)) {
for (std::size_t index = 0; index < targets_.slot_count; ++index) {
locks_[index] = std::unique_lock<mutex_type>{owner_->locks_[targets_.slots[index]]};
if constexpr (uses_real_mutexes) {
for (std::size_t index = 0; index < targets_.slot_count; ++index) {
locks_[index] = std::unique_lock<mutex_type>{owner_->mutex(targets_.slots[index])};
}
}
}
public:
@@ -617,10 +695,12 @@ private:
callback(context, property_index, key, typeid(Value), std::addressof(value));
}
};
if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
if constexpr (!uses_real_mutexes) {
emit();
} else if (lock_slot == Resolved_Synchronization_View::unsynchronized_slot) {
emit();
} else {
std::shared_lock lock{locks_[lock_slot]};
std::shared_lock lock{mutex(lock_slot)};
emit();
}
result = Runtime_Access_Result::ok;
@@ -688,10 +768,7 @@ private:
return &value;
}
protected:
Property_Object() : Property_Object_Base(runtime_interface()) {
const auto& schema = type_descriptor<Derived>();
initialize(schema, schema.synchronization_plan());
}
Property_Object() : Property_Object_Base(runtime_interface()), mutex_storage_(default_resolved_synchronization().lock_count) {}
explicit Property_Object(Property_Synchronization synchronization) : Property_Object_Base(runtime_interface()) {
initialize(type_descriptor<Derived>(), synchronization.plan);
}
@@ -713,7 +790,12 @@ public:
return type_descriptor<Derived>();
}
Resolved_Synchronization_View resolved_synchronization() const noexcept {
return {lock_slots_, lock_count_};
using Schema = type_descriptor_schema_t<Derived>;
if (custom_lock_layout_) {
return {{custom_lock_layout_.get() + 1, Schema::property_count}, custom_lock_layout_[0]};
}
const auto& resolved = default_resolved_synchronization();
return {resolved.lock_slots, resolved.lock_count};
}
Derived& unsafe_object() noexcept {
return static_cast<Derived&>(*this);
+34
View File
@@ -74,6 +74,25 @@ struct structive::Type_Descriptor<Non_Copyable_Device> {
);
}
};
struct Lockless_Device : Property_Object<Lockless_Device, No_Lock_Policy> {
Lockless_Device() = default;
explicit Lockless_Device(Property_Synchronization synchronization) : Property_Object(std::move(synchronization)) {}
int left{1};
int right{2};
};
template <>
struct structive::Type_Descriptor<Lockless_Device> {
static auto get() {
return object<Lockless_Device>(
synchronization(sync_all_independent, sync_group("sum", "left", "right", "sum")),
field < &Lockless_Device::left > (key < "left" >),
field < &Lockless_Device::right > (key < "right" >),
computed_property<Lockless_Device, int>([](const auto& view) {
return view.template get<&Lockless_Device::left>() + view.template get<&Lockless_Device::right>();
}, key < "sum" >)
);
}
};
template <class View>
concept Has_Write_Temperature = requires(View view) {
view.template write<&Device::temperature>(1);
@@ -124,6 +143,14 @@ int main() {
REQUIRE(shared_device.lock_slot<&Device::pressure>() == shared_device.lock_slot<&Device::min_speed>());
Device grouped_device{property_synchronization<Device>(synchronization(sync_all_independent, sync_group < &Device::temperature, &Device::pressure > ("environment")))};
REQUIRE(grouped_device.lock_slot<&Device::temperature>() == grouped_device.lock_slot<&Device::pressure>());
Device shared_copy{shared_device};
REQUIRE(shared_copy.lock_slot<&Device::temperature>() == shared_copy.lock_slot<&Device::pressure>());
Device shared_move{std::move(shared_copy)};
REQUIRE(shared_move.lock_slot<&Device::temperature>() == shared_move.lock_slot<&Device::pressure>());
Device assignment_target;
auto assignment_target_slot = assignment_target.lock_slot<&Device::pressure>();
assignment_target = shared_device;
REQUIRE(assignment_target.lock_slot<&Device::pressure>() == assignment_target_slot);
device.external().write < &Device::temperature > (30);
REQUIRE(device.external().read<&Device::temperature>() == 30);
device.persistence().load < &Device::pressure > (101);
@@ -151,6 +178,13 @@ int main() {
REQUIRE(computed.external().read_key<"speed_span">() == 90);
computed.write < &Computed_Device::min_speed > (20);
REQUIRE(computed.external().read_key<"speed_span">() == 80);
Lockless_Device lockless;
REQUIRE(lockless.read<&Lockless_Device::left>() == 1);
lockless.write < &Lockless_Device::right > (4);
REQUIRE(lockless.read_key<"sum">() == 5);
auto lockless_guard = lockless.lock_shared<&Lockless_Device::left, &Lockless_Device::right>();
REQUIRE(lockless_guard.get<&Lockless_Device::left>() == 1);
REQUIRE(lockless_guard.get<&Lockless_Device::right>() == 4);
Non_Copyable_Device non_copyable;
bool non_copyable_visited = false;
non_copyable.external().for_each_readable_locked([&](auto, const auto&, const auto& value) {
+2 -2
View File
@@ -407,7 +407,7 @@ auto policy = property_synchronization<Device>(
);
```
The schema remains the same; only the instance lock topology changes.
The schema remains the same; only that instance uses an override lock topology. Default instances share one resolved topology per type, so the default plan is not re-resolved for every object.
## 15. Inspect resolved synchronization
@@ -628,7 +628,7 @@ struct Device : Property_Object<Device, No_Lock_Policy> {
};
```
`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct.
`No_Lock_Policy` uses `Null_Shared_Mutex`; it removes real mutual exclusion and should only be selected when external ownership guarantees make that correct. Its default managed-object path stores no mutex array and performs no per-instance heap allocation for synchronization. A custom `Property_Synchronization` may still allocate a compact override topology because computed-property synchronization-domain checks must preserve the selected per-instance layout.
## 23. Raw object access
+2 -2
View File
@@ -407,7 +407,7 @@ auto policy = property_synchronization<Device>(
);
```
Schema 本身不变,只改变该实例的 lock topology。
Schema 本身不变,只有这个实例使用覆盖后的 lock topology。默认实例按类型共享一份解析结果,因此不会为每个对象重复解析默认同步计划。
## 15. 查看解析后的同步拓扑
@@ -628,7 +628,7 @@ struct Device : Property_Object<Device, No_Lock_Policy> {
};
```
`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。
`No_Lock_Policy` 使用 `Null_Shared_Mutex`,它取消真实互斥;只有外部所有权规则能够保证正确性时才应该使用。默认 managed-object 路径不保存 mutex 数组,也不会为同步状态产生每实例堆分配。显式使用自定义 `Property_Synchronization` 时仍可能分配一份紧凑覆盖 topology,因为 computed property 的同步域检查必须保留该实例选择的布局。
## 23. Raw Object Access
+4 -3
View File
@@ -72,8 +72,9 @@ This is the structural definition of the type.
`Property_Object<T>` provides:
- resolved lock-slot topology;
- per-instance mutex storage;
- a type-shared resolved default lock-slot topology;
- per-instance mutex storage only when the selected lock policy requires real mutexes;
- a compact per-instance topology only for explicit synchronization overrides;
- managed typed reads and writes;
- capability views;
- static and runtime multi-property guards;
@@ -84,7 +85,7 @@ This is instance behavior, not schema identity.
### 3.3 Design rule
Do not move instance state into the schema, and do not make schema metadata depend on one particular instance-management policy.
Do not move mutable instance synchronization state into the schema, and do not make schema metadata depend on one particular instance-management policy. Immutable topology derived from the type-level default plan may be shared across all instances of that type.
A future user may want to describe a large number of plain objects without paying per-object synchronization cost. The architecture should continue to leave that possibility open.
+4 -3
View File
@@ -72,8 +72,9 @@ Structive 把类型级描述和实例级管理分开。
`Property_Object<T>` 提供:
- 解析后的 lock slot 拓扑;
- 每实例 mutex 存储;
- 同一类型共享的默认解析 lock slot 拓扑;
- 只有真实锁策略才需要的每实例 mutex 存储;
- 只有显式同步覆盖实例才持有的紧凑 topology;
- managed typed read/write
- capability view
- 静态与运行时多属性 guard
@@ -84,7 +85,7 @@ Structive 把类型级描述和实例级管理分开。
### 3.3 设计约束
不要把实例状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。
不要把可变的实例同步状态塞进 Schema,也不要让 Schema 必须依赖某一种特定的实例管理策略。由类型级默认同步计划推导出的不可变 topology 可以由同类型所有实例共享。
以后完全可能有用户只想描述大量普通对象,却不愿意为每个对象承担同步状态成本。当前架构应该持续保留这种可能性。