6

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

我写了一些基准代码(从这篇关于 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 中(wherestd::unique_ptr不可用),似乎最好的选择是vector<shared_ptr<X>>; 相反,在 C++11 中,启用移动语义std::unique_ptr似乎提供了最好的结果。

我在这里错过了什么吗?这是一个很好的 C++ 指南,在 big 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;
    }
}

////////////////////////////////////////////////////////////////////////////////
4

4 回答 4

3

Should we store smart pointers to class instances in large std::vector's for better performance?

Unless your class instances are gigantic and moving or copying them will require a lot of work, I'd consider this too early to worry about it. If each move operation requires moving dozens or hundreds of bytes, it will make a much more significant difference. As it is, the vast majority of the operation is vector overhead anyway.

I'll assume you're using a 64 bit system. Right now, sizeof(MyObject) will, I think, equal 24 (It does here anyway). Otherwise you'll be dealing with unique pointers, which are probably sized at 12 bytes, or shared pointers, which are, I think, are sized at 16.

You're saving about 0.3 seconds for 1,500,000 operations, or about 200 nanoseconds for each operation. Is it really worth it? Are you really going to be dealing with many millions of elements in your vector? Could you simplify the whole thing by just storing a pointer to the vector and sharing that (since you're using move semantics and not copy, you should be able to make this work somehow)?

Seems an awful lot like premature optimization to me. So I'm going to say, No, you shouldn't store a vector of smart pointers to instances instead of a vector to instances. Yet.

于 2012-11-14T18:24:04.067 回答
3

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

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

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

于 2012-11-14T18:21:00.783 回答
3

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

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

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

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

于 2012-11-14T20:35:28.627 回答
2

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

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

于 2012-11-14T18:36:01.253 回答