web-dev-qa-db-fra.com

Quand est-ce que l'utilisation d'un std :: multimap est logique

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.

  • Simplicité: Un std :: multimap est certainement plus simple à utiliser, car on n'a pas à gérer l'imbrication supplémentaire. Cependant, l'accès à une gamme d'éléments en tant qu'ensemble peut nécessiter la copie des données des itérateurs vers une autre infrastructure de données (par exemple, un std::vector).
  • Vitesse: La localité du vecteur rend très probablement l'itération sur la plage d'éléments égaux beaucoup plus rapide, car l'utilisation du cache est optimisée. Cependant, je suppose que 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?

38
LiKao

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
  }
}
26
Kerrek SB

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.

8
Matthieu M.

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.

7
Michael Kristofik