web-dev-qa-db-ja.com

マップに値を割り当てる最も効率的な方法

マップに値を割り当てる最も効率的な方法はどれですか?それとも、それらはすべて同じコードに最適化されていますか(ほとんどの最新のコンパイラーで)?

   // 1) Assignment using array index notation
   Foo["Bar"] = 12345;

   // 2) Assignment using member function insert() and STL pair
   Foo.insert(std::pair<string,int>("Bar", 12345));

   // 3) Assignment using member function insert() and "value_type()"
   Foo.insert(map<string,int>::value_type("Bar", 12345));

   // 4) Assignment using member function insert() and "make_pair()"
   Foo.insert(std::make_pair("Bar", 12345));

(コンパイラの出力をベンチマークして確認できることはわかっていますが、この質問が発生しました。手元にあるのは携帯電話だけです... hehe)

12
inquam

まず、_[]_とinsertの間には意味上の違いがあります。

  • _[]_は古い値(存在する場合)を置き換えます
  • insert willignore新しい値(古い値がすでに存在する場合)

したがって、2つを比較することは一般的に役に立たない。

インサートバージョンについて:

  • _std::map<std::string, int>::value_type_is_std::pair<std::string const, int>_なので、3と4の間に(重要な)違いはありません
  • std::make_pair("Bar", 12345)std::pair<std::string, int>("Bar", 12345)よりも安価です。これは、_std::string_型がコピーで実行する操作を備えた本格的なクラスであり、_"Bar"_が単なる文字列リテラルであるためです(したがって、ポインタコピー);ただし、最後に_std::string_...を作成する必要があるため、コンパイラの品質によって異なります。

一般的に、私はお勧めします:

  • _[]_更新用
  • 重複を無視するためのinsert(std::make_pair())

_std::make_pair_は短いだけでなく、DRYガイドライン:自分を繰り返さないでください。


ただし、完全を期すために、最速(かつ最も簡単)は emplace (C++ 11対応):

_map.emplace("Bar", 12345);
_

その動作はinsertの動作ですが、新しい要素をインプレースで構築します。

24
Matthieu M.

1)std::map::operator[]が最初にデフォルトであるため、他のメソッドよりも少し遅い場合があります-オブジェクトがまだ存在しない場合は作成し、次にoperator=を使用して目的の値を設定できる参照を返します。つまり、2つの操作。

2-4)map::value_typeは同じタイプのstd::pairとtypedefであるため、同等である必要があります。したがって、make_pairも同等です。コンパイラはこれらを同じように扱う必要があります。

また、map::lower_boundを使用して最初に場所のヒントを取得することにより、存在を確認し(たとえば、存在するかどうかに応じて特別なロジックを実行する)、挿入する必要がある場合は、パフォーマンスをさらに向上させることができます。要素はである必要があるため、map::insertmap全体を再度検索する必要はありません。

 // get the iterator to where the key *should* be if it existed:
 std::map::iterator hint = mymap.lower_bound(key);

 if (hint == mymap.end() || mymap.key_comp()(key, hint->first)) {
     // key didn't exist in map
     // special logic A here...

     // insert at the correct location
     mymap.insert(hint, make_pair(key, new_value));
 } else { 
     // key exists in map already
     // special logic B here...

     // just update value
     hint->second = new_value;
 }
2

あなたの最初の可能性:Foo["Bar"] = 12345;のセマンティクスは他のものとは多少異なります-存在しない場合は新しいオブジェクトを挿入しますが(他のオブジェクトと同様)、存在しない場合は上書き現在のコンテンツ(他のオブジェクトがinsertそのキーがすでに存在する場合は失敗します)。

速度に関しては、他の速度よりも遅くなる可能性があります。新しいオブジェクトを挿入すると、指定されたキーとデフォルトで作成されたvalue_typeを使用してペアが作成され、その後、正しいvalue_typeが割り当てられます。他のすべては、正しいキーと値のペアを構築し、そのオブジェクトを挿入します。ただし、公平を期すために、私の経験では、違いが重要になることはめったにありません(古いコンパイラでは、より重要でしたが、新しいコンパイラではごくわずかです)。

次の2つは互いに同等です。同じタイプに名前を付けるために2つの異なる方法を使用しているだけです。実行時では、それらの間にまったく違いはありません。

4つ目は、理論的にはcould追加レベルの関数呼び出しを伴うテンプレート関数(make_pair)を使用します。ただし、コンパイラが完全にno最適化(特にインライン化)を実行するように注意した場合を除いて、これとの実際の違いを見て非常に驚きます。

結論:最初のものは他のものより少し遅くなることがよくあります(しかし常にではなく、それほどではありません)。他の3つはほとんどの場合等しくなります(たとえば、通常、妥当なコンパイラーが3つすべてに対して同一のコードを生成することを期待します)。

1
Jerry Coffin

すでにいくつかの良い答えがありますが、私は簡単なベンチマークを実行したほうがよいと思いました。それぞれを500万回実行し、c ++ 11のクロノを使用して所要時間を測定しました。

コードは次のとおりです。

#include <string>
#include <map>
#include <chrono>
#include <cstdio>

// 5 million
#define times 5000000

int main()
{
    std::map<std::string, int> foo1, foo2, foo3, foo4, foo5;
    std::chrono::steady_clock::time_point timeStart, timeEnd;
    int x = 0;

    // 1) Assignment using array index notation
    timeStart = std::chrono::steady_clock::now();
    for (x = 0; x <= times; x++)
    {
        foo1[std::to_string(x)] = 12345;
    }
    timeEnd = std::chrono::steady_clock::now();
    printf("1) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());

    // 2) Assignment using member function insert() and STL pair
    timeStart = std::chrono::steady_clock::now();
    for (x = 0; x <= times; x++)
    {
        foo2.insert(std::pair<std::string, int>(std::to_string(x), 12345));
    }
    timeEnd = std::chrono::steady_clock::now();
    printf("2) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());

    // 3) Assignment using member function insert() and "value_type()"
    timeStart = std::chrono::steady_clock::now();
    for (x = 0; x <= times; x++)
    {
        foo3.insert(std::map<std::string, int>::value_type(std::to_string(x), 12345));
    }
    timeEnd = std::chrono::steady_clock::now();
    printf("3) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());

    // 4) Assignment using member function insert() and "make_pair()"
    timeStart = std::chrono::steady_clock::now();
    for (x = 0; x <= times; x++)
    {
        foo4.insert(std::make_pair(std::to_string(x), 12345));
    }
    timeEnd = std::chrono::steady_clock::now();
    printf("4) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());

    // 5) Matthieu M.'s suggestion of C++11's emplace
    timeStart = std::chrono::steady_clock::now();
    for (x = 0; x <= times; x++)
    {
        foo5.emplace(std::to_string(x), 12345);
    }
    timeEnd = std::chrono::steady_clock::now();
    printf("5) took %i milliseconds\n", (unsigned long long)std::chrono::duration_cast<std::chrono::milliseconds>(timeEnd-timeStart).count());

    return 0;
}

500万回の反復の出力は次のとおりです。

1) took 23448 milliseconds
2) took 22854 milliseconds
3) took 22372 milliseconds
4) took 22988 milliseconds
5) took 21356 milliseconds

GCCバージョン:

g++ (Built by MinGW-builds project) 4.8.0 20121225 (experimental)

私のマシン:

Intel i5-3570k overclocked at 4.6 GHz

EDIT1:コードを修正し、ベンチマークをやり直しました。

EDIT2:C++ 11のemplaceに対するMatthieuM。の提案を追加しました。彼は正しいです、emplaceが最速です

0
Edward A

そのキーの場所にオブジェクトがない場合は、次のようにします。

_std::map::emplace_ が最も効率的です。 insertは2番目です(ただし、非常に近くなります)。 _[]_は最も効率が悪いです。

_[]_、そこにオブジェクトがない場合、トリビアルはオブジェクトを構築します。次に、_operator=_を呼び出します。

insertは、_std::pair_引数に対してコピーコンストラクター呼び出しを行います。

ただし、マップの場合、map.insert( make_pair( std::move(key), std::move(value) ) )map.emplace( std::move(key), std::move(value) )に近くなります。

キーの場所にオブジェクトがある場合、_[]_は_operator=_を呼び出し、insert/emplaceは古いオブジェクトを破棄して新しいオブジェクトを作成します。この場合、_[]_は簡単に安くなる可能性があります。

結局のところ、それはあなたの_operator=_対コピー構築対トリビアル構築対デストラクタのコストがあなたのキーと価値に対して何であるかに依存します。

_std::map_のツリー構造内で物事を検索する実際の作業は、まったく同じに近いため、面白くありません。

3番目のものが最良の選択(IMHO)ですが、2、3、および4は同じです。

// 3) Assignment using member function insert() and "value_type()"
Foo.insert(map<string,int>::value_type("Bar", 12345));

3番目のものが最良の選択であると思う理由:値を挿入するために1つだけ操作を実行しています:挿入するだけで(まあ、検索もあります)、値が挿入されたかどうかを知ることができます、戻り値のsecondメンバーをチェックすると、実装は値を上書きしないことを許可します。

value_typeの使用にも利点があります。マップされたタイプやキータイプを知る必要がないため、テンプレートプログラミングで役立ちます。

最悪(IMHO)は最初のものです:

// 1) Assignment using array index notation
Foo["Bar"] = 12345;

オブジェクトを作成してそのオブジェクトへの参照を返すstd::map::operator[]を呼び出すと、マップされたオブジェクトoperator =が呼び出されます。挿入には2つの操作を実行します。1つ目は挿入、2つ目は割り当てです。

また、別の問題があります。値が挿入されたか上書きされたかがわかりません。

0
PaperBirdMaster