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



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

问题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

我有一个实例方法(s),它被快速调用,每次在堆栈上创建一个std::vector<std::string>。我决定将矢量指针存储在静态std::map(即std::map<std::String,std::vector<std::string>*>)中。如果键的映射中不存在向量(可以是方法名)。创建有效的矢量地址并将其添加到映射中。所以我的问题是"是否值得在映射中搜索向量地址并返回有效地址,而不是像std::vector<std::string> somevector那样在堆栈上创建一个。"我还想对std::map find的性能有一个想法。

如果有任何关于这些问题的想法,我将不胜感激。

回答问# 1

如果普通指针更快,我已经有了共享指针,为了调用共享指针指向的方法,我有什么选择?

boost::shared_ptr中的operator->有断言:

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);

testASM代码当TFoo*是(不要害怕,我下面有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>());

Tboost::shared_ptr<Foo>testASM code:

_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)。移动或c++ 11) -那么就不会有任何引用计数-它会更快。

ASM输出的实时演示

#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>());
    }
}

对问题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>*>,您正在尝试执行某种记忆。

如前所述,std::map具有O(ln(N))查找/插入,您可以尝试使用boost::unordered_map/std::unordered_map,它具有O(1)平均值和O(N)最坏情况的查找/插入复杂度,以及更好的局域性/紧凑性(缓存友好)。

另外,考虑尝试Boost。轻量级选手:

Flyweights是小型句柄类,授予对共享公共数据的恒定访问,从而允许在合理的内存限制内管理大量实体。提振。Flyweight通过提供类模板 Flyweight使使用这种常见的编程风格变得容易,它充当const T的临时替代品。

For Question1:

主要的性能增益可以在架构设计中实现,使用算法,而低层关注也只有在高层设计强大时才重要。让我们回到你的问题,常规指针的性能比shared_ptr更高。但是,不使用shared_ptr的开销也更多,这增加了长期运行时维护代码的成本。在性能关键型应用程序中必须避免冗余的对象创建和销毁。在这种情况下,shared_ptr在跨线程共享公共对象方面起着重要作用通过减少释放资源的开销。是的,共享指针比普通指针消耗更多的时间,因为重计数,分配(对象,计数器,删除器)等。您可以通过防止不必要的复制来提高shared_ptr的速度。使用它作为ref(shared_ptr const&)。此外,如果你不需要跨线程共享资源,不要使用shared_ptr,在这种情况下,常规ptr会提供更好的性能。

问题2

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

问题1:

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

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

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

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

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

这种方法在我的项目中被证明是有效的,但说实话,我从来没有真正测量过它带来的性能差异。

另外,在可能的情况下,建议使用auto_ptr (c++ 03)或unique_ptr (c++ 11)。

问题2:

我不明白你为什么要考虑使用std::map。首先,hash_map会更快(只要它不是vc++ Dinkumware在VS2008/2010中的实现,细节在这里某处),其次,如果每个方法只需要一个向量,为什么不使用std::vector<std::string>类型的静态变量?

如果每次调用该方法时都必须在哈希表中查找向量,我的猜测是,与每次创建新向量相比,您将节省很少或根本没有时间。如果在std::map中查找vector对象,则需要更长的时间。

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),对于大量的元素来说,这将更快,但不可能说阈值在哪里。性能仍然取决于你在键上使用的哈希函数的开销,它们的长度(一些哈希实现,如微软的std::hash,只包含沿被哈希字符串间隔的最多10个字符,因此所花费的时间有上限,但更有可能发生冲突),哈希表冲突处理方法(例如,置换列表搜索备选桶、备选哈希函数、从桶链出的容器),以及碰撞倾向性。

相关内容

  • 没有找到相关文章

最新更新