J'expérimente actuellement sur une utilisation des infrastructures stl. Cependant, je ne sais toujours pas quand utiliser lequel et quand utiliser une certaine combinaison. Actuellement, j'essaie de comprendre, lorsque j'utilise un std::multimap
a du sens. Pour autant que je sache, on peut facilement créer sa propre implémentation multimap en combinant std::map
et std::vector
. Il me reste donc la question de savoir quand chacune de ces infrastructures de données doit être utilisée.
std::vector
).std::multimaps
ont également de nombreuses astuces d'optimisation derrière le dos pour rendre l'itération sur des éléments égaux aussi rapidement que possible. Il est également possible d'optimiser la plage d'éléments appropriée pour std::multimaps
.Afin d'essayer les problèmes de vitesse, j'ai fait quelques comparaisons simples en utilisant le programme suivant:
#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;
}
}
Comme je le soupçonnais, cela dépend principalement du rapport entre num_partitions
et num_elements
, donc je suis toujours perdu ici. Voici quelques exemples de sorties:
Pour num_partitions = 100000
et 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
Pour num_partitions = 100000
et 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
Pour num_partitions = 100000
et 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
Pour num_partitions = 1000
et 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
Je ne sais pas comment interpréter ces résultats. Comment décideriez-vous de la bonne structure de données? Y a-t-il des contraintes supplémentaires pour la décision, que j'aurais peut-être manquées?
Il est difficile de dire si votre indice de référence fait la bonne chose, je ne peux donc pas commenter les chiffres. Cependant, quelques points généraux:
Pourquoi multimap
plutôt que carte de vecteurs: Les cartes, multimaps, ensembles et multisets sont tous essentiellement la même structure de données, et une fois que vous en avez une, il est trivial de simplement énoncer les quatre. Donc la première réponse est: "pourquoi pas l'avoir"?
En quoi est-ce utile: Les multimaps sont l'une de ces choses dont vous avez rarement besoin, mais quand vous en avez besoin, vous en avez vraiment besoin.
Pourquoi ne pas rouler ma propre solution? Comme je l'ai dit, je ne suis pas sûr de ces repères, mais même si vous pourriez faire autre chose qui n'est pas pire que la norme conteneur (que je remets en question), alors vous devriez considérer le fardeau global de bien le faire, de le tester et de le maintenir. Imaginez un monde dans lequel vous seriez taxé pour chaque ligne de code que vous avez écrite (c'est la suggestion de Stepanov). Réutilisez les composants standard de l'industrie dans la mesure du possible.
Enfin, voici la manière typique d'itérer un multimap:
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
}
}
Vous avez oublié une alternative très importante: toutes les séquences ne sont pas créées égales.
Surtout, pourquoi un vector
et non un deque
ou un list
?
Utilisation de list
UNE std::map<int, std::list<int> >
devrait fonctionner à peu près de la même manière qu'un std::multimap<int, int>
puisque list
est également basé sur un nœud.
Utilisation de deque
Un deque
est le conteneur par défaut à utiliser lorsque vous ne savez pas à qui vous adresser et que vous n'avez aucune exigence particulière.
En ce qui concerne le vector
, vous échangez une certaine vitesse de lecture (pas beaucoup) pour des opérations plus rapides Push
et pop
.
En utilisant un deque
à la place, et quelques optimisations évidentes , j'obtiens:
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)
Ou dans le "mauvais" cas:
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)
Ainsi, la lecture est inconditionnellement plus rapide, mais le remplissage est également beaucoup plus lent.
Une carte des vecteurs est fournie avec la surcharge de mémoire pour la capacité de chaque vecteur. std::vector
alloue généralement de l'espace pour plus d'éléments que vous n'en avez réellement. Ce n'est peut-être pas un gros problème pour votre application, mais c'est un autre compromis que vous n'avez pas envisagé.
Si vous effectuez de nombreuses lectures, le temps de recherche O(1) de unordered_multimap
pourrait être un meilleur choix.
Si vous avez un compilateur raisonnablement moderne (et étant donné la présence du mot-clé auto
, vous l'avez), alors en général, vous aurez du mal à battre les conteneurs standard en termes de performances et de fiabilité. Les personnes qui les ont écrites sont des experts. Je commencerais toujours par le conteneur standard qui exprime le plus facilement ce que vous voulez faire. Profilez votre code tôt et souvent, et s'il ne fonctionne pas assez rapidement, recherchez des moyens de l'améliorer (par exemple, en utilisant le unordered_
conteneurs lors des lectures).
Donc, pour répondre à votre question d'origine, si vous avez besoin d'un tableau associatif de valeurs où ces valeurs ne seront pas uniques, alors utilisez std::multimap
a vraiment du sens.