联合中的 atomic<> 作为性能黑客
atomic<> within a union as a performance hack
我正在考虑实现一个自制的共享指针作为垃圾收集器的一部分,以避免 std::shared_ptr
的(可能是轻微的)开销是内部原子的。大致等同于:
template <typename T>
struct my_atomic_shared
{
std::atomic<std::size_t> refcount;
std::unique_ptr<T> value;
};
我的短暂希望是小整数和 atomic<small integers>
是等价的,但是转储 asm 显示 mov
用于 std::size_t
与 xchgl
用于 atomic
.我现在正在考虑以下可能是 UB 调用的实现:
template <typename T>
struct my_normal_shared
{
std::size_t refcount;
std::unique_ptr<T> value;
};
template <typename T>
struct my_shared
{
// Surprisingly tedious constructor/destructor definition omitted
enum {ATOMIC, NORMAL} tag;
union
{
my_atomic_shared<T> atomic;
my_normal_shared<T> normal;
} value;
void promote()
{
// pseudocode. Sequential consistency, calls should go via store/load
if(tag == NORMAL)
{
T got = *(value.normal);
*(value.atomic) = got;
tag = ATOMIC;
}
}
};
前提是默认创建非原子版本并在将 my_shared<>
的实例传递给另一个线程之前调用 promote()
。
我想知道是否有更好的方法来实现这种性能破解。我也很想知道将 atomic<>
变量放入联合中是否注定要失败。
编辑:添加不连贯的伪代码。存储的类型比用于引用计数的类型更有趣。
template <typename T>
struct my_shared_state
{
enum {ATOMIC, NORMAL} tag;
union
{
std::atomic<std::size_t> atomic;
std::size_t normal;
} refcount;
T value;
void incr();
void decr();
}
template <typename T>
struct my_shared_pointer
{
my_shared_state<T> * state;
// ctors and dtors modify refcount held in state
}
首先,在shared_ptr等引用计数指针设计中,shared_ptrclass中没有引用计数。它位于共享指针指向的节点中。
关系是这样的:
template<typename T>
class Node
{
std::atomic<unsigned int> RefCount;
T * obj;
};
template<typename T>
class shared_ptr
{
Node<T> * node;
T * shadow;
};
这不是 shared_ptr 的代码,而是数据结构布局的伪代码。 shared_ptr 指向一个 Node<T>
,"owns" 对象是动态创建的,以及所有 shared_ptr 的引用计数器,它有 "linked"。 shared_ptr<T>
有一个指向 Node<T>
的 obj 指针的 "shadow" 指针。
这是基本的设计布局。原子 increment/decrement 是通过取消引用完成的,这几乎与您认为从切换到非原子 increment/decrement 计数器中删除的开销一样多。
我假设 objective 允许非原子引用计数所有权可用于非线程工作,这样引用计数器就不需要同步。
请注意,选择不在 shared_ptr 内,而是在 Node.js 内。
此外,最近的 C++11/C++14 库中采用了 boost::shared_ptr 的最重要的性能增强,体现在模板函数 make_shared 中。
这个概念是基于这样的观察,即分配这个结构意味着至少两次分配,一次分配给用 new 创建的对象,另一个分配给将拥有它的节点(假定 shared_ptr在堆栈上,或成员)。因此,在 boost 中实现了一种设计(然后被 C++11 采用),通过 make_shared 函数,它为节点和正在创建的对象执行一次分配,它使用 "in place construction"并销毁被管理的对象(所有这些中的 T)。这种优化可能至少是一种性能增强,以及在此之上的内存效率优化,除非您也选择实现它,否则您将失去它。
另请注意,垃圾收集(某种)现在可在兼容的 C++11/C++14 版本中使用,因此在继续之前应该询问是否已对其进行检查并因不合适而被拒绝。
假设被拒绝,您可能会询问关于工会的问题,但另一种方法提供了您可能尚未考虑过的可能性和灵活性。
使用基于策略的设计,您可以通过构建模板 classes 参数来创建编译时多态选项。基本思想是从模板 class 中的参数之一派生。例如:
class AtomicRefCount
{ ...
};
class NonAtomicRefCount
{ ...
};
template< typename T, typename Ref >
class Node : public Ref
{ ...
};
typedef Node< SomeStruct, AtomicRefCount > SomeStruct_Node;
想法是能够select 行为或构造选项基于模板的参数。它们可以嵌套以深入链接这个概念。在此示例中,想法是根据原子或非原子引用计数整数类型的选项创建一个节点。
然而,挑战在于这意味着这两种节点是不同类型的。他们不再是 'Node< T >'。它们是 'Node< T, Ref>' 类型,因此 shared_ptr<T>
无法理解它们,它们必须是 shared_ptr< T, Ref >
但是,使用此技术可以从模板接口声明的公共基础设计 shared_ptr 和 weak_ptr,根据在声明指针时提供的参数,不同的行为会有所不同。在我多年前写的一个智能指针库中,为了处理一些各种各样的问题,包括作为一个选项的垃圾收集,这些是可能的:
template< typename T > struct MetaPtrTypes
{
typedef typename SmartPointers::LockPolicy< T, SmartPointers::StrongAttachmentPolicy > StrongLocking;
typedef typename SmartPointers::NoLockPolicy< T, SmartPointers::WeakAttachmentPolicy > WeakNoLocking;
typedef SmartPointers::LockPolicy< T, SmartPointers::WeakAttachmentPolicy > WeakLocking;
typedef SmartPointers::NoLockPolicy< T, SmartPointers::StrongAttachmentPolicy > StrongNoLocking;
typedef SmartPointers::PublicAccessPolicy< T, StrongNoLocking > PublicStrongNoLock;
typedef SmartPointers::MetaPtr< T, StrongLocking > LPtr;
typedef SmartPointers::MetaPtr< T, WeakNoLocking > WPtr;
typedef SmartPointers::MetaPtr< T, WeakLocking > WLPtr;
typedef SmartPointers::MetaPtr< T, PublicStrongNoLock > MPtr;
typedef T Type;
};
这是针对 C++03 左右的,因为我们等待 C++11 特性从草稿中逐渐出现。想法是创建一个 MetaPtrTypes< SomeClass > SomeClassPtrs;
这样做得到了像 SomeClassPtrs::MPtr
这样的类型,这是一种 shared_ptr。 WPtr 是一个弱指针,与 std::shared_ptr 和 std::weak_ptr 有点相似,但有几个自定义内存分配选项不适用于该时期的 shared_ptr,并且具有某些通常需要的锁定功能在应用程序中使用互斥体保护 shared_ptr(因为写入 shared_ptr 不是线程安全的)。
注意基于策略的设计策略。 MPtr 相当于:
typedef std::shared_ptr< SomeClass > SPtr;
哪里可以使用 SPtr,哪里就可以使用 MPtr。但是看看 MPtr 是如何构造的。这是一个MetaPtr< T, PublicStrongNoLock >
。这是一个建立起来的政策建设范式。 MetaPtr 就像前面提到的 Node< T, Ref >
,但是里面嵌入了几个策略。
查看 MPtr、WPtr、LPtr,您会发现有基于各种策略的 MetaPtr 创建,其中包括 StrongLocking 和 WeakLocking。
他们是:
typedef typename SmartPointers
::LockPolicy< T, SmartPointers
::StrongAttachmentPolicy > StrongLocking;
typedef SmartPointers
::LockPolicy< T, SmartPointers
::WeakAttachmentPolicy > WeakLocking;
这是可以用来构造智能指针的两个策略。请注意,对于 WLPtr,策略是 WeakLocking,而对于 LPtr,策略是针对 StrongLocking。
它们都是由主要用户界面 MetaPtr 制作的 class。如果 MetaPtr 是用一种 Weak 策略构建的,它就是一个 weak_ptr。如果它采用 Strong 类型的策略,则为 shared_ptr。差异在这个库中被称为附件策略,并且恰好是从中派生层次结构的根 class。在附件策略和 MetaPtr 之间是一个锁定策略。这两个是储物柜,StrongLocking和WeakLocking。有非储物柜,StrongNoLock 和 WeakNoLock。
几种不同类型的智能指针可以从几个小模板中形成 classes 实现不少于 6 种不同类型的智能指针,所有这些都基于相同的接口并共享大部分代码。
基于策略的设计是一种无需求助于联合即可实现所需内容的方法,尽管这不是一个糟糕的选择。这很简单,但是如果您的设计中有更多选项,您应该考虑基于策略的设计。
更多关于基于策略的智能指针设计可以在 Alexandrescu 2001 年的书中找到,他在书中介绍了基于策略的智能指针 loki。
在给出的示例中,MetaPtr 用于许多需要自定义内存分配的高性能场景,但当时的 shared_ptr 不支持它(并且多年不支持) .在选项中,select政策允许:
Standard memory allocation
Custom memory allocation
Garbage collection/memory management
Fast locking of writeable pointers
Lightweight reference counted smart pointers
Non-reference counted smart pointers (something like unique_ptr)
Array/Container aware smart pointers
GPU resource managers (loading/unloading textures,models and shader code)
其中许多 select 可以进行各种组合,所有版本均提供弱版本和标准版本。
我正在考虑实现一个自制的共享指针作为垃圾收集器的一部分,以避免 std::shared_ptr
的(可能是轻微的)开销是内部原子的。大致等同于:
template <typename T>
struct my_atomic_shared
{
std::atomic<std::size_t> refcount;
std::unique_ptr<T> value;
};
我的短暂希望是小整数和 atomic<small integers>
是等价的,但是转储 asm 显示 mov
用于 std::size_t
与 xchgl
用于 atomic
.我现在正在考虑以下可能是 UB 调用的实现:
template <typename T>
struct my_normal_shared
{
std::size_t refcount;
std::unique_ptr<T> value;
};
template <typename T>
struct my_shared
{
// Surprisingly tedious constructor/destructor definition omitted
enum {ATOMIC, NORMAL} tag;
union
{
my_atomic_shared<T> atomic;
my_normal_shared<T> normal;
} value;
void promote()
{
// pseudocode. Sequential consistency, calls should go via store/load
if(tag == NORMAL)
{
T got = *(value.normal);
*(value.atomic) = got;
tag = ATOMIC;
}
}
};
前提是默认创建非原子版本并在将 my_shared<>
的实例传递给另一个线程之前调用 promote()
。
我想知道是否有更好的方法来实现这种性能破解。我也很想知道将 atomic<>
变量放入联合中是否注定要失败。
编辑:添加不连贯的伪代码。存储的类型比用于引用计数的类型更有趣。
template <typename T>
struct my_shared_state
{
enum {ATOMIC, NORMAL} tag;
union
{
std::atomic<std::size_t> atomic;
std::size_t normal;
} refcount;
T value;
void incr();
void decr();
}
template <typename T>
struct my_shared_pointer
{
my_shared_state<T> * state;
// ctors and dtors modify refcount held in state
}
首先,在shared_ptr等引用计数指针设计中,shared_ptrclass中没有引用计数。它位于共享指针指向的节点中。
关系是这样的:
template<typename T>
class Node
{
std::atomic<unsigned int> RefCount;
T * obj;
};
template<typename T>
class shared_ptr
{
Node<T> * node;
T * shadow;
};
这不是 shared_ptr 的代码,而是数据结构布局的伪代码。 shared_ptr 指向一个 Node<T>
,"owns" 对象是动态创建的,以及所有 shared_ptr 的引用计数器,它有 "linked"。 shared_ptr<T>
有一个指向 Node<T>
的 obj 指针的 "shadow" 指针。
这是基本的设计布局。原子 increment/decrement 是通过取消引用完成的,这几乎与您认为从切换到非原子 increment/decrement 计数器中删除的开销一样多。
我假设 objective 允许非原子引用计数所有权可用于非线程工作,这样引用计数器就不需要同步。
请注意,选择不在 shared_ptr 内,而是在 Node.js 内。
此外,最近的 C++11/C++14 库中采用了 boost::shared_ptr 的最重要的性能增强,体现在模板函数 make_shared 中。
这个概念是基于这样的观察,即分配这个结构意味着至少两次分配,一次分配给用 new 创建的对象,另一个分配给将拥有它的节点(假定 shared_ptr在堆栈上,或成员)。因此,在 boost 中实现了一种设计(然后被 C++11 采用),通过 make_shared 函数,它为节点和正在创建的对象执行一次分配,它使用 "in place construction"并销毁被管理的对象(所有这些中的 T)。这种优化可能至少是一种性能增强,以及在此之上的内存效率优化,除非您也选择实现它,否则您将失去它。
另请注意,垃圾收集(某种)现在可在兼容的 C++11/C++14 版本中使用,因此在继续之前应该询问是否已对其进行检查并因不合适而被拒绝。
假设被拒绝,您可能会询问关于工会的问题,但另一种方法提供了您可能尚未考虑过的可能性和灵活性。
使用基于策略的设计,您可以通过构建模板 classes 参数来创建编译时多态选项。基本思想是从模板 class 中的参数之一派生。例如:
class AtomicRefCount
{ ...
};
class NonAtomicRefCount
{ ...
};
template< typename T, typename Ref >
class Node : public Ref
{ ...
};
typedef Node< SomeStruct, AtomicRefCount > SomeStruct_Node;
想法是能够select 行为或构造选项基于模板的参数。它们可以嵌套以深入链接这个概念。在此示例中,想法是根据原子或非原子引用计数整数类型的选项创建一个节点。
然而,挑战在于这意味着这两种节点是不同类型的。他们不再是 'Node< T >'。它们是 'Node< T, Ref>' 类型,因此 shared_ptr<T>
无法理解它们,它们必须是 shared_ptr< T, Ref >
但是,使用此技术可以从模板接口声明的公共基础设计 shared_ptr 和 weak_ptr,根据在声明指针时提供的参数,不同的行为会有所不同。在我多年前写的一个智能指针库中,为了处理一些各种各样的问题,包括作为一个选项的垃圾收集,这些是可能的:
template< typename T > struct MetaPtrTypes
{
typedef typename SmartPointers::LockPolicy< T, SmartPointers::StrongAttachmentPolicy > StrongLocking;
typedef typename SmartPointers::NoLockPolicy< T, SmartPointers::WeakAttachmentPolicy > WeakNoLocking;
typedef SmartPointers::LockPolicy< T, SmartPointers::WeakAttachmentPolicy > WeakLocking;
typedef SmartPointers::NoLockPolicy< T, SmartPointers::StrongAttachmentPolicy > StrongNoLocking;
typedef SmartPointers::PublicAccessPolicy< T, StrongNoLocking > PublicStrongNoLock;
typedef SmartPointers::MetaPtr< T, StrongLocking > LPtr;
typedef SmartPointers::MetaPtr< T, WeakNoLocking > WPtr;
typedef SmartPointers::MetaPtr< T, WeakLocking > WLPtr;
typedef SmartPointers::MetaPtr< T, PublicStrongNoLock > MPtr;
typedef T Type;
};
这是针对 C++03 左右的,因为我们等待 C++11 特性从草稿中逐渐出现。想法是创建一个 MetaPtrTypes< SomeClass > SomeClassPtrs;
这样做得到了像 SomeClassPtrs::MPtr
这样的类型,这是一种 shared_ptr。 WPtr 是一个弱指针,与 std::shared_ptr 和 std::weak_ptr 有点相似,但有几个自定义内存分配选项不适用于该时期的 shared_ptr,并且具有某些通常需要的锁定功能在应用程序中使用互斥体保护 shared_ptr(因为写入 shared_ptr 不是线程安全的)。
注意基于策略的设计策略。 MPtr 相当于:
typedef std::shared_ptr< SomeClass > SPtr;
哪里可以使用 SPtr,哪里就可以使用 MPtr。但是看看 MPtr 是如何构造的。这是一个MetaPtr< T, PublicStrongNoLock >
。这是一个建立起来的政策建设范式。 MetaPtr 就像前面提到的 Node< T, Ref >
,但是里面嵌入了几个策略。
查看 MPtr、WPtr、LPtr,您会发现有基于各种策略的 MetaPtr 创建,其中包括 StrongLocking 和 WeakLocking。
他们是:
typedef typename SmartPointers
::LockPolicy< T, SmartPointers
::StrongAttachmentPolicy > StrongLocking;
typedef SmartPointers
::LockPolicy< T, SmartPointers
::WeakAttachmentPolicy > WeakLocking;
这是可以用来构造智能指针的两个策略。请注意,对于 WLPtr,策略是 WeakLocking,而对于 LPtr,策略是针对 StrongLocking。
它们都是由主要用户界面 MetaPtr 制作的 class。如果 MetaPtr 是用一种 Weak 策略构建的,它就是一个 weak_ptr。如果它采用 Strong 类型的策略,则为 shared_ptr。差异在这个库中被称为附件策略,并且恰好是从中派生层次结构的根 class。在附件策略和 MetaPtr 之间是一个锁定策略。这两个是储物柜,StrongLocking和WeakLocking。有非储物柜,StrongNoLock 和 WeakNoLock。
几种不同类型的智能指针可以从几个小模板中形成 classes 实现不少于 6 种不同类型的智能指针,所有这些都基于相同的接口并共享大部分代码。
基于策略的设计是一种无需求助于联合即可实现所需内容的方法,尽管这不是一个糟糕的选择。这很简单,但是如果您的设计中有更多选项,您应该考虑基于策略的设计。
更多关于基于策略的智能指针设计可以在 Alexandrescu 2001 年的书中找到,他在书中介绍了基于策略的智能指针 loki。
在给出的示例中,MetaPtr 用于许多需要自定义内存分配的高性能场景,但当时的 shared_ptr 不支持它(并且多年不支持) .在选项中,select政策允许:
Standard memory allocation
Custom memory allocation
Garbage collection/memory management
Fast locking of writeable pointers
Lightweight reference counted smart pointers
Non-reference counted smart pointers (something like unique_ptr)
Array/Container aware smart pointers
GPU resource managers (loading/unloading textures,models and shader code)
其中许多 select 可以进行各种组合,所有版本均提供弱版本和标准版本。