我在这里读到:
make_shared (实际上)更有效,因为它在一个动态分配中将引用控制块与实际对象一起分配。相比之下,接受裸对象指针的 shared_ptr 的构造函数必须为引用计数分配另一个动态变量
这是否意味着使用 std::make_shared 创建的 std::shared_ptr 向量将是“缓存友好的”,因为数据(控制块和实际指针的数据)在一个块中?
我的用例是一个包含 100 000 个共享指针的向量,其中指向的对象是 14 个字节。
也许吧,但不要指望它。
为了缓存友好性,您希望使用尽可能少的内存,并且您希望地址上接近的操作在时间上也接近(即,足够接近以使第二个操作使用仍在某个级别的内存of cache 从第一个操作的效果看:cache 级别越低越好)。
如果您使用,那么总内存使用可能会略有节省,无论您的内存使用模式如何make_shared
,这至少对缓存来说是一个胜利。
如果使用make_shared
,则控制块和引用的对象(referand)在内存中是相邻的。
如果您不使用make_shared
,并且您的对象与控制块的大小不同,那么使用常见的内存分配器,对象很有可能会聚集在一个地方,而控制块会聚集在不同的地方。如果它们的大小相同(一旦由内存分配器以某种特定于实现的方式四舍五入),那么对于常见的内存分配器,它们很有可能只是在内存中长时间交替运行,除非shared_ptr
有什么影响它。
您的内存访问模式将确定哪些布局更适合缓存 - 当然,您在非make_shared
情况下获得的实际布局可能又是其他东西,具体取决于实现细节。
您拥有 a 的事实vector
基本上与所有这些无关,因为shared_ptr
对象与控制块和引用是分开的。
用 .创建共享指针向量是不可能的make_shared
。试试看,你做不到。您可以做的最好的事情是复制构造或复制分配向量中的指针来自使用make_shared
. 但随后它们将在内存中的其他地方。
但是,控制块仍会靠近对象。当你调用 时make_shared
,你实际上做了三件事:一个对象,一个用于跟踪对象引用的共享指针控制块,以及一个共享指针。该make_shared
函数使控制块和对象本身被分配在一个连续的内存块中。
这是否对缓存友好是一个有趣的问题。基本上,这取决于您如何使用该对象。
如果您经常只对共享指针而不是它们指向的对象进行操作(例如,复制向量并因此增加每个共享指针的引用计数),那么单独分配可能对缓存更友好,而不是组合分配这make_share
给了你。
如果每次操作共享指针时都频繁地操作对象本身,那么make_shared
在典型情况下应该对缓存更加友好。
正如上面提到的海报,使用 make_shared 制作对象会使“控制块”与所引用的对象相邻。
但是,在您的情况下,我认为这是一个糟糕的选择。
当您分配内存时,即使在一个大块中,您也无法保证获得连续的“物理空间”,而不是稀疏的、碎片化的页面分配。出于这个原因,遍历您的列表会导致读取大跨度内存只是为了获取控制结构(然后指向数据)。
“但我的缓存行是 64 字节长!” 你说。如果这是真的,您可能会想, “这将意味着对象与控制结构一起被加载到缓存中”,但这不一定是真的。这取决于许多因素,例如数据对齐、缓存行大小、缓存的关联性以及您使用的实际内存带宽。
您遇到的问题是,首先需要获取控制结构以找出数据的位置,而这些数据可能已经存在于缓存中,因此您的部分数据(控制结构)至少可以是如果您将它们全部分配在一起而不是使用make_shared,实际上可以保证它们在缓存中。
如果您想让您的数据缓存友好,您需要确保对它的所有引用都适合最高级别的缓存。继续使用它将有助于确保它保留在缓存中。缓存算法足够复杂,可以处理获取数据,除非您的代码非常多分支。这是使您的数据“缓存友好”的另一部分:在处理数据时使用尽可能少的分支。
此外,在处理它时,请尝试将其分解为适合缓存的部分。如果可能的话,一次只能操作 32k ——这在现代处理器上是一个保守的数字。如果您确切知道将在哪个 CPU 上运行代码,则可以根据需要对其进行不那么保守的优化。
编辑:我忘了提到一个相关的细节。最常分配的页面大小是 4k。缓存通常是“关联的”,尤其是在低端处理器中。2路关联意味着内存中的每个位置只能映射到其他每个缓存条目;4 路关联意味着它可以适合 4 种可能的映射中的任何一种,8 路意味着 8 种可能的映射中的任何一种等等。关联性越高,对您越好。处理器上最快的缓存 (L1) 往往关联性最低,因为它需要的控制逻辑更少;具有要引用的连续数据块(例如连续的控制结构)是一件好事。完全关联的缓存是可取的。