私は現在、stl-datastructuresのいくつかの使用法を実験しています。ただし、どの組み合わせをいつ使用し、特定の組み合わせをいつ使用するかはまだわかりません。現在、私はstd::multimap
を使用することが理にかなっているのかを理解しようとしています。私の知る限り、std::map
とstd::vector
を組み合わせることで、独自のマルチマップ実装を簡単に構築できます。したがって、これらの各データ構造をいつ使用するべきかという疑問が残ります。
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_partitions
とnum_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
これらの結果をどのように解釈するかは不明です。どのようにして正しいデータ構造を決定しますか?私が見逃したかもしれない決定のための追加の制約はありますか?
ベンチマークが正しいことを行っているかどうかを判断するのは難しいため、数値についてコメントすることはできません。ただし、いくつかの一般的なポイント:
ベクトルのマップではなく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
}
}
非常に重要な代替案を1つ忘れてしまいました。すべてのシーケンスが等しく作成されるわけではありません。
特に、なぜvector
ではなくdeque
やlist
ではないのですか?
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)
したがって、読み取りは無条件に速くなりますが、充填も非常に遅くなります。
ベクトルのマップには、各ベクトルの容量のメモリオーバーヘッドが付属しています。 std::vector
は通常、実際よりも多くの要素にスペースを割り当てます。これはアプリケーションにとって大した問題ではないかもしれませんが、考慮していないもう1つのトレードオフです。
多くの読み取りを行う場合は、O(1)ルックアップ時間unordered_multimap
の方が適切な選択かもしれません。
合理的に最新のコンパイラーを使用している場合(そしてauto
キーワードがある場合はそうします)、一般に、パフォーマンスと信頼性の点で標準コンテナーを打ち負かすのは難しいでしょう。それらを書いた人々は専門家です。私はいつもあなたがしたいことを最も簡単に表現する標準のコンテナから始めます。コードを早期かつ頻繁にプロファイリングし、十分に速く実行されていない場合は、コードを改善する方法を探します(たとえば、ほとんど読み取りを行うときにunordered_
コンテナーを使用する)。
したがって、元の質問に答えるために、それらの値が一意にならない値の連想配列が必要な場合は、std::multimap
を使用することは間違いなく意味があります。