【问题标题】:Should we store smart pointers to class instances in large std::vector's for better performance?我们是否应该将指向类实例的智能指针存储在大型 std::vector 中以获得更好的性能?
【发布时间】:2012-11-03 07:11:54
【问题描述】:

当在std::vector 中存储大量 个自定义类(不是“简单”类,例如不是std::string,不是std::complex 等)的实例时,我们应该选择简单的std::vector<X>,还是std::vector<std::unique_ptr<X>> 更好?

我编写了一些基准代码(从 this blog post 扩展代码,关于 C++03 上的 C++11 移动语义改进),似乎 vector<unique_ptr<X>> 为 1,500,000 项向量提供了更好的性能。事实上,在装有 Windows 7 64 位、Intel Core i5 四核 CPU 和 8 GB RAM 的 PC 上,我得到了以下结果 (test.exe 1500):

  1. vector<unique_ptr<MyObject>>:1.5 秒
  2. vector<shared_ptr<MyObject>>:1.6 秒
  3. vector<MyObject>:1.8 秒

所以,在C++03中(std::unique_ptr不可用),似乎最好的选择是vector<shared_ptr<X>>;而在 C++11 中,启用移动语义的 std::unique_ptr 似乎提供了最好的结果。

我在这里遗漏了什么吗?这是一个很好的 C++ 指南吗?在大 vectors 中存储(智能)指针指向类实例比类实例本身更好?

基准代码如下:

////////////////////////////////////////////////////////////////////////////////
//
// Test vector<X> vs. vector<unique_ptr<X>> vs. vector<shared_ptr<X>>.
//
// Original benchmark code from:
//   http://blogs.msdn.com/b/vcblog/archive/2009/06/23/stl-performance.aspx
//
////////////////////////////////////////////////////////////////////////////////


#include <exception>    // std::invalid_argument
#include <iostream>     // std::cout
#include <memory>       // std::shared_ptr, std::unique_ptr
#include <ostream>      // std::endl
#include <stdexcept>    // std::exception
#include <string>       // std::wstring
#include <utility>      // std::move
#include <vector>       // std::vector

#include <Windows.h>    // Win32 Platform SDK (high performance counters, etc.)

using namespace std;


// Measure time.
class Stopwatch
{
public:

    Stopwatch()
        : m_start(0),
          m_finish(0)
    {
    }

    static void PerfStartup()
    {
        // to confine the test to run on a single processor 
        // in order to get consistent results for all tests.
        SetThreadAffinityMask(GetCurrentThread(), 1);
        SetThreadIdealProcessor(GetCurrentThread(), 0);
        Sleep(1);
    }

    void Start()
    {
        m_finish = 0;
        m_start = Counter();
    }

    void Stop()
    {
        m_finish = Counter();
    }

    // Elapsed time, in seconds
    double ElapsedTime() const
    {
        return (m_finish - m_start) * 1.0 / Frequency();
    }

    void Reset()
    {
        m_start = m_finish = 0;
    }


private:
    long long m_start;
    long long m_finish;

    static long long Counter() 
    {
        LARGE_INTEGER li;
        QueryPerformanceCounter(&li);
        return li.QuadPart;
    }

    static long long Frequency() 
    {
        LARGE_INTEGER li;
        QueryPerformanceFrequency(&li);
        return li.QuadPart;
    }


// Ban copy
private:
    Stopwatch(const Stopwatch&);
    Stopwatch& operator=(const Stopwatch&);
};


// Measure execution time of a block of code.
class ScopedStopwatch
{
public:

    ScopedStopwatch()
    {
        m_sw.Start();
    }

    ~ScopedStopwatch()
    {
        m_sw.Stop();
        cout << "Elapsed time: " << m_sw.ElapsedTime() << " sec" << endl;
    }

private:
    Stopwatch m_sw;

    ScopedStopwatch(const ScopedStopwatch&);
    ScopedStopwatch& operator=(const ScopedStopwatch&);
};


// User Defined Type
class MyObject
{
public:
    wstring name;
    wstring address;
    wstring telephone;
    wstring name2;
    wstring address2;
    wstring telephone2;

    // Default constructor
    MyObject()
    {
    }

    // Copy Constructor
    MyObject(const MyObject& other)
        : name(other.name),
          telephone(other.telephone),
          address(other.address),
          name2(other.name2),
          telephone2(other.telephone2),
          address2(other.address2)
    {
    }

    // Copy assignment operator
    MyObject& operator=(const MyObject& other)
    {
        if (this != &other)
        {
            name = other.name;
            telephone = other.telephone;
            address = other.address;
            name2 = other.name2;
            telephone2 = other.telephone2;
            address2 = other.address2;
        }

        return *this;
    }

    // Move constructor
    MyObject(MyObject&& other)
        : name(move(other.name)),
          telephone(move(other.telephone)),
          address(move(other.address)),
          name2(move(other.name2)),
          telephone2(move(other.telephone2)),
          address2(move(other.address2))
    {
    }

    // Move assignment operator
    MyObject& operator=(MyObject&& other)
    {
        if (this != &other)
        {
            name = move(other.name);
            telephone = move(other.telephone);
            address = move(other.address);
            name2 = move(other.name2);
            telephone2 = move(other.telephone2);
            address2 = move(other.address2);
        }

        return *this;
    }
};


MyObject MakeTestObject()
{
    MyObject obj;
    obj.name = L"Stephan T. Lavavej Stephan T. Lavavej Stephan T. Lavavej";
    obj.telephone = L"314159265 314159265 314159265 314159265 314159265";
    obj.address = L"127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0";
    obj.name2 = L"Mohammad Usman. Mohammad Usman. Mohammad Usman. ";
    obj.telephone2 = L"1234567890 1234567890 1234567890 1234567890 1234567890";
    obj.address2 = L"Republik Of mancunia. Republik Of mancunia Republik Of mancunia";

    return obj;
}


unique_ptr<MyObject> MakeUniqueTestObject()
{
    unique_ptr<MyObject> obj( new MyObject() );
    obj->name = L"Stephan T. Lavavej Stephan T. Lavavej Stephan T. Lavavej";
    obj->telephone = L"314159265 314159265 314159265 314159265 314159265";
    obj->address = L"127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0";
    obj->name2 = L"Mohammad Usman. Mohammad Usman. Mohammad Usman. ";
    obj->telephone2 = L"1234567890 1234567890 1234567890 1234567890 1234567890";
    obj->address2 = L"Republik Of mancunia. Republik Of mancunia Republik Of mancunia";

    return obj;
}


shared_ptr<MyObject> MakeSharedTestObject()

{    
    auto obj = make_shared<MyObject>();
    obj->name = L"Stephan T. Lavavej Stephan T. Lavavej Stephan T. Lavavej";
    obj->telephone = L"314159265 314159265 314159265 314159265 314159265";
    obj->address = L"127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0 127.0.0.0";
    obj->name2 = L"Mohammad Usman. Mohammad Usman. Mohammad Usman. ";
    obj->telephone2 = L"1234567890 1234567890 1234567890 1234567890 1234567890";
    obj->address2 = L"Republik Of mancunia. Republik Of mancunia Republik Of mancunia";

    return obj;
}


void Test(int count)
{
    Stopwatch::PerfStartup();

    cout << "Inserting " << count << " items in vector.\n";


    cout << "\nTesting vector<MyObject>\n";
    {
        ScopedStopwatch sw;

        vector<MyObject> v;
        for (int i = 0; i < count; i++)
        {
            v.push_back(MakeTestObject());
        }
    }


    cout << "\nTesting vector<unique_ptr<MyObject>>\n";
    {
        ScopedStopwatch sw;

        vector<unique_ptr<MyObject>> v;
        for (int i = 0; i < count; i++)
        {
            v.push_back(MakeUniqueTestObject());
        }
    }


    cout << "\nTesting vector<shared_ptr<MyObject>>\n";
    {
        ScopedStopwatch sw;

        vector<shared_ptr<MyObject>> v;
        for (int i = 0; i < count; i++)
        {
            v.push_back(MakeSharedTestObject());
        }
    }
}


int main(int argc, char * argv[])
{
    static const int kExitOk = 0;
    static const int kExitError = 1;

    try
    {
        if (argc != 2)
        {
            throw invalid_argument("Bad syntax. Pass insertion count (x 1,000).");
        }

        const int countK = atoi(argv[1]);
        Test(countK * 1000);

        return kExitOk;
    }
    catch (const exception & e)   
    {
        cerr << "*** ERROR: " << e.what() << endl;
        return kExitError;
    }
}

////////////////////////////////////////////////////////////////////////////////

【问题讨论】:

  • 在可能的情况下,reserve 向量中需要的空间通常更简单,或者使用deque 而不是vector。可能值得将这两个选项添加到您的基准中进行比较,但对于特定的用例,它们中的任何一个都可能是不可能的。我认为deque 可能比vector&lt;unique_ptr&gt; 的破坏性更小。也可以看boost:stable_vector
  • 我想性能是一样的,但你试过MyObject(MyObject&amp;&amp; src) = default 看看内置的移动构造函数是否有效?我没有看到任何需要手动移动东西的特殊内存管理标准。
  • 另外,当您说“禁止复制”时,只需 = delete 那些方法。例如。 Stopwatch(const Stopwatch&amp;) = delete;
  • @Sean:我正在使用 VC10 (VS2010 SP1),不幸的是它支持非常原始的 C++11;默认和删除函数aren't available 即使在 VC11 (VS2012) 中也是如此。
  • @Mr.C64 Drat,我担心你会说这样的话。此外,如果您想比较移动语义,您应该将复制构造函数作为基线。与普通的复制 ctor 相比,Move 非常好。

标签: c++ performance vector c++11 move-semantics


【解决方案1】:

C++11 中,如果您没有使用启用移动的对象,那么您应该使用来自#include &lt;memory&gt;std::unique_ptr&lt;T&gt; 向量。 std::unique_ptr&lt;T&gt; 重量更轻,具有与std::shared_ptr&lt;T&gt; 相似的语义,但在一个重要方面有所不同:对象所有权是明确的。对于vectorvector 拥有它包含的对象。现在,如果您正在使用启用移动的对象,只需使用对象的vector,因为它通常“足够快”。 C++11 中所有启用 STL 的容器都使用了移动语义(即,是的,速度稍慢,但您会在生产力方面有所收获)。如果性能是一个问题,您可以回退到std::unqiue_ptr&lt;T&gt;,原因如下所述。

如果您使用的是 C++11 之前的版本,boost::shared_ptr&lt;T&gt; 并不是您可以做的最糟糕的事情,并且可能是一个合适的过渡路径,直到您可以使用 std::unique_ptr&lt;T&gt;boost::shared_ptr&lt;T&gt; 的使用涉及原子增量和指针分配。两者都非常便宜,但比 std::unique_ptr&lt;T&gt; 更昂贵(并且语义不同)。

Move 构造函数比移动 std::unique_ptr&lt;T&gt; 更昂贵并不让我感到惊讶,因为移动构造函数仍在分配对象(即使它的内脏/内容正在被借用、移动、重定位),而移动 @ 987654337@ 只是一个整数/指针赋值。使用 jemalloc(3) 可能会降低 Move 构造函数的成本,但这仅适用于 *NIX 平台。

由于最后一点,基准并不完全是苹果对苹果。如果您正在寻找一致的性能,std::unique_ptr&lt;T&gt; 可能是要走的路(无分配),但如果您正在寻找一种“本机”开发习惯,可以促进简单的开发方法,其中性能不是最重要的方面(即生产力比性能更重要),然后使用带有移动构造函数的普通对象。

【讨论】:

  • 我认为你的回答有误:boost::shared_pointer&lt;T&gt;(在第二段中)应该是boost::shared_ptr&lt;T&gt;
【解决方案2】:

我们是否应该将指向类实例的智能指针存储在大型 std::vector 中以获得更好的性能?

除非您的类实例非常庞大并且移动或复制它们需要大量工作,否则我认为这为时过早。如果每个移动操作都需要移动几十个或几百个字节,那将产生更显着的差异。事实上,绝大多数操作都是矢量开销。

我假设您使用的是 64 位系统。现在,sizeof(MyObject) 将,我认为,等于 24(它确实是 here)。否则,您将处理可能大小为 12 字节的唯一指针,或大小为 16 的共享指针。

您为 1,500,000 次操作节省了大约 0.3 秒,或者为每个操作节省了大约 200 纳秒。是不是真的值得吗?您真的要处理向量中的数百万个元素吗?你能通过存储一个指向向量的指针并共享它来简化整个事情吗(因为你使用的是移动语义而不是复制,你应该能够以某种方式完成这项工作)?

在我看来,这很像过早的优化。所以我要说,不,你不应该存储指向实例的智能指针向量而不是指向实例的向量。然而。

【讨论】:

  • 相关的是sizeof(MyObject),而不是sizeof(StopWatch)。移动 6 个字符串将比移动一个 unique_ptr 更昂贵,这就是基准测试结果似乎表明的。 +1 虽然:我同意 15-20% 的速度提升不值得提问者提出的“良好的 C++ 指南”。 可能让你的容器复杂化是值得的,但考虑到你为它付出的代码复杂性其他地方可能的性能成本,这通常是不值得的。
  • 哎呀。我刚刚使用了我在代码中看到的第一个类。他们不会那么大。 wstring 对象的大小为 4。ideone.com/VKco7L 使用移动语义而不是副本,移动 wstring 应该像复制它的 4 个字节一样简单。
  • 当然,这与移动unique_ptr 大致相同,并且提问者实际上将这样的移动与 6 进行了基准测试。不过,在像提问者这样的 64 位机器上,这个大小会令人印象深刻;- )
  • 我相信 stl 字符串使用某种索引和间接方式。否则,他们的所作所为是不可能奏效的。
【解决方案3】:

这取决于你如何使用它。如果您经常复制项目,那么使用指针而不是值会更快,因为您只需复制/移动指针。请注意,在插入项目时,复制占主导地位,因为当重新分配向量的内存时,所有项目都必须移动/复制到新位置。

当您更多地只是读取或修改向量中的项目时,将项目存储为值会更快,因为间接性更少并且内存局部性更好(更好地使用 CPU 缓存)。如果直接存储项目,使用的内存也会更少。使用reserveemplace_back 可以避免插入速度较慢的小缺点。那么它很可能会比使用指针向量更快。

除非我需要其他代码指向对象,否则我将始终使用值向量。

【讨论】:

    【解决方案4】:

    这一切都取决于类本身和用例。如果您有一个多态类,那么除了存储指针(指向基类)之外别无他法。这些指针可以是原始指针或智能指针。在第一种情况下,每次从向量中删除元素或销毁它时,您都必须记住正确的清理。后者对用户更友好,与原始指针相比,unique_ptr 不应提供任何开销(速度、大小等)。 shared_ptr 将在内存(共享状态可能有 24-48 个字节)和速度(线程安全引用计数)方面增加大量开销。

    如果你的类不是多态的,那它又取决于。如果您的类很小或易于移动(例如,具有指向数据而不是数据成员的指针),那么按值存储它应该更好,因为动态分配、释放和指针间接的数量较少。如果您以可预测的模式通过std::vector,缓存位置也会对您有很大帮助。但是,如果您的班级很大,因此移动起来很重,这又取决于。如果它是 POD 类型,std::vector 可能会使用 memmove 来复制非常快的内存,并且不会影响您的性能。对于其他类,将为每个项目调用 copy(move)-constructor,这将花费你一些性能。在这种情况下,使用指针可能会为您提供更好的性能。或者,您可以考虑使用其他容器,例如 std::dequestd::list,它们会限制按值存储的元素所需的副本数。

    【讨论】:

    • 我的意思是与原始指针相比“没有开销”。我认为这还不够清楚,所以我将编辑答案。谢谢。
    猜你喜欢
    • 1970-01-01
    • 2016-02-19
    • 2017-12-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-20
    • 2016-10-09
    • 1970-01-01
    相关资源
    最近更新 更多