19

几天前,我碰巧看了Stephan T. Lavavej 的这个非常有趣的演讲,其中提到了“我们知道你住在哪里”的优化(抱歉在问题标题中使用了首字母缩写词,所以警告我这个问题可能已经关闭,否则),以及Herb Sutter 关于机器架构的这个漂亮的作品。

简而言之,“我们知道你住在哪里”优化包括将引用计数器放置在与正在创建的对象相同的内存块上make_shared,从而导致一个内存分配而不是两个内存分配,并且shared_ptr更加紧凑。

然而,在总结了我从上面两个演示文稿中学到的东西之后,我开始怀疑 WKWYL 优化是否不会降低性能,以防被运行在不同内核上shared_ptr的多个线程访问。

如果引用计数器接近内存中的实际对象,实际上,它们应该更有可能被提取到与对象本身相同的缓存行中。反过来,如果我得到正确的教训,这将使线程更有可能在竞争相同的缓存行时变慢,即使它们不需要。

假设一个线程需要多次更新引用计数器shared_ptr(例如,在复制周围时),而其他线程只需要访问指向的对象:这不会通过让所有线程竞争来减慢所有线程的执行速度吗?对于同一个缓存行?

如果 refcount 存在于内存中的其他位置,我会说争用不太可能发生

这是否是反对在类似情况下使用的一个很好的论据make_shared()(当然,只要它实现了 WKWYL 优化)?还是我的推理存在谬误?

4

3 回答 3

11

如果那是您的使用模式,那么肯定make_shared会导致“错误共享”,这是我知道的使用相同缓存行的不同线程的名称,即使它们没有访问相同的字节。

对于附近部分被不同线程(其中一个是写入)使用的任何对象也是如此。在这种情况下,“对象”是由make_shared. 您也可以询问,在不同线程或多或少同时使用近端数据的情况下,任何从数据局部性中受益的尝试是否会适得其反。是的,它可以。

可以得出结论,如果每个对象的每个可写部分都分配在遥远的位置,则不太可能发生争用。因此,错误共享的解决方法通常是将内容分散开(在这种情况下,您可以停止使用make_shared,或者您可以将填充放入对象中以将其部分分隔到不同的缓存行中)。

与此相反,当在同一个线程中使用不同的部分时,如果您将它们分散到内存中,那么这是有代价的,因为还有更多的东西要提取到缓存中。由于分散事物有其自身的成本,因此对于您最初认为的如此多的应用程序而言,这实际上可能无济于事。但毫无疑问,可以编写有帮助的代码。

有时,make_shared它的好处与缓存行和位置无关,只是它进行了一次动态分配而不是两次。它的价值取决于您分配和释放的对象数量:它可能可以忽略不计;这可能是您的应用程序适合 RAM 与疯狂交换之间的区别;在某些情况下,您的应用可能需要进行所有需要的分配。

make_shared仅供参考,还有另一种情况可能不使用shared_ptr. 原因是在弱指针消失之前控制块不会被释放,因此如果你使用make_shared了对象占用的整个内存,直到弱指针消失才会释放。当然,一旦共享指针被销毁,对象就会被销毁,所以重要的是类的大小,而不是关联的资源。

于 2013-01-15T18:23:10.010 回答
5

Note that allocating the ref count isn't directly about the WKWYL optimization -- that's the primary intended effect of std::make_shared itself. You have full control: use make_shared<T>() to save an allocation and put the reference count with the object, or use shared_ptr<T>( new T() ) to keep it separate.

Yes, if you place the object and the reference count in the same cacheline, it might lead to performance degradations due to false sharing, if the reference count is updated frequently while the object is accessed only reading.

However the way I see it there are two factors why this isn't factored into the decision for doing this optimization:

  1. In general you don't want the reference count to change frequently, since that by itself is a performance problem (atomic operations, several threads accessing it, ...) which you want to avoid (and probably can for most cases)
  2. Doing this optimization doesn't necessarily incur the potential extra performance problems you described. For that to happen the reference count and (parts of) the object need to be in the same cacheline. It could therefore easily be avoided by adding appropriate padding between the reference count (+other data) and the object. In that case the optimization would still only do one allocation instead of two and therefore still be beneficial. However for the more likely case which doesn't trigger this behaviour, it would be slower then the non padded version, since in the non padded version you benefit from the better locality (the object and reference count being in the same cacheline). For this reason I think that this variant is a possible optimization for highly threaded code, but not necessarily one to be made in the standard version.
  3. If you know how shared_ptr is implemented on your platform you could emulate the padding, either by inserting padding into the object, or (possibly, depending on the order in memory), by giving it a deleter, which includes enough padding.
于 2013-01-15T16:58:03.373 回答
4

假设其中一个线程需要多次更新引用计数器shared_ptr(例如,在复制周围时),而其他线程只需要访问指向的对象:这不会通过让所有线程竞争来减慢所有线程的执行速度吗?对于同一个缓存行?

是的,但这是一个现实的场景吗?

在我的代码中,复制该对象的线程之所以shared_ptr这样做是因为它们希望共享对象的所有权以便可以使用它。如果进行所有这些引用计数更新的线程不关心该对象,他们为什么还要费心分享它的所有权呢?

const shared_ptr&您可以通过传递引用并仅在您真正想要拥有和访问对象时制作(或销毁)副本来缓解问题,例如,在跨线程或模块边界传输它时,或者在获得对象的所有权以使用它时。

通常,侵入性引用计数优于外部引用计数(请参阅智能指针计时,因为它们位于单个缓存行上,因此您不需要为对象及其引用计数使用两个宝贵的缓存行。请记住,如果您已用完一个额外的缓存行,那么其他所有缓存行就会少一个缓存行,并且某些内容将被驱逐,并且在下一次需要时您将获得缓存未命中。

于 2013-01-15T20:52:59.893 回答