Core: new blenlib library for implicit-sharing #105994

Merged
Jacques Lucke merged 17 commits from JacquesLucke/blender:implicit-sharing into main 2023-03-28 13:58:02 +02:00
5 changed files with 37 additions and 31 deletions
Showing only changes of commit d5b14539e3 - Show all commits

View File

@ -54,7 +54,7 @@ class AnonymousAttributeID : public ImplicitSharingMixin {
};
/** Wrapper for #AnonymousAttributeID that avoids manual reference counting. */
using AutoAnonymousAttributeID = ImplicitSharePtr<const AnonymousAttributeID>;
using AutoAnonymousAttributeID = ImplicitSharingPtr<const AnonymousAttributeID>;
/**
* A set of anonymous attribute names that is passed around in geometry nodes.

View File

@ -101,7 +101,7 @@ inline constexpr bool is_geometry_component_v = std::is_base_of_v<GeometryCompon
*/
struct GeometrySet {
private:
using GeometryComponentPtr = blender::ImplicitSharePtr<class GeometryComponent>;
using GeometryComponentPtr = blender::ImplicitSharingPtr<class GeometryComponent>;
/* Indexed by #GeometryComponentType. */
std::array<GeometryComponentPtr, GEO_COMPONENT_TYPE_ENUM_SIZE> components_;

View File

@ -15,26 +15,27 @@
namespace blender {
/**
* #bCopyOnWrite allows implementing copy-on-write behavior, i.e. it allows sharing read-only data
* between multiple independend systems (e.g. meshes). The data is only copied when it is shared
* and is about to be modified. This is in contrast to making copies before it is actually known
* that it is necessary.
* #ImplicitSharingInfo is the core data structure for implicit sharing in Blender. Implicit
* sharing is a technique that avoids copying data when it is not necessary. This results in better
* memory usage and performance. Only read-only data can be shared, because otherwise multiple
* owners might want to change the data in conflicting ways.
*
* Internally, this is mostly just a glorified reference count. If the reference count is 1, the
* data only has a single owner and is mutable. If it is larger than 1, it is shared and must be
* logically const.
* To determine whether data is shared, #ImplicitSharingInfo keeps a user count. If the count is 1,
* the data only has a single owner and is therefore mutable. If some code wants to modify data
* that is currently shared, it has to make a copy first.
* This behavior is also called "copy on write".
*
* On top of containing a reference count, #bCopyOnWrite also knows how to destruct the referenced
* data. This is important because the code freeing the data in the end might not know how it was
* allocated (for example, it doesn't know whether an array was allocated using the system or
* guarded allocator).
* On top of containing a reference count, #ImplicitSharingInfo also knows how to destruct the
* referenced data. This is important because the code freeing the data in the end might not know
* how it was allocated (for example, it doesn't know whether an array was allocated using the
* system or guarded allocator).
*
* #bCopyOnWrite is used in two ways:
* - It can be allocated separately from the referenced data as is typically the case with raw
* arrays (e.g. for mesh attributes).
* #ImplicitSharingInfo can be used in two ways:
* - It can be allocated separately from the referenced data. This is used when the shared data is
* e.g. a plain data array.
* - It can be embedded into another struct. For that it's best to use #ImplicitSharingMixin.
*/
struct ImplicitSharingInfo : blender::NonCopyable, blender::NonMovable {
class ImplicitSharingInfo : blender::NonCopyable, blender::NonMovable {
private:
mutable std::atomic<int> users_;
@ -74,14 +75,14 @@ struct ImplicitSharingInfo : blender::NonCopyable, blender::NonMovable {
}
brecht marked this conversation as resolved Outdated

My understanding is that user count decrement requires std::memory_order_acq_rel, while increment can use std::memory_order_relaxed.

For example libc++ does this for shared_ptr. More details:
https://stackoverflow.com/questions/48124031/stdmemory-order-relaxed-atomicity-with-respect-to-the-same-atomic-variable

My understanding is that user count decrement requires `std::memory_order_acq_rel`, while increment can use `std::memory_order_relaxed`. For example libc++ does this for shared_ptr. More details: https://stackoverflow.com/questions/48124031/stdmemory-order-relaxed-atomicity-with-respect-to-the-same-atomic-variable
private:
/** Has to free the #bCopyOnWrite and the referenced data. */
/** Has to free the #ImplicitSharingInfo and the referenced data. */
virtual void delete_self_with_data() = 0;
};
/**
* Makes it easy to embed copy-on-write behavior into a struct.
* Makes it easy to embed implicit-sharing behavior into a struct.
JacquesLucke marked this conversation as resolved Outdated

This comment is a bit hard to follow, maybe replacing the subclass with something more specific would help (like saying the ptr class can be used with a class that derives from the mixin class).

This comment is a bit hard to follow, maybe replacing `the subclass` with something more specific would help (like saying the ptr class can be used with a class that derives from the mixin class).
*/
struct ImplicitSharingMixin : public ImplicitSharingInfo {
class ImplicitSharingMixin : public ImplicitSharingInfo {
public:
ImplicitSharingMixin() : ImplicitSharingInfo(1)
{

View File

@ -10,33 +10,38 @@
namespace blender {
template<typename T> class ImplicitSharePtr {
/**
* #ImplicitSharingPtr is a smart pointer that manages implicit sharing. It's designed to work with
* types that derive from #ImplicitSharingMixin. It is fairly similar to #std::shared_ptr but
* expects the reference count to be embedded in the data.
JacquesLucke marked this conversation as resolved Outdated

expects -> requires, right? std::make_shared will allocate the use count with the object so it's helpful to clarify this a bit.

`expects` -> `requires`, right? `std::make_shared` will allocate the use count with the object so it's helpful to clarify this a bit.
*/
template<typename T> class ImplicitSharingPtr {
private:
T *data_ = nullptr;
public:
ImplicitSharePtr() = default;
ImplicitSharingPtr() = default;
ImplicitSharePtr(T *data) : data_(data)
ImplicitSharingPtr(T *data) : data_(data)
{
}
ImplicitSharePtr(const ImplicitSharePtr &other) : data_(other.data_)
ImplicitSharingPtr(const ImplicitSharingPtr &other) : data_(other.data_)
{
this->add_user(data_);
}
ImplicitSharePtr(ImplicitSharePtr &&other) : data_(other.data_)
ImplicitSharingPtr(ImplicitSharingPtr &&other) : data_(other.data_)
{
other.data_ = nullptr;
}
~ImplicitSharePtr()
~ImplicitSharingPtr()
{
this->remove_user_and_delete_if_last(data_);
}
ImplicitSharePtr &operator=(const ImplicitSharePtr &other)
ImplicitSharingPtr &operator=(const ImplicitSharingPtr &other)
{
if (this == &other) {
return *this;
@ -48,7 +53,7 @@ template<typename T> class ImplicitSharePtr {
return *this;
}
ImplicitSharePtr &operator=(ImplicitSharePtr &&other)
ImplicitSharingPtr &operator=(ImplicitSharingPtr &&other)
{
if (this == &other) {
return *this;
@ -122,7 +127,7 @@ template<typename T> class ImplicitSharePtr {
return get_default_hash(data_);
}
friend bool operator==(const ImplicitSharePtr &a, const ImplicitSharePtr &b)
friend bool operator==(const ImplicitSharingPtr &a, const ImplicitSharingPtr &b)
{
return a.data_ == b.data_;
}

View File

@ -226,8 +226,8 @@ struct GatherTasks {
/* Volumes only have very simple support currently. Only the first found volume is put into the
* output. */
ImplicitSharePtr<const VolumeComponent> first_volume;
ImplicitSharePtr<const GeometryComponentEditData> first_edit_data;
ImplicitSharingPtr<const VolumeComponent> first_volume;
ImplicitSharingPtr<const GeometryComponentEditData> first_edit_data;
};
/** Current offsets while during the gather operation. */