【问题标题】:Data cache implications of using std::make_shared()使用 std::make_shared() 的数据缓存影响
【发布时间】:2012-10-11 11:45:36
【问题描述】:

我读到here

make_shared (在实践中)更有效,因为它分配 参考控制块与实际对象合二为一 动态分配。相比之下,shared_ptr 的构造函数 需要一个裸对象指针必须分配另一个动态变量 参考计数

这是否意味着使用 std::make_shared 创建的 std::shared_ptr 向量将是 “缓存友好”,因为数据(控制块和实际指针的数据)在一个块中?

我的用例是一个包含 100 000 个共享指针的向量,其中指向的对象是 14 个字节。

【问题讨论】:

  • 目前尚不清楚缓存是否更友好。如果你经常只对共享指针而不是它们指向的对象进行操作(例如,复制向量,从而增加每个共享指针的引用计数),单独的分配将更加缓存友好。
  • @DavidSchwartz 是否。如果shared_ptr 正在为计数器使用池分配器,那么在仅操作指针的算法中肯定会有缓存获胜。如果没有,根据全局分配器使用的算法,您最终可能会得到与make_shared 完全相同的内存顺序,只是分散一点,因为分配器需要额外的管理信息来进行更多单独的分配.
  • @James:你知道吗,在实践中使用什么样的池分配器shared_ptr 实现?控制块的大小不是取决于删除器的类型,所以不像对所有东西都使用固定大小那么简单?
  • @SteveJessop 我从未看过。删除器的影响是正确的,但是使用默认删除器时使用池会相当简单,否则使用通常的new

标签: c++ caching boost stl make-shared


【解决方案1】:

也许,但不要指望它。

为了缓存友好性,您希望使用尽可能少的内存,并且您希望地址上接近的操作在时间上也接近(即,足够接近以使第二个操作使用仍然存在的内存)从第一次操作的效果来看,在某些级别的缓存中:缓存级别越低越好。

如果您使用make_shared,那么总内存使用可能会略有节省,无论您的内存使用模式如何,这至少倾向于对缓存有利。

如果使用make_shared,那么控制块和引用的对象(r​​eferand)在内存中会相邻。

如果您使用make_shared,并且您的对象与您的控制块的大小不同,那么使用通用内存分配器有合理的机会将对象聚集在一起地方和控制块聚集在不同的地方。如果它们相同的大小(一旦被内存分配器以某种特定于实现的方式四舍五入),那么对于常见的内存分配器,它们很有可能只是在内存中交替长时间运行,除非@ 987654324@ 做了一些事情来影响它。

您的内存访问模式将确定哪些布局更适合缓存 - 当然,您在非make_shared 情况下获得的实际布局可能又是另一回事,具体取决于实现细节。

您拥有vector 的事实基本上与这一切无关,因为shared_ptr 对象与控制块和引用是分开的。

【讨论】:

    【解决方案2】:

    不可能创建一个使用make_shared 创建的共享指针向量。试试看,你做不到。您可以做的最好的事情是从使用make_shared 创建的共享指针复制构造或复制分配向量中的指针。但随后它们将在内存中的其他地方。

    但是,控制块仍会靠近对象。当你调用make_shared 时,你实际上做了三件事:一个对象,一个用于跟踪对对象的引用的共享指针控制块,以及一个共享指针。 make_shared 函数将控制块和对象本身分配到一个连续的内存块中。

    这是否对缓存友好是一个有趣的问题。基本上,这取决于您如何使用该对象。

    如果您经常只对共享指针而不是它们指向的对象进行操作(例如,复制向量并因此增加每个共享指针的引用计数),那么单独分配可能对缓存更友好,而不是make_share 给你的组合。

    如果您每次操作共享指针时都频繁操作对象本身,那么make_shared 在典型情况下应该对缓存更友好。

    【讨论】:

    • 你的意思是说不能把控制块和对象放在vector里面?
    • @LucDanton:向量将只包含指向控制块和对象的共享指针。对象和控制块将被分配到任何地方。向量中的共享指针只能从其他共享指针复制分配,因为向量必须位于连续内存中。
    • 您第一段中的措辞是有问题的,因为它表明std::make_sharedstd::vector 或它们的串联使用存在问题——实际上没有问题。
    • 没问题,它只是没有创建一个使用make_shared 创建的共享指针向量。这样做是不可能的。
    • 换一种说法,不可能创建一个 any 类型的向量,该向量是用 Alloc::construct 以外的 anything 创建的,其中 Alloc 是向量的分配器类型。原因很简单,这就是向量创建元素的方式pow 创建 doubles,并且您不能创建使用 pow 创建的 doubles 的向量。或者使用new 创建的原始指针向量。您只能制作它们值的向量。有点迂腐,但新手通常认为向量包含他们放入的对象,而不是它的副本/移动。
    【解决方案3】:

    正如上面的海报所提到的,使用 make_shared 制作一个对象会使“控制块”与所引用的对象相邻。

    但是,在你的情况下,我认为这是一个糟糕的选择。

    当您分配内存时,即使是在一个大块中,您也无法保证获得连续的“物理空间”,而不是稀疏、碎片化的页面分配。出于这个原因,遍历您的列表会导致读取大跨度内存只是为了获取控制结构(然后指向数据)。

    “但是我的缓存行是 64 字节长!”你说。如果这是真的,您可能会认为,“这将意味着对象与控制结构一起被加载到缓存中”,但这不一定是真的。这取决于许多因素,例如数据对齐、缓存行大小、缓存的关联性以及您使用的实际内存带宽。

    您遇到的问题是,首先需要获取控制结构以找出数据的位置,而这些数据可能已经存在于缓存中,因此您的部分数据(控制结构)可能如果您将它们全部分配在一起而不是使用 make_shared,至少实际上可以保证它们在缓存中。

    如果您想让您的数据缓存友好,您需要确保对它的所有引用都适合最高级别的缓存。继续使用它将有助于确保它保留在缓存中。缓存算法足够复杂,可以处理获取数据,除非您的代码非常多分支。这是使您的数据“缓存友好”的另一部分:在处理数据时使用尽可能少的分支。

    此外,在处理它时,请尝试将其分解为适合缓存的部分。如果可能的话,一次只能运行 32k ——这在现代处理器上是一个保守的数字。如果您确切知道将在哪个 CPU 上运行代码,则可以根据需要进行不太保守的优化。

    编辑:我忘了提一个相关的细节。最常分配的页面大小是 4k。缓存通常是“关联的”,尤其是在低端处理器中。 2路关联意味着内存中的每个位置只能映射到其他每个缓存条目; 4 路关联意味着它可以适合 4 种可能的映射中的任何一种,8 路意味着 8 种可能的映射中的任何一种等等。关联性越高对您越好。处理器上最快的高速缓存 (L1) 往往关联性最低,因为它需要的控制逻辑更少;拥有要引用的连续数据块(例如连续的控制结构)是一件好事。完全关联的缓存是可取的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-11-06
      • 2021-12-30
      • 1970-01-01
      • 2020-04-26
      • 1970-01-01
      • 1970-01-01
      • 2018-08-20
      • 2010-09-06
      相关资源
      最近更新 更多