我知道一个好的编译器可以执行优化,这样通过std::shared_ptr访问int*就像使用原始int*指针一样使用相同的程序集。
我的问题是:包含优化的智能指针的缓存线是否会被来自智能指针的其他数据成员污染,如引用计数器?因此,尽管生成的程序集与实际指针相同,但缓存性能可能会更差,因为没有有效利用大量的缓存线?
编辑:如果我们遍历像std::vector<std::shared_ptr<int>>这样的结构并使用int,这种性能效果可能会更明显。
发布于 2014-04-17 18:49:29
有几件事需要考虑,但总而言之,人们可以说:这真的无关紧要。
首先,根本不能保证(或者更确切地说,没有要求)存在引用计数器。这仅仅是要求std::shared_ptr的行为方式。使用引用计数器是实现此目的的一种方式,循环链表是另一种方式。
实际上,所有的实现(至少据我所知)都使用引用计数器。
其次,引用计数器可以通过operator new单独分配,或者使用与托管对象(通过placement new创建)相同的分配(如果使用make_shared )。后者也不是严格保证的。标准的声明是"Implementations to perform __,但不是必需的,执行不超过一个内存分配“,显式地允许一些不同的东西
如果引用计数器被单独分配,那么它很可能位于不同的缓存线中,因此当访问对象时,系统将消耗两个缓存线而不是一个缓存线。然而,在某些情况下,这是一种优势,而不是劣势(即,每当您复制智能指针时,请参见下面的内容)。
除了数据缓存之外,还存在TLB,它要小得多(通常少于64个条目)。假设有一定的可能性,两个单独分配的对象不仅在不同的缓存线中,而且在不同的内存页中,这可能是另一件需要花费几个额外周期的事情。
如果引用计数器被分配在相同的位置,那么它很可能与对象的开头位于同一缓存行中(但也可能位于前一个缓存行中)。这看起来像是一个优势,但不一定是。
每当复制shared_ptr或引用同一对象的智能指针超出作用域时,都必须修改引用计数器。这是对计数器所在的缓存线的写操作(或者更确切地说,因为它是原子操作,所以不会对缓存线的写进行,但是对外部世界的净影响是相同的)。该高速缓存线无效,并且必须由想要访问该高速缓存线的其他任何人再次获取。
存在一个常见的问题,称为“假共享”,其中朴素的并行处理运行速度比每个人预期的要慢得多,这是由于完全相同的原因发生的。
现在,如果引用计数器与对象一起分配,这意味着每当复制(或超出范围)共享指针时,对对象的下一次访问是保证高速缓存未命中(因为包含对象开始的高速缓存线将从高速缓存中清除)。
tl;dr
是的,有缓存的影响,但是,你不应该太担心。缓存未命中总是会发生,而且是有规律的(TLB也是如此)。只要您至少具有某种程度上一致的访问模式,这就不会有任何影响。
在下一次上下文切换之后(即每隔几毫秒,或者在下一次中断或系统调用之后),您的缓存和TLB可能无论如何都会消失。这是每个人都不得不接受的,这根本不是问题。
使用shared_ptr并不是为了好玩,而是因为它提供了您需要的有价值的功能。复制指针可能比复制原始指针慢3-4个周期,而且当您复制它时,它可能会偶尔导致额外的缓存未命中,但您不会每秒复制十万个副本。
到目前为止,安全性和整体效用的好处远远大于缺点。
发布于 2014-04-17 18:05:53
我有充分的理由相信你在担心错误的事情。
考虑以下两个代码片段:
#include <cstdio>
#include <memory>
#include <vector>
int main() {
std::vector<std::shared_ptr<int>> v = { std::make_shared<int>(1),
std::make_shared<int>(2),
std::make_shared<int>(3) };
for (auto& e : v)
std::printf("%d\n", *e);
}和相同的功能,但使用unique_ptr
#include <cstdio>
#include <memory>
#include <vector>
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main() {
std::vector<std::unique_ptr<int>> v;
v.reserve(3);
v.emplace_back(make_unique<int>(1));
v.emplace_back(make_unique<int>(2));
v.emplace_back(make_unique<int>(3));
for (auto& e : v)
std::printf("%d\n", *e);
}只是为了回答你的问题:
如果我们遍历像std::vector<std::shared_ptr<int>>这样的结构并使用ints,这种性能效果可能会更加明显。
下面是相应的循环:
shared_ptr version | unique_ptr version
.L66: | .L70:
movq (%rbx), %rax | movq (%rbx), %rax
movl $.LC0, %esi | movl $.LC0, %esi
movl $1, %edi | movl $1, %edi
movl (%rax), %edx | movl (%rax), %edx
xorl %eax, %eax | xorl %eax, %eax
.LEHB2: |.LEHB6:
call __printf_chk | call __printf_chk
.LEHE2: |.LEHE6:
addq $16, %rbx | addq $8, %rbx
cmpq %rbx, %rbp | cmpq %rbx, %rbp
jne .L66 | jne .L70唯一相关的区别是,在读取内存时,您使用shared_ptr执行了更大的步骤。在这两种情况下,存储器访问模式是具有固定步长的线性的。我没能想出两者之间有任何可衡量的差异的情况。
在我看来,这回答了你的问题。
一些不请自来的建议,只是为了告诉你你在担心错误的事情。为上面的两个代码片段分析生成的汇编代码(g++ -std=c++11 -Wall -Wextra -pedantic -fwhole-program -O3 -S并通过c++filt运行程序集)。与unique_ptr相比,您会发现shared_ptr是一个重量级对象。您将看到,构建和销毁shared_ptr以及维护原子引用计数都是非常昂贵的;您还需要为重量级多线程机器买单。
我知道这是一个值得怀疑的指标,但是shared_ptr的汇编代码是740行,而unique_ptr的代码是402行。如果分析对象的构造和销毁,您会注意到为shared_ptr 和执行的代码要多得多,其中许多指令的开销更大(昂贵的多线程程序)。shared_ptr还会消耗更多的内存来进行额外的记账(在这些愚蠢的代码片段中: 144个字节与36个字节,也就是说,使用shared_ptr的内存是前者的4倍)。
在这一点上,潜在的缓存未命中将是我最不担心的事情。
发布于 2014-04-17 05:17:34
如果您正在复制智能指针(以便需要修改引用计数),那么它肯定会被放入缓存中进行更新。如果使用std::make_shared可以确保在单个分配中分配引用计数和目标对象,那么与此相关的问题可以得到缓解。
https://stackoverflow.com/questions/23120201
复制相似问题