【问题标题】:Smart pointer wrapping penalty. Memoization with std::map智能指针环绕惩罚。使用 std::map 进行记忆
【发布时间】:2013-04-04 12:29:39
【问题描述】:

我目前正在进行一个对性能至关重要的项目。以下是我对这个问题的一些疑问。

问题1

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

 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::mapfind性能的想法。

任何关于这些问题的想法都将不胜感激。

【问题讨论】:

  • Donald Knuth:“我们应该忘记小的效率,说大约 97% 的时间:过早的优化是万恶之源” => 首先配置文件,识别热点,优化 那些.

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


【解决方案1】:

对于问题1:

在架构设计、所使用的算法以及低层次的关注点只有在高层次的设计很强大的情况下,才能实现主要的性能提升。 让我们来回答您的问题,常规指针性能高于 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 更快。将其用作 ref(shared_ptr const&)。”这会有很大的不同!
  • 感谢您的回答,您是否建议我将指针作为参考传递,以防止复制?我在几个安静的地方传递它..
  • @Rajeshwar 是的,只有在共享内存时才使用 shared_ptr 。如果不需要共享内存,您可以使用 unique_ptr 或任何其他智能指针方法。此外,如果您的对象创建/删除消耗更多时间,请将这些对象创建放在对象池中,以便您在应用程序开始时创建对象池,并且您可以重用这些对象而无需一次又一次地重新创建它们。
  • 正如你所建议的,我已经在使用对象池了。我使用 shared_ptr 的唯一原因是出于代码维护的目的,这样我就不必担心自己删除指针。
  • @Rajeshwar 如果您不需要自己删除指针,那么您可以使用 unique_ptr。此智能指针拥有对象的单一所有权,并且比 shared_ptr(管理对象的多个所有者)更快。 msdn.microsoft.com/en-us/library/vstudio/ee410601.aspx
【解决方案2】:

Q#1 的答案

如果常规指针更快并且我已经拥有共享指针,我有哪些选项可以调用共享指针指向的方法?

operator->boost::shared_ptr has assertion:

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 的代码当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&lt;Foo&gt;testASM 代码:

_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&lt;std::string,std::vector&lt;std::string&gt;*&gt;,您正试图执行某种memoization

正如已经建议的那样,您可以尝试使用具有 O(ln(N)) 查找/插入的 std::map 而不是具有 O( 1) 查找/插入的平均和 O(N) 最坏情况复杂度,以及更好的局部性/紧凑性(缓存友好)。

另外,一起试试Boost.Flyweight

享元是小型句柄类,授予对共享公共数据的持续访问权限,从而允许在合理的内存限制内管理大量实体。 Boost.Flyweight 通过提供类模板 flyweight 可以轻松使用这种常见的编程习惯,该类模板可替代 const T

【讨论】:

    【解决方案3】:

    Q1:只看实现:

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

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

    Q2:“是否值得在map 中搜索vector 地址并返回一个有效地址,而不是像std::vector&lt;std::string&gt; somevector 这样在堆栈上创建一个地址。我还想了解@987654325 的性能@。”

    是否值得取决于必须在vector 中复制的数据量,以及在较小程度上map 中的节点数、被比较的键中公共前缀的长度等.. 与往常一样,如果您关心,请进行基准测试。不过,一般来说,如果向量包含大量数据(或者该数据的再生速度很慢),我希望答案是肯定的。 std::map 是一棵平衡二叉树,因此通常您希望在 O(log2N) 中查找,其中 N 是当前元素数(即 size())。

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

    【讨论】:

      【解决方案4】:

      问题 1:

      我在我的项目中广泛使用共享指针,但我不想使用shared_ptr&lt;T&gt;。它需要一个与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 会更快(只要不是 VS2008/2010 中的 VC++ Dinkumware 实现,details in here somewhere),其次,如果每个方法只需要一个向量,为什么不使用std::vector&lt;std::string&gt; 类型的静态变量?

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

      【讨论】:

      • " 它需要一个与 T 本身分开分配的堆对象,因此内存分配开销增加了一倍" - 您可以使用 make_shared 避免这种情况
      • "void Foo(intrusive_ptr&lt;T&gt; x)每次调用Foo时,参数x的引用计数必须以相对昂贵的原子增量递增"——当你使用intrusive_ptr时——可以实现increment/ decrement 你自己,如果你不需要线程同步,你不必使用 atomic 递增/递减。
      • @Evgeny 你说得对,我忘了。但是,如果类型 T 的 intrusive_ptr_add_ref() 确实支持原子增量,那么就会发生这种情况。当 intrusive_ptr 作为参数传递给方法时,您不能要求“以原子方式递增 - except”。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-11-03
      • 1970-01-01
      • 1970-01-01
      • 2014-06-17
      • 2011-08-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多