6

私は現在、パフォーマンスが非常に重要なプロジェクトの真っ最中です。以下は、この問題に関して私が持っていた質問の一部です。

質問1

私のプロジェクトには多くのものが含まれます。参照を追跡する必要があるため、多くのオーバーヘッドがあるためboost::shared_ptr、実行時に共有ポインターを作成するのboost::make_sharedが遅いことはわかっています.ブースト共有ポインターが既に作成されている場合、これら2つのステートメントはどうなるか知りたいと思いました.同じパフォーマンスであるか、一方が他方よりも高速になります。通常のポインターの方が高速で、既に共有ポインターがある場合、共有ポインターが指すメソッドを呼び出すためにどのようなオプションがありますか?

 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>毎回スタック上にを作成する、迅速に呼び出されるインスタンス メソッドがあります。そのベクトルポインターを static std::map (ie) に格納することにしましたstd::map<std::String,std::vector<std::string>*>。キーのマップにベクトルが存在しない場合 (メソッドの名前である可能性があります)。有効なベクター アドレスが作成され、マップに追加されます。したがって、私の質問は、「ベクター アドレスのマップを検索し、有効なアドレスをスタック上に作成するよりも有効なアドレスに戻す価値があるかどうかですstd::vector<std::string> somevectorstd::map検索のパフォーマンス。

これらの懸念に関するアイデアをいただければ幸いです。

4

4 に答える 4

13

Q#1 への回答

通常のポインターの方が高速で、既に共有ポインターがある場合、共有ポインターが指すメソッドを呼び出すにはどのようなオプションがありますか?

operator->boost::shared_ptr にアサーションがあります:

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いつTのコードFoo*ですか(怖がらないでください、私は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>());

ASMのコードtestは:Tboost::shared_ptr<Foo>

_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をスレッド同期なしで実装できます。これにより、参照カウントが高速化されます。

セマンティックを ( Boost.Moveunique_ptrまたは 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>());
    }
}

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<std::string,std::vector<std::string>*>、ある種のmemoizationを実行しようとしているようです。

すでに示唆されているように、std::mapこれはO(ln(N))ルックアップ/挿入の代わりに、/ を使用しようとすることができますboost::unordered_map/std::unordered_mapこれは、ルックアップ/挿入のO(1)平均とO(N)最悪の場合の複雑さ、およびより良い局所性/コンパクト性 (キャッシュフレンドリー)。

また、Boost.Flyweight を試してみてください:

Flyweightsは、共有共通データへの常時アクセスを許可する小さなサイズのハンドル クラスであるため、妥当なメモリ制限内で大量のエンティティを管理できます。Boost.Flyweightは、 const Tのドロップイン置換として機能するクラス テンプレートflyweightを提供することで、この一般的なプログラミング イディオムを簡単に使用できるようにします。

于 2013-04-12T05:58:25.997 に答える
4

質問 1 の場合:

主要なパフォーマンスの向上は、アーキテクチャ設計、使用されるアルゴリズムで達成できますが、低レベルの懸念も高レベルの設計が強力な場合にのみ重要です。あなたの質問に行きましょう、通常のポインターのパフォーマンスは shared_ptr よりも高いです。しかし、shared_ptr を使用しない場合に見られるオーバーヘッドの量も多くなり、長期的にはコードを維持するコストが増加します。パフォーマンスが重要なアプリケーションでは、冗長なオブジェクトの作成と破棄を避ける必要があります。このような場合、shared_ptr は、リソースを解放するオーバーヘッドを削減することで、スレッド間で共通オブジェクトを共有するという重要な役割を果たします。はい、共有ポインターは、refcount、allocation(object、counter、deleter) などのために、通常のポインターよりも多くの時間を消費します。それらの不要なコピーを防ぐことで、shared_ptr を高速化できます。ref(shared_ptr const&) として使用します。

質問2

shared_ptr オブジェクトの再利用プールを使用する場合は、オブジェクト プールの設計パターン アプローチを検討することをお勧めします。 http://en.wikipedia.org/wiki/Object_pool_pattern

于 2013-04-12T05:50:29.350 に答える
1

Q1: 実装を見てください:

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

明らかに、メンバー変数を返し、その場で何も計算しないため、パフォーマンスはプレーンポインターを逆参照するのと同じくらい高速になります(コンパイラーの最適化の通常の癖の影響を受けます/最適化されていないビルドのパフォーマンスは常に低下すると予想されます-考慮する価値はありません)。

mapQ2: 「 のようにスタック上にアドレスを作成するだけでなく、アドレスを検索しvectorて有効なアドレスを返す価値はありますかstd::vector<std::string> somevector。また、 のパフォーマンスに関するアイデアも欲しいですstd::map::find。」

それが価値があるかどうかは、 にコピーする必要があるデータの量vector、および のノード数、map比較されるキーの一般的なプレフィックスの長さなどによって異なります。基準。ただし、一般的には、ベクトルに大量のデータが含まれている場合 (またはそのデータの再生成が遅い場合) は、答えは「はい」であると予想されます。 std::mapはバランス バイナリ ツリーであるため、通常、N は現在の要素数 (つまり ) である O(log2N) でのルックアップが期待されますsize()

ハッシュ テーブルを使用することもできます。これにより、膨大な数の要素に対してより高速になる O(1) が得られますが、しきい値がどこにあるかを言うことは不可能です。パフォーマンスは、キーで使用するハッシュ関数のコスト、キーの長さに依存します (Microsoft のような一部のハッシュ実装では、ハッシュさstd::hashれる文字列に沿って最大 10 文字の間隔しか組み込まれていないため、かかる時間には上限がありますが、衝突の可能性が大幅に高くなります)。 、ハッシュ テーブル衝突処理アプローチ (たとえば、代替バケットを検索するための置換リスト、代替ハッシュ関数、バケットからチェーンされたコンテナー)、および衝突の傾向自体。

于 2013-04-12T06:01:51.433 に答える