关于shared_ptr的引用计数块

3

关于std::shared_ptr控制块,我有两个问题:

(1) 关于大小: 如何在程序中找到std::shared_ptr的控制块确切大小?

(2) 关于逻辑: 此外,boost::shared_ptr提到它们在控制块更改方面完全无锁。(从Boost 1.33.0版本开始,在大多数常见平台上,shared_ptr使用无锁实现。)我不认为std::shared_ptr遵循相同的规则 - 是否计划在未来的C++版本中实现这一点?这是否意味着boost::shared_ptr在多线程情况下更好?


4
std::shared_ptr 是一个模板,因此必须对编译器可见。你可以研究编译器实现的std::shared_ptr并了解其工作原理。 - Sam Varshavchik
是的,但如果可能的话,我希望以编程方式实现。 - tangy
2
你可以查看编译器中 std::shared_ptr 的实现。除此之外,我认为 std::shared_ptr 只是使用了原子操作,因此在这方面是“无锁”的。请注意,如果可能的话,最好根本不要在线程之间共享可变数据,在这种情况下,您可能需要更简单的引用计数指针,它不需要原子操作或锁定。 - Cubic
谢谢,是的,在非多线程的情况下通常不需要原子操作。 但这里有两个要点 - (1) 在技术上使用原子操作并不能算作“无锁”对吧?(2) 没有实现提供简单的非原子引用计数操作,是吗? - tangy
2
@tangy 无法保证 std::shared_ptr 是基于标准原子类型实现的。但无论如何,你为什么在意呢? - curiousguy
显示剩余5条评论
3个回答

5
控制块未公开。在我阅读的实现中,它的大小是动态的,以便将删除器连续存储(和/或在make shared的情况下,存储对象本身)。
通常,它至少包含3个指针大小的字段——弱引用,强引用计数和删除器调用者。
至少有一个实现依赖RTTI;其他实现则不依赖。
在我阅读的实现中,计数操作使用原子操作;请注意,C++并不要求所有原子操作都具有无锁特性(我相信某些平台不具备指针大小的无锁操作也可以成为符合C++标准的平台)。
它们的状态与彼此及自身一致,但没有尝试使它们与对象状态一致。这就是为什么在某些平台上,使用原始的共享指针作为写时复制pImpls可能会出现错误的原因。

1
是的,只有 std::atomic_flag 能够保证无锁。当目标平台支持时,对于 shared_ptratomic<T>,好的实现会使用无锁原子操作,当然,其宽度不会超过硬件所支持的范围。 - Peter Cordes

5
(1) 关于大小: 我该如何编程查找std::shared_ptr的控制块确切大小?
没有方法直接访问它,因此无法编程查找其确切大小。
(2) 关于逻辑: 此外,boost :: shared_ptr提到它们在控制块中对于更改是完全无锁的。(从Boost 1.33.0版本开始,在大多数常见平台上,shared_ptr使用无锁实现。)我认为std::shared_ptr不遵循相同的规则 - 这是否计划用于任何未来的C ++版本?这是否意味着boost::shared_ptr在多线程情况下更好?
绝对不是。无锁实现并不总是比使用锁的实现更好。在最佳情况下,具有附加约束条件的实现不会使实现变得更糟,但它不能使实现变得更好。
考虑两个同样能干的程序员各自尽力实现shared_ptr。其中一个必须生成无锁实现。另一个完全可以自由地使用自己的判断力。没有办法在其他所有事情都相等的情况下,必须生成无锁实现的人可以产生更好的实现。最好的情况是,无锁实现最好,他们两个都会生成一个。在最坏的情况下,对于此平台,无锁实现具有巨大的缺点,因此必须使用一种实现。很烦人。

请注意,Boost引用并非“完全无锁”;它仅在“大多数常见平台上”是无锁的。 - Nicol Bolas
2
@NicolBolas 是的。这可能只是实现者认为最好的语句。很难想象在任何现代平台上您会需要或想要锁定 — 不存在任何线程需要等待任何其他线程的情况。 - David Schwartz
3
C++11只需要支持无锁的std::atomic_flag,这足以构建锁,但不足以实现无锁引用计数。将无锁的std::shared_ptr要求/保证包含在标准中理论上会限制哪些平台可以支持符合C++11的实现。我认为这就是原因,而不是在普通平台上锁定可能实际上更好,即使无锁也是可能的。 - Peter Cordes

1

(1) 当然最好检查实现,但您仍然可以从程序中进行一些检查。

控制块是动态分配的,因此为了确定其大小,您可以重载new运算符。

然后,您还可以检查std :: make_shared是否为您提供了一些控制块大小的优化。 在正确的实现中,我希望这将进行两个分配(objectA和控制块):

std::shared_ptr<A> i(new A());

然而,这只会进行一次分配(然后使用放置 new 初始化 objectA):

auto a = std::make_shared<A>();

考虑以下示例:
#include <iostream>
#include <memory>

void * operator new(size_t size) 
{ 
    std::cout << "Requested allocation: " << size << std::endl; 
    void * p = malloc(size); 
    return p; 
} 

class A {};

class B
{
    int a[8];
};

int main()
{
  std::cout << "Sizeof int: " << sizeof(int) << ", A(empty): " << sizeof(A) << ", B(8 ints): " << sizeof(B) << std::endl;
  {
      std::cout << "Just new:" << std::endl;
      std::cout << "- int:" << std::endl;
      std::shared_ptr<int> i(new int());
      std::cout << "- A(empty):" << std::endl;
      std::shared_ptr<A> a(new A());
      std::cout << "- B(8 ints):" << std::endl;
      std::shared_ptr<B> b(new B());
  }
  {
      std::cout << "Make shared:" << std::endl;
      std::cout << "- int:" << std::endl;
      auto i = std::make_shared<int>();
      std::cout << "- A(empty):" << std::endl;
      auto a = std::make_shared<A>();
      std::cout << "- B(8 ints):" << std::endl;
      auto b = std::make_shared<B>();
  }
}

我收到的输出(当然这取决于硬件架构和编译器):
Sizeof int: 4, A(empty): 1, B(8 ints): 32
Just new:
- int:
Requested allocation: 4
Requested allocation: 24

int类型的首次分配占用4个字节,下一个分配用于控制块,占用24个字节。

- A(empty):
Requested allocation: 1
Requested allocation: 24
- B(8 ints):
Requested allocation: 32
Requested allocation: 24

看起来控制块(很可能)是24个字节。

以下是使用make_shared的原因:

Make shared:
- int:
Requested allocation: 24

只需要分配一次,int + 控制块 = 24字节,比以前少了。

- A(empty):
Requested allocation: 24
- B(8 ints):
Requested allocation: 48

这里本来可能会有56个字节(32+24),但看起来实现进行了优化。如果您使用make_shared,控制块中不需要指向实际对象的指针,其大小仅为16个字节。

检查控制块大小的另一种可能方法是:

std::cout<< sizeof(std::enable_shared_from_this<int>);

在我的情况下:
16

在我的情况下,控制块的大小为16-24字节,具体取决于它是如何创建的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接