联合中的 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_txchgl 用于 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 可以进行各种组合,所有版本均提供弱版本和标准版本。