智能指针包装惩罚。使用std :: map进行记忆

时间:2013-04-12 05:13:56

标签: c++ performance boost shared-ptr smart-pointers

我目前正处于一个性能至关重要的项目中。以下是我对此问题的一些问题。

问题1

我的项目涉及大量boost::shared_ptr。我知道使用boost::make_shared在运行中创建共享指针很慢,因为它需要跟踪引用有很多开销。我想知道如果已经创建了boost共享指针,那么这两个语句会有相同的性能,还是会比另一个更快。如果常规指针更快并且我已经有共享指针我有什么选项来调用共享指针指向的方法?

 statement1: sharedptr->someMethod();  //here the pointer is a shared ptr created by boost::make_shared
 statement2: regularptr->someMethod(); //here the pointer is a regular one made with new

问题2

我有一个快速调用的实例方法,每次都会在堆栈上创建std::vector<std::string>。我决定将该向量指针存储在静态std :: map(即)std::map<std::String,std::vector<std::string>*>中。如果键的映射中不存在向量(可以是方法的名称)。创建了有效的向量地址并将其添加到地图中。所以我的问题是“是否值得在地图中搜索向量地址并返回有效地址,而不是像std::vector<std::string> somevector那样在堆栈上创建一个地址。我也会就像std::map发现的表现一样。

有关这些问题的任何想法都将受到赞赏。

4 个答案:

答案 0 :(得分:13)

回答Q#1

  

如果常规指针更快并且我已经有共享指针我有什么选项来调用共享指针指向的方法?

operator-> has assertion内的

boost::shared_ptr

typename boost::detail::sp_member_access< T >::type operator-> () const 
{
    BOOST_ASSERT( px != 0 );
    return px;
}

因此,首先,请确保您已定义NDEBUG(通常在发布版本中,它会自动完成):

#define NDEBUG

我在解除引用boost::shared_ptr和原始指针之间进行了汇编比较:

template<int tag,typename T>
NOINLINE void test(const T &p)
{
    volatile auto anti_opti=0;
    ASM_MARKER<tag+0>();
    anti_opti = p->data;
    anti_opti = p->data;
    ASM_MARKER<tag+1>();
    (void)anti_opti;
}

test<1000>(new Foo);
ASMtest时{p} T代码为Foo*(不要害怕,我在下方diff):

_Z4testILi1000EP3FooEvRKT0_:
.LFB4088:
.cfi_startproc
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movq %rdi, %rbx
subq $16, %rsp
.cfi_def_cfa_offset 32
movl $0, 12(%rsp)
call _Z10ASM_MARKERILi1000EEvv
movq (%rbx), %rax
movl (%rax), %eax
movl %eax, 12(%rsp)
movl %eax, 12(%rsp)
call _Z10ASM_MARKERILi1001EEvv
movl 12(%rsp), %eax
addq $16, %rsp
.cfi_def_cfa_offset 16
popq %rbx
.cfi_def_cfa_offset 8
ret
.cfi_endproc

test<2000>(boost::make_shared<Foo>());
ASMtest时{p} T代码为boost::shared_ptr<Foo>

_Z4testILi2000EN5boost10shared_ptrI3FooEEEvRKT0_:
.LFB4090:
.cfi_startproc
pushq %rbx
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
movq %rdi, %rbx
subq $16, %rsp
.cfi_def_cfa_offset 32
movl $0, 12(%rsp)
call _Z10ASM_MARKERILi2000EEvv
movq (%rbx), %rax
movl (%rax), %eax
movl %eax, 12(%rsp)
movl %eax, 12(%rsp)
call _Z10ASM_MARKERILi2001EEvv
movl 12(%rsp), %eax
addq $16, %rsp
.cfi_def_cfa_offset 16
popq %rbx
.cfi_def_cfa_offset 8
ret
.cfi_endproc

以下是diff -U 0 foo_p.asm shared_ptr_foo_p.asm命令的输出:

--- foo_p.asm   Fri Apr 12 10:38:05 2013
+++ shared_ptr_foo_p.asm        Fri Apr 12 10:37:52 2013
@@ -1,2 +1,2 @@
-_Z4testILi1000EP3FooEvRKT0_:
-.LFB4088:
+_Z4testILi2000EN5boost10shared_ptrI3FooEEEvRKT0_:
+.LFB4090:
@@ -11 +11 @@
-call _Z10ASM_MARKERILi1000EEvv
+call _Z10ASM_MARKERILi2000EEvv
@@ -16 +16 @@
-call _Z10ASM_MARKERILi1001EEvv
+call _Z10ASM_MARKERILi2001EEvv

正如您所看到的,差异仅在于函数签名,tag非类型模板参数值,其余代码为IDENTICAL


通常 - shared_ptr非常昂贵 - 它的引用计数在线程之间同步(通常通过原子操作)。如果您改用boost::intrusive_ptr,那么您可以在没有线程同步的情况下实现自己的increment / decrement,这会加快引用计数。

如果你能负担得起unique_ptr或移动语义(通过Boost.Move或C ++ 11) - 那么就没有任何引用计数 - 它会更快更快。

LIVE DEMO WITH ASM OUTPUT

#define NDEBUG

#include <boost/make_shared.hpp>
#include <boost/shared_ptr.hpp>

#define NOINLINE __attribute__ ((noinline))

template<int>
NOINLINE void ASM_MARKER()
{
    volatile auto anti_opti = 11;
    (void)anti_opti;
}

struct Foo
{
    int data;
};

template<int tag,typename T>
NOINLINE void test(const T &p)
{
    volatile auto anti_opti=0;
    ASM_MARKER<tag+0>();
    anti_opti = p->data;
    anti_opti = p->data;
    ASM_MARKER<tag+1>();
    (void)anti_opti;
}

int main()
{
    {
        auto p = new Foo;
        test<1000>(p);
        delete p;
    }
    {
        test<2000>(boost::make_shared<Foo>());
    }
}

回答Q#2

  

我有一个快速调用的实例方法,每次都会在堆栈上创建一个std :: vector。

通常,最好尝试重用vector的容量,以防止代价高昂的重新分配。例如,最好更换:

{
    for(/*...*/)
    {
        std::vector<value> temp;
        // do work on temp
    }
}

使用:

{
    std::vector<value> temp;
    for(/*...*/)
    {
        // do work on temp
        temp.clear();
    }
}

但看起来由于std::map<std::string,std::vector<std::string>*>类型,你试图表现出某种memoization

正如已经建议的那样,您可以尝试使用boost::unordered_map / std::unordered_map代替std::map O(ln(N))查找/插入 O(1)平均值和 O(N)查找/插入的最坏情况复杂度,以及更好的位置/ compactess(缓存友好)。

另外,cosider要尝试Boost.Flyweight

  

Flyweights 是小型句柄类,可以持续访问共享公共数据,从而允许在合理的内存限制内管理大量实体。的 Boost.Flyweight 可以很容易地通过提供类模板的轻量级下,其充当简易替换为的常量Ť

答案 1 :(得分:4)

问题1:

主要的性能增益可以在架构设计,使用的算法中实现,而低级别的关注也只有在高级设计强大时才重要。 让我们来看你的问题,常规指针性能高于shared_ptr。但是,您看到的不使用shared_ptr的开销也更多,这增加了在更长时间内维护代码的成本。 在性能关键型应用程序中必须避免冗余对象的创建和销毁。  在这种情况下,shared_ptr扮演着重要的角色,它在跨线程共享公共对象时起作用 通过减少释放资源的开销。 由于引用计数,分配(对象,计数器,删除)等,共享指针比常规指针消耗更多时间。 你可以通过防止不必要的副本来使shared_ptr更快。将它用作ref(shared_ptr const&amp;)。 此外,您不需要跨线程共享资源不使用shared_ptr,而常规ptr将在这种情况下提供更好的性能。

问题2

如果要使用shared_ptr对象的重用池,可以更好地研究对象池设计模式方法。 http://en.wikipedia.org/wiki/Object_pool_pattern

答案 2 :(得分:2)

问题1:

我在项目中广泛使用共享指针,但我不想使用shared_ptr<T>。它需要一个与T本身分开分配的堆对象,因此内存分配开销加倍,内存使用量增加一定量,这取决于运行时库的实现。 intrusive_ptr效率更高,但有一个关键问题让我烦恼,那就是函数调用:

void Foo(intrusive_ptr<T> x) {...}

每次调用Foo时,参数x的引用计数必须以相对昂贵的原子增量递增,然后在出路时递减。但这是多余的,因为您通常可以假设调用者已经具有对x的引用,并且该引用在调用期间有效。有可能调用者可能没有引用的方法,但是编写代码并不难以使调用者的引用始终有效。

因此,我更喜欢使用我自己的智能指针类,它与intrusive_ptr相同,只是它隐式转换为T *和从T *转换。然后我总是声明我的方法采用普通指针,避免不必要的引用计数:

void Foo(T* x) {...}

这种方法已被证明在我的项目中运作良好,但说实话,我从未真正测量过它所带来的性能差异。

另外,尽可能使用auto_ptr(C ++ 03)或unique_ptr(C ++ 11)。

问题2:

我不明白你为什么要考虑使用std :: map。首先,hash_map会更快(只要它不是VS2008 / 2010中的VC ++ Dinkumware实现,details in here somewhere),其次如果每个方法只需要一个向量,为什么不使用类型为{{{ 1}}?

如果每次调用方法时都必须在哈希表中查找向量,我的猜测是,与每次创建新向量相比,您将节省很少或没有时间。如果你在std :: map中查找向量,它将花费更长的时间。

答案 3 :(得分:1)

Q1:看看实施情况:

T * operator-> () const // never throws
{
    BOOST_ASSERT(px != 0);
    return px;
}

显然它正在返回一个成员变量而不是动态计算任何东西,因此性能将与解除引用普通指针一样快(受编译器优化的常见怪癖/未优化构建的性能总是可以预期 - 不值得考虑。)

Q2:“是否值得在map搜索vector地址并返回有效地址,而不是像std::vector<std::string> somevector那样在堆栈上创建一个地址。我也想要一个想法关于std::map::find的表现。“

是否值得它取决于必须在vector中复制的数据量,以及map中节点数量的较小范围,即公共前缀的长度。钥匙被比较等。一如既往,如果你关心,基准。但一般来说,如果向量包含大量数据(或者数据重新生成缓慢),我希望答案是肯定的。 std::map是一个平衡二叉树,所以通常你期望在O(log2N)中查找,其中N是当前元素数(即size())。

你也可以使用一个哈希表 - 它给出了O(1),它对于大量的元素来说会更快,但是不可能说出阈值在哪里。性能仍然取决于您在密钥上使用的哈希函数的昂贵程度,它们的长度(某些哈希实现,如Microsoft的std::hash只包含沿着被散列的字符串间隔的最多10个字符,因此所用的时间有一个上限但是大量更多的碰撞可能性),哈希表冲突处理方法(例如,用于搜索替代存储桶的位移列表与替代哈希函数与从存储桶链接的容器)以及碰撞倾向本身。