6

カスタムクラス(「単純な」クラスではない、たとえば、ではない、などではない)のインスタンスを多数格納する場合、単純なを選択する必要がありますか、それともより適切な選択ですか?std::stringstd::complexstd::vectorstd::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(std::unique_ptr利用できない場合)では、最良の選択はvector<shared_ptr<X>>;であるように思われます。代わりに、C ++ 11では、move-semantics-enabledstd::unique_ptrが最良の結果を提供するようです。

ここで何かが足りませんか?これは、クラスインスタンス自体よりもクラスインスタンスへvectorの(スマート)ポインタを格納する方が良いという、優れたC ++ガイドラインですか?

ベンチマークコードは次のとおりです。

////////////////////////////////////////////////////////////////////////////////
//
// 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

パフォーマンスを向上させるために、クラスインスタンスへのスマートポインタを大きなstd :: vectorに格納する必要がありますか?

クラスインスタンスが巨大で、それらを移動またはコピーするのに多くの作業が必要になる場合を除いて、これについて心配するのは時期尚早だと思います。各移動操作で数十バイトまたは数百バイトの移動が必要な場合は、はるかに大きな違いが生じます。とにかく、操作の大部分はベクトルオーバーヘッドです。

64ビットシステムを使用していると仮定します。今のところ、sizeof(MyObject)24に等しいと思います(とにかくここではそうです)。それ以外の場合は、おそらく12バイトのサイズの一意のポインター、または16のサイズの共有ポインターを処理することになります。

1,500,000回の操作で約0.3秒、または各操作で約200ナノ秒節約できます。それは本当に価値がありますか?あなたは本当にあなたのベクトルの何百万もの要素を扱うつもりですか?ベクトルへのポインターを格納し、それを共有するだけで全体を単純化できますか(コピーではなく移動セマンティクスを使用しているため、これを何らかの方法で機能させることができるはずです)。

私には時期尚早の最適化に非常によく似ているようです。つまり、いいえ、インスタンスへのベクトルではなく、インスタンスへのスマートポインタのベクトルを格納するべきではありません。まだ。

于 2012-11-14T18:24:04.067 に答える
3

それはあなたがそれをどのように使うかに依存します。アイテムを頻繁にコピーする場合は、ポインターをコピー/移動するだけでよいため、値の代わりにポインターを使用する方が高速です。ベクトルのメモリが再割り当てされると、すべてのアイテムを新しい場所に移動/コピーする必要があるため、アイテムを挿入するときはコピーが支配的であることに注意してください。

ベクター内のアイテムを読み取るか変更することが多い場合は、間接参照が1つ少なくなり、メモリの局所性が向上するため(CPUキャッシュの使用が向上するため)、アイテムを値として格納する方が高速になります。また、アイテムを直接保存すると、使用されるメモリが少なくなります。挿入が遅いという小さな欠点は、とを使用することで回避できreserveますemplace_back。そうすれば、ポインタのベクトルを使用するよりもおそらくさらに高速になります。

オブジェクトが他のコードによってポイントされる必要がない限り、私は常に値のベクトルを使用します。

于 2012-11-14T18:21:00.783 に答える
3

、移動が有効なオブジェクトを使用していないC++11場合は、からのベクトルを使用する必要があります。は軽量で、セマンティクスは似ていますが、重要な領域が1つ異なります。オブジェクトの所有権は明示的です。の場合、はそれが含むオブジェクトを所有します。ここで、移動が有効なオブジェクトを使用している場合は、オブジェクトのを使用するだけです。これは、通常、「十分に高速」であるためです。すべてのSTL対応コンテナーは、移動セマンティクスを利用します(つまり、少し遅くなりますが、生産性の面でメリットがあります)。パフォーマンスが問題になる場合は、以下に概説する理由でフォールバックできます。std::unique_ptr<T>#include <memory>std::unique_ptr<T>std::shared_ptr<T>vectorvectorvectorC++11std::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>移動コンストラクターはまだオブジェクトを割り当てているため(その内臓/コンテンツが借用、移動、再配置されている場合でも)、移動コンストラクターが移動するよりもコストがかかるのは驚きではありませんが、aの移動std::unique_ptr<T>は単なる整数/ポインターの割り当てです。jemalloc(3)を使用すると、Moveコンストラクターのコストが削減される可能性がありますが、これは*NIXプラットフォームでのみ使用できます。

その最後の点のために、ベンチマークは完全にリンゴ同士ではありません。一貫したパフォーマンスを探している場合std::unique_ptr<T>は、おそらく行く方法です(割り当てなし)が、パフォーマンスが最も重要な側面ではない簡単な開発方法論を促進する「ネイティブ」開発イディオムを探している場合(つまり、生産性はパフォーマンスよりも重要です)、次に、moveコンストラクターを使用して通常のオブジェクトを使用します。

于 2012-11-14T20:35:28.627 に答える
2

それはすべて、クラス自体とユースケースに依存します。ポリモーフィッククラスがある場合、(基本クラスへの)ポインタを格納する以外に方法はありません。これらのポインターは、生のポインターまたはスマートポインターにすることができます。最初のケースでは、ベクターから要素を削除したり破棄したりするたびに、適切なクリーンアップについて覚えておく必要があります。後者の方がユーザーフレンドリーであり、unique_ptr生のポインターと比較してオーバーヘッド(速度、サイズなど)を提供するべきではありません。shared_ptrは、メモリ(共有状態は24〜48バイトの場合があります)と速度(スレッドセーフな参照カウント)の点でかなりのオーバーヘッドを追加します。

あなたのクラスが多形性でない場合、それは再び依存します。クラスが小さいか移動しやすい場合(たとえば、データメンバーではなくデータへのポインターがある場合)、動的割り当て、解放、およびポインターの間接化の数が少ないため、値で格納するよりも優れているはずです。std::vectorキャッシュの局所性は、予測可能なパターンで進行する場合にも非常に役立ちます。ただし、クラスが巨大で移動が重い場合は、やはり状況によって異なります。それがPODタイプである場合、std::vectorおそらくmemmove非常に高速でパフォーマンスを低下させないメモリのコピーに使用されます。他のクラスの場合、copy(move)-コンストラクターがアイテムごとに呼び出され、パフォーマンスが低下します。このような場合、ポインタを使用するとパフォーマンスが向上する可能性があります。std::dequeまたは、またはのような他のコンテナの使用を検討することもできますstd::listこれにより、値によって格納される要素に必要なコピー数が制限されます。

于 2012-11-14T18:36:01.253 に答える