4
votes

Comment optimiser la réutilisation d'un grand std :: unordered_map en tant que temporaire dans une fonction fréquemment appelée?

Question simplifiée avec un exemple fonctionnel: je veux réutiliser plusieurs fois un std :: unordered_map (appelons-le umap), similaire au code factice suivant (qui ne fait rien de significatif). Comment puis-je accélérer ce code?

#include <iostream>
#include <unordered_map>
#include <time.h>

unsigned size = 1000000;

void foo(){
    std::unordered_map<int, double> umap;
    umap.reserve(size);
    for (int i = 0; i < size; i++) {
        // in my real program: umap gets filled with meaningful data here
        umap.emplace(i, i * 0.1);
    }
    // ... some code here which does something meaningful with umap
}

int main() {

    clock_t t = clock();

    for(int i = 0; i < 50; i++){
        foo();
    }

    t = clock() - t;
    printf ("%f s\n",((float)t)/CLOCKS_PER_SEC);

    return 0;
}

Dans mon code d'origine, je souhaite stocker les entrées de la matrice dans umap. Dans chaque appel à foo, les valeurs de clé commencent de 0 à N, et N peut être différent dans chaque appel à foo, mais il existe une limite supérieure de 10M pour les indices. De plus, les valeurs peuvent être différentes (contrairement au code factice ici qui est toujours i*0.1).

J'ai essayé de faire de umap un non- variable locale, pour éviter l'allocation répétée de mémoire de umap.reserve () à chaque appel. Cela nécessite d'appeler umap.clear () à la fin de foo , mais cela s'est avéré être en fait plus lent que d'utiliser une variable locale (je l'ai mesurée). p>


15 commentaires

Inclure un programme jouet qui démontre le problème serait utile ici - les descriptions en anglais sont intrinsèquement plus ambiguës que les exemples de code réels.


Quelle est la base de référence des performances (déclarer une carte locale dans toto)?


@JeremyFriesner Je viens d'ajouter un morceau de code pour clarifier.


@ n.m. Oui, version 1 dans mon "Edit".


Dans la version 2, umap est-il passé par référence lorsque foo (umap) est appelé, ou est-ce que chaque appel fait une copie de la carte?


J'ai posé des questions sur les chiffres de performance. Veuillez également préférer le vrai code à construire au pseudocode.


Je suis d'accord. Veuillez extraire un véritable exemple minimal reproductible , c'est juste pour vous assurer que vous avez d'abord fait votre part en essayant de comprendre ce que est faux. Avec les extraits que vous avez fournis, vous n'avez pas fourni suffisamment d'informations pour reproduire le problème.


@JeremyFriesner Il est passé par référence. J'ai essayé d'expliquer une question plus claire dans Edit2.


@ n.m. Mon premier message n'était pas assez clair. J'ai reformulé la question dans Edit2 avec du code de travail.


@Abaris Dans cet exemple, umap a toujours le même ensemble de données. Pourquoi générez-vous récursivement le même ensemble de données encore et encore? Sont-ils différents les uns des autres dans votre code source réel? La vraie solution dépend fortement de ses détails.


Vous rencontrez le problème que la norme spécifie le hachage ouvert pour std :: unordered_map, alors que votre cas d'utilisation serait bien mieux servi par le hachage fermé. Vous pouvez donc essayer de trouver (ou d'implémenter) une carte de hachage fermée plutôt que d'utiliser std :: unordered_map


@Hiroki Dans mon application, je stocke des entrées de matrice dans la carte, et cela peut changer de taille et de valeur à chaque appel à foo (), mais je connais la taille maximale de umap.


@Abaris thx :). Encore une question. Votre jeu de clés de umap est-il toujours des entiers continus {1,2,3,4 ..., N} et fait la valeur maximale N change par les appels récursifs? Ou le jeu de clés change-t-il également par les appels récursifs comme celui-ci {1,2,3,4, ...} -> {1,5,9,10, ...} -> ...?


@Hiroki merci. C'est toujours {0, 1, 2, ..., N} mais N peut changer à chaque appel. J'ai ajouté la réponse à votre question dans mon message.


@ChrisDodd C'est une bonne suggestion, je lis sur le hachage ouvert / fermé pour essayer de l'implémenter moi-même.


4 Réponses :


3
votes

Je ne pense pas qu'il existe un bon moyen d'accomplir directement ce que vous recherchez - c'est-à-dire que vous ne pouvez pas effacer la carte sans effacer la carte. Je suppose que vous pourriez attribuer un certain nombre de cartes à l'avance, et simplement utiliser chacune d'elles une seule fois comme "carte jetable", puis continuer à utiliser la carte suivante lors de votre prochain appel, mais je doute que cela donne vous accélérez globalement, car à la fin de tout cela, vous devrez tous les effacer en même temps, et dans tous les cas, ce serait très gourmand en RAM et peu compatible avec le cache (dans les processeurs modernes, l'accès à la RAM est très souvent le goulot d'étranglement des performances, et donc minimiser le nombre de défauts de cache est le moyen de maximiser l'efficacité).

Ma suggestion serait que si la vitesse en clair est si critique, vous devrez peut-être abandonner entièrement unordered_map et utiliser à la place quelque chose de plus simple comme un std :: vector - dans ce cas, vous pouvez simplement conserver un nombre entier d'éléments valides dans le vecteur, et "effacer" le vecteur consiste simplement à remettre le compte à zéro. (Bien sûr, cela signifie que vous sacrifiez les propriétés de recherche rapide de unordered_map , mais peut-être n'en avez-vous pas besoin à ce stade de votre calcul?)


1 commentaires

Merci pour la suggestion Jeremy. Une idée que j'ai essayé d'accélérer clear () était d'utiliser l'alternative à unordered_map, sparsepp, pour laquelle la fonction clear () était presque 4 fois plus rapide. De plus, j'ai utilisé l'idée de vecteur exactement comme vous l'avez expliqué. Pour mon application, dans certains cas, le vecteur est plus rapide et pour certains autres unordered_map, en fonction de la taille. Je voulais voir si je pouvais faire quelque chose de similaire à un pool de mémoire pour unordered_map, mais je pense que je ne peux pas.



2
votes

Un moyen simple et efficace consiste à réutiliser le même conteneur et la même mémoire encore et encore avec un passage par référence comme suit. Dans cette méthode, vous pouvez éviter leur allocation de mémoire récursive std :: unordered_map :: reserve et std :: unordered_map :: ~ unordered_map qui ont tous deux la complexité O (num. of elemenrs):

void foo(std::vector<double>& vec)
{    
    int N = ...// set N here

    for(int i = 0; i<N; ++i)
    {
        // overwrite vec[0], ..., vec[N-1]
        vec[i] = i*0.1;
    }

    // do something and not access to vec[N], ..., vec[size-1] !            
}

Le côté appelant serait le suivant:

std::unordered_map<int,double> umap;
umap.reserve(size);

for(int i=0; i<50; ++i){
    foo(umap);
}

Mais puisque votre jeu de clés est toujours des entiers continus {1,2, ..., N} , je pense que std :: vector qui permet d'éviter les calculs de hachage serait plus préférable pour enregistrer les valeurs umap [0], ..., umap [N] :

void foo(std::unordered_map<int, double>& umap)
{        
    std::size_t N = ...// set N here

    for (int i = 0; i < N; ++i)
    {
        // overwrite umap[0], ..., umap[N-1]
        // If umap does not have key=i, then it is inserted.
        umap[i] = i*0.1;
    }

    // do something and not access to umap[N], ..., umap[size-1] !
}


4 commentaires

Vous avez écrit une réponse très claire, @Hiroki. Je vous en suis reconnaissant. J'ai aussi la version vectorielle et cela fonctionne mieux que umap pour les petites tailles, donc je passe de l'une à l'autre dans mon code, en fonction du problème. En fait, j'ai essayé de retirer umap de foo, comme vous l'avez fait, mais je dois appeler umap.clear () à la fin de foo (ou il commence) pour le prochain appel et cela a rendu mon code plus lent au total, car clear ( ) est juste lent je suppose.


@Abaris beaucoup merci :)! Pourquoi avez-vous besoin d'appeler std :: unordered_map :: clear () plutôt que d'écraser les valeurs de celui-ci? Devez-vous effacer ou invalider des éléments supplémentaires (N, umap [N]) , ..., (size-1, umap [size-1]) pour certains les raisons?


Je veux dire les N premières entrées de l'appel précédent. Je devais le mentionner dans ma question, désolé. C'est ainsi que j'utilise umap pour ajouter des entrées dans mon code d'origine: s'il y a un élément avec la même clé, j'ajouterai leurs valeurs, sinon je l'ajouterai comme nouvelle entrée. Si je n'efface pas umap de l'appel précédent, y a-t-il un moyen de réutiliser umap?


@Abaris Hmm… Nous ne parvenons peut-être pas à partager le cœur de notre problème :). Pourriez-vous s'il vous plaît nous montrer un code source de jouet à construire qui doit appeler std :: unordered_map :: clear () ? J'espère que nous pouvons l'éviter. Par exemple, Ceci est mon exemple rapide d'appels récursifs sans allocation récursive.



1
votes

Avez-vous essayé d'éviter toute allocation de mémoire en utilisant un simple tableau? Vous avez dit ci-dessus que vous connaissez la taille maximale de umap sur tous les appels à foo():

#include <iostream>
#include <unordered_map>
#include <time.h>

constexpr int size = 1000000;
double af[size];

void foo(int N) {
    // assert(N<=size);
    for (int i = 0; i < N; i++) {
        af[i] = i;
    }
    // ... af
}

int main() {    
    clock_t t = clock();

    for(int i = 0; i < 50; i++){
        foo(size /* or some other N<=size */);
    }

    t = clock() - t;
    printf ("%f s\n",((float)t)/CLOCKS_PER_SEC);

    return 0;
}


0 commentaires

0
votes

Comme je l'ai suggéré dans les commentaires, le hachage fermé serait mieux pour votre cas d'utilisation. Voici une carte de hachage fermée rapide et sale avec une taille de table de hachage fixe que vous pouvez expérimenter:

template<class Key, class T, size_t size = 1000003, class Hash = std::hash<Key>>
class closed_hash_map {
    typedef std::pair<const Key, T>                     value_type;
    typedef typename std::vector<value_type>::iterator  iterator;
    std::array<int, size>                               hashtable;
    std::vector<value_type>                             data;
 public:
    iterator begin() { return data.begin(); }
    iterator end() { return data.end(); }
    iterator find(const Key &k) {
        size_t h = Hash()(k) % size;
        while (hashtable[h]) {
            if (data[hashtable[h]-1].first == k)
                return data.begin() + (hashtable[h] - 1);
            if (++h == size) h = 0; }
        return data.end(); }
    std::pair<iterator, bool> insert(const value_type& obj) {
        size_t h = Hash()(obj.first) % size;
        while (hashtable[h]) {
            if (data[hashtable[h]-1].first == obj.first)
                return std::make_pair(data.begin() + (hashtable[h] - 1), false);
            if (++h == size) h = 0; }
        data.emplace_back(obj);
        hashtable[h] = data.size();
        return std::make_pair(data.end() - 1, true); }
    void clear() {
        data.clear();
        hashtable.fill(0); }
};

Elle peut être rendue plus flexible en redimensionnant dynamiquement la table de hachage à la demande le cas échéant, et plus efficace en utilisant robin- remplacement du capot.


0 commentaires