web-dev-qa-db-ja.com

std :: multimapを使用することが理にかなっている場合

私は現在、stl-datastructuresのいくつかの使用法を実験しています。ただし、どの組み合わせをいつ使用し、特定の組み合わせをいつ使用するかはまだわかりません。現在、私はstd::multimapを使用することが理にかなっているのかを理解しようとしています。私の知る限り、std::mapstd::vectorを組み合わせることで、独自のマルチマップ実装を簡単に構築できます。したがって、これらの各データ構造をいつ使用するべきかという疑問が残ります。

  • 単純性:追加のネストを処理する必要がないため、std :: multimapは間違いなく使用が簡単です。ただし、要素の範囲に一括でアクセスするには、イテレーターから別のデータ構造(std::vectorなど)にデータをコピーする必要がある場合があります。
  • 速度:キャッシュの使用が最適化されているため、ベクトルの局所性により、等しい要素の範囲での反復処理がはるかに速くなります。ただし、std::multimapsには、同等の要素をできるだけ高速に反復処理するために、最適化の裏技がたくさんあると思います。また、正しい要素範囲に到達することは、おそらくstd::multimaps用に最適化される可能性があります。

速度の問題を試すために、次のプログラムを使用して簡単な比較を行いました。

#include <stdint.h>
#include <iostream>
#include <map>
#include <vector>
#include <utility>

typedef std::map<uint32_t, std::vector<uint64_t> > my_mumap_t;

const uint32_t num_partitions = 100000;
const size_t num_elements =     500000;

int main() {
  srand( 1337 );
  std::vector<std::pair<uint32_t,uint64_t>> values;
  for( size_t i = 0; i <= num_elements; ++i ) {
    uint32_t key = Rand() % num_partitions;
    uint64_t value = Rand();
    values.Push_back( std::make_pair( key, value ) );
  }
  clock_t start;
  clock_t stop;
  {
    start = clock();
    std::multimap< uint32_t, uint64_t > mumap;
    for( auto iter = values.begin(); iter != values.end(); ++iter ) {
      mumap.insert( *iter );
    }
    stop = clock();
    std::cout << "Filling std::multimap: " << stop - start << " ticks" << std::endl;
    std::vector<uint64_t> sums;
    start = clock();
    for( uint32_t i = 0; i <= num_partitions; ++i ) {
      uint64_t sum = 0;
      auto range = mumap.equal_range( i );
      for( auto iter = range.first; iter != range.second; ++iter ) {
        sum += iter->second;
      }
      sums.Push_back( sum );
    }
    stop = clock();
    std::cout << "Reading std::multimap: " << stop - start << " ticks" << std::endl;
  }
  {
    start = clock();
    my_mumap_t mumap;
    for( auto iter = values.begin(); iter != values.end(); ++iter ) {
      mumap[ iter->first ].Push_back( iter->second );
    }
    stop = clock();
    std::cout << "Filling my_mumap_t: " << stop - start << " ticks" << std::endl;
    std::vector<uint64_t> sums;
    start = clock();
    for( uint32_t i = 0; i <= num_partitions; ++i ) {
      uint64_t sum = 0;
      auto range = std::make_pair( mumap[i].begin(), mumap[i].end() );
      for( auto iter = range.first; iter != range.second; ++iter ) {
        sum += *iter;
      }
      sums.Push_back( sum );
    }
    stop = clock();
    std::cout << "Reading my_mumap_t: " << stop - start << " ticks" << std::endl;
  }
}

私が疑ったように、それは主にnum_partitionsnum_elementsの比率に依存するので、私はまだここで途方に暮れています。次に出力例をいくつか示します。

num_partitions = 100000およびnum_elements = 1000000の場合

Filling std::multimap: 1440000 ticks
Reading std::multimap: 230000 ticks
Filling    my_mumap_t: 1500000 ticks
Reading    my_mumap_t: 170000 ticks

num_partitions = 100000およびnum_elements = 500000の場合

Filling std::multimap: 580000 ticks
Reading std::multimap: 150000 ticks
Filling    my_mumap_t: 770000 ticks
Reading    my_mumap_t: 140000 ticks

num_partitions = 100000およびnum_elements = 200000の場合

Filling std::multimap: 180000 ticks
Reading std::multimap:  90000 ticks
Filling    my_mumap_t: 290000 ticks
Reading    my_mumap_t: 130000 ticks

num_partitions = 1000およびnum_elements = 1000000の場合

Filling std::multimap: 970000 ticks
Reading std::multimap: 150000 ticks
Filling    my_mumap_t: 710000 ticks
Reading    my_mumap_t:  10000 ticks

これらの結果をどのように解釈するかは不明です。どのようにして正しいデータ構造を決定しますか?私が見逃したかもしれない決定のための追加の制約はありますか?

38
LiKao

ベンチマークが正しいことを行っているかどうかを判断するのは難しいため、数値についてコメントすることはできません。ただし、いくつかの一般的なポイント:

  • ベクトルのマップではなくmultimapの理由:マップ、マルチマップ、セット、マルチセットはすべて基本的に同じデータ構造であり、1つを取得したら、4つすべてをスペルアウトするのは簡単です。それで、最初の答えは、「なぜnot持っているのか」です。

  • それはどのように役立つのですか:マルチマップは、めったに必要としないものの1つですが、必要なときに本当に必要です。

  • なぜ私自身のソリューションをロールバックしないのですか?言ったように、それらのベンチマークについてはわかりませんが、ifでも、標準よりも悪くない何かを作成できますコンテナ(私が質問します)の場合は、適切に設定し、テストし、維持する全体的な負担を考慮する必要があります。あなたが書いたすべてのコード行に対してtaxedになる世界を想像してみてください(それがStepanovの提案です)。可能な限り、業界標準のコンポーネントを再利用してください。

最後に、マルチマップを反復する一般的な方法を次に示します。

for (auto it1 = m.cbegin(), it2 = it1, end = m.cend(); it1 != end; it1 = it2)
{
  // unique key values at this level
  for ( ; it2 != end && it2->first == it1->first; ++it2)
  {
    // equal key value (`== it1->first`) at this level
  }
}
26
Kerrek SB

非常に重要な代替案を1つ忘れてしまいました。すべてのシーケンスが等しく作成されるわけではありません。

特に、なぜvectorではなくdequelistではないのですか?

listを使用する

listもノードベースであるため、std::map<int, std::list<int> >std::multimap<int, int>とほぼ同等に実行する必要があります。

dequeを使用する

dequeは、どちらを使用するかわからず、特別な要件がない場合に使用するデフォルトのコンテナーです。

vectorについては、より高速なPushおよびpopオペレーションと、ある程度の読み取り速度(それほどではありません)を交換します。

代わりにdequeを使用し、 明らかな最適化 を使用すると、次のようになります。

const uint32_t num_partitions = 100000;
const size_t num_elements =     500000;

Filling std::multimap: 360000 ticks
Filling MyMumap:       530000 ticks

Reading std::multimap: 70000 ticks (0)
Reading MyMumap:       30000 ticks (0)

または「悪い」場合:

const uint32_t num_partitions = 100000;
const size_t num_elements =     200000;

Filling std::multimap: 100000 ticks
Filling MyMumap:       240000 ticks

Reading std::multimap: 30000 ticks (0)
Reading MyMumap:       10000 ticks (0)

したがって、読み取りは無条件に速くなりますが、充填も非常に遅くなります。

8
Matthieu M.

ベクトルのマップには、各ベクトルの容量のメモリオーバーヘッドが付属しています。 std::vectorは通常、実際よりも多くの要素にスペースを割り当てます。これはアプリケーションにとって大した問題ではないかもしれませんが、考慮していないもう1つのトレードオフです。

多くの読み取りを行う場合は、O(1)ルックアップ時間unordered_multimapの方が適切な選択かもしれません。

合理的に最新のコンパイラーを使用している場合(そしてautoキーワードがある場合はそうします)、一般に、パフォーマンスと信頼性の点で標準コンテナーを打ち負かすのは難しいでしょう。それらを書いた人々は専門家です。私はいつもあなたがしたいことを最も簡単に表現する標準のコンテナから始めます。コードを早期かつ頻繁にプロファイリングし、十分に速く実行されていない場合は、コードを改善する方法を探します(たとえば、ほとんど読み取りを行うときにunordered_コンテナーを使用する)。

したがって、元の質問に答えるために、それらの値が一意にならない値の連想配列が必要な場合は、std::multimapを使用することは間違いなく意味があります。

7