web-dev-qa-db-fra.com

Python: réduction de l'utilisation de la mémoire du dictionnaire

J'essaie de charger quelques fichiers dans la mémoire. Les fichiers ont l'un des 3 formats suivants:

  • chaîne TAB int
  • string TAB float
  • int TAB float.

En effet, ce sont des fichiers statiques ngram, au cas où cela aiderait à la solution. Par exemple:

i_love TAB 10
love_you TAB 12

Actuellement, le pseudocode que je fais en ce moment est

loadData(file):
     data = {}
     for line in file:
        first, second = line.split('\t')
        data[first] = int(second) #or float(second)

     return data

À ma grande surprise, alors que la taille totale des fichiers sur le disque est d'environ 21 Mo, une fois chargé en mémoire, le processus prend 120 à 180 Mo de mémoire! (l'ensemble python ne charge aucune autre donnée en mémoire).

Il y a moins de 10 fichiers, la plupart d'entre eux resteraient stables à environ 50 à 80 000 lignes, à l'exception d'un fichier qui compte actuellement des millions de lignes.

Je voudrais donc demander une technique/structure de données pour réduire la consommation de mémoire:

  • Des conseils pour les techniques de compression?
  • Si j'utilise toujours dict, existe-t-il un moyen de réduire la mémoire? Est-il possible de définir le "facteur de charge" comme dans Java for Python dict?
  • Si vous avez d'autres structures de données, je suis également prêt à échanger une partie de la vitesse pour réduire la mémoire. Néanmoins, il s'agit d'une application sensible au temps, de sorte qu'une fois que les utilisateurs ont saisi leurs requêtes, je pense qu'il ne serait pas tout à fait raisonnable de prendre plus de quelques secondes pour retourner le résultat. À cet égard, je suis toujours étonné de voir comment Google parvient à faire le Google Translate si rapidement: ils doivent utiliser beaucoup de techniques + beaucoup de puissance des serveurs?

Merci beaucoup. J'attends vos conseils avec impatience.

45
Paul Hoang

Je ne peux pas offrir une stratégie complète qui aiderait à améliorer l'empreinte mémoire, mais je pense que cela peut aider à analyser ce qui prend exactement autant de mémoire.

Si vous regardez la implémentation Python du dictionnaire (qui est une implémentation relativement simple d'une table de hachage), ainsi que l'implémentation de la construction -dans les types de données chaîne et entier, par exemple ici (spécifiquement object.h, intobject.h, stringobject.h et dictobject.h, ainsi que les fichiers * .c correspondants dans ../Objects ), vous pouvez calculer avec une certaine précision l'espace requis:

  1. Un entier est un objet de taille fixe, c'est-à-dire qu'il contient un compte de référence, un pointeur de type et l'entier réel, au total généralement au moins 12 octets sur un système 32 bits et 24 octets sur un système 64 bits, sans tenir compte compte de l'espace supplémentaire éventuellement perdu par l'alignement.

  2. Un objet chaîne est de taille variable, ce qui signifie qu'il contient

    • compte de référence
    • pointeur de type
    • informations sur la taille
    • espace pour le code de hachage calculé paresseusement
    • informations d'état (par exemple utilisées pour interned chaînes)
    • un pointeur vers le contenu dynamique

    au total au moins 24 octets sur 32 bits ou 60 octets sur 64 bits, n'inclut pas l'espace pour la chaîne elle-même.

  3. Le dictionnaire lui-même se compose d'un certain nombre de compartiments, chacun contenant

    • le code de hachage de l'objet actuellement stocké (qui n'est pas prévisible à partir de la position du bucket en raison de la stratégie de résolution de collision utilisée)
    • un pointeur sur l'objet clé
    • un pointeur sur l'objet valeur

    au total au moins 12 octets sur 32 bits et 24 octets sur 64 bits.

  4. Le dictionnaire commence par 8 compartiments vides et est redimensionné en doublant le nombre d'entrées chaque fois que sa capacité est atteinte.

J'ai effectué un test avec une liste de 46 461 chaînes uniques (337 670 octets de taille de chaîne concaténée), chacune associée à un entier - similaire à votre configuration, sur une machine 32 bits. Selon le calcul ci-dessus, je m'attendrais à une empreinte mémoire minimale de

  • 46 461 * (24 + 12) octets = 1,6 Mo pour les combinaisons chaîne/entier
  • 337 670 = 0,3 Mo pour le contenu de la chaîne
  • 65 536 * 12 octets = 1,6 Mo pour les compartiments de hachage (après avoir redimensionné 13 fois)

au total 2,65 Mo. (Pour un système 64 bits, le calcul correspondant donne 5,5 Mo.)

Lors de l'exécution de l'interpréteur Python inactif, son encombrement en fonction de l'outil ps- est de 4,6 Mo. La consommation totale de mémoire attendue après la création du dictionnaire est donc d'environ 4,6 + 2,65 = 7,25 Mo. L'empreinte mémoire réelle (selon ps) dans mon test était 7,6 Mo. Je suppose que les 0,35 Mo supplémentaires ont été consommés par les frais généraux générés par la stratégie d'allocation de mémoire de Python (pour les arénas de mémoire, etc. .)

Bien sûr, beaucoup de gens vont maintenant souligner que mon utilisation de ps pour mesurer l'empreinte mémoire est inexacte et mes hypothèses sur la taille des types de pointeurs et des entiers sur les systèmes 32 bits et 64 bits peuvent être erronées sur de nombreux systèmes spécifiques. Accordé.

Mais, néanmoins, les principales conclusions , je crois, sont les suivantes:

  • L'implémentation du dictionnaire Python consomme étonnamment un petit quantité de mémoire
  • Mais l'espace occupé par les nombreux int et (en particulier) objets chaîne , pour les nombres de référence, les codes de hachage pré-calculés, etc., est plus que vous ne le pensez au premier abord
  • Il n'y a pratiquement aucun moyen d'éviter la surcharge de mémoire , tant que vous utilisez Python et que vous voulez les chaînes et les entiers représentés comme des objets individuels - au moins je ne vois pas comment cela pourrait être fait
  • Il peut être utile de rechercher (ou de vous implémenter) une extension Python-C qui implémente un hachage qui stocke les clés et les valeurs en tant que pointeurs C (plutôt que Python objets). Je ne sais pas si cela existe, mais je pense que cela pourrait être fait et pourrait réduire l'empreinte mémoire de plus de la moitié.
77
jogojapan

1) SQLite en mémoire semble être une excellente solution, il vous permettra d'interroger vos données plus facilement une fois chargées, ce qui est un plaisir

sqlite3.connect (': mémoire:')

2) vous voulez probablement un Tuple nommé - je suis sûr qu'ils sont plus légers que les dictionnaires et vous pouvez accéder aux propriétés en utilisant la notation par points (pour laquelle j'ai une préférence esthétique de toute façon).

http://docs.python.org/dev/library/collections

3) vous voudrez peut-être jeter un œil à Redis: https://github.com/andymccurdy/redis-py

Il est RAPIDE et vous permettra de persister facilement, ce qui signifie que vous n'avez pas à charger l'ensemble entier à chaque fois que vous souhaitez l'utiliser.

4) un trie semble être une bonne idée, mais ajoute une certaine complexité théorique à la fin de votre travail. Vous pouvez utiliser Redis pour l'implémenter et le stocker, ce qui augmentera encore plus votre vitesse.

Mais dans l'ensemble, les tuples nommés sont probablement l'astuce ici.

8
Moshe Bildner

Dans le disque, vous n'avez que des chaînes, lors du chargement dans Python l'interpréteur doit créer une structure entière pour chaque chaîne et pour chaque dictionnaire, en plus de la chaîne elle-même.

Il n'y a aucun moyen de réduire la mémoire utilisée par les dict, mais il existe d'autres façons d'aborder le problème. Si vous souhaitez échanger de la vitesse contre de la mémoire, vous devriez envisager de stocker et d'interroger les chaînes d'un fichier SQLite au lieu de tout charger dans les dictionnaires en mémoire.

6
Pedro Werneck

Cela ressemble à une structure de données Trie ( http://en.m.wikipedia.org/wiki/Trie ) pourrait mieux répondre à votre désir d'efficacité de la mémoire.

Mise à jour: l'efficacité de la mémoire de python dict a été soulevée comme un problème, bien qu'elle ait été rejetée de la bibliothèque standard compte tenu de la disponibilité de bibliothèques tierces. Voir: http: // bugs.python.org/issue952

4
Garrett

Vous pouvez remplacer dict par blist.sorteddict pour l'accès au journal (n) sans surcharge de mémoire. C'est pratique car il se comporte exactement comme un dictionnaire, c'est-à-dire qu'il implémente toutes ses méthodes, vous n'avez donc qu'à changer le type initial.

3
ealfonso

Si vous essayez de stocker des données numériques de manière compacte dans python en mémoire, votre meilleure solution est probablement Numpy.

Numpy ( http://numpy.org ) alloue des structures de données à l'aide de structures C natives. La plupart de ses structures de données supposent que vous stockez un seul type de données, donc ce n'est pas pour toutes les situations (vous devrez peut-être stocker null, etc.), mais il peut être très, très, très rapide et est à peu près aussi compact que vous pourriez demander. Beaucoup de science se fait avec (voir aussi: SciPy).

Bien sûr, il existe une autre option: zlib , si vous avez:

  • Vastes cycles CPU, et
  • BEAUCOUP de données qui ne rentreront pas dans la mémoire

Vous pouvez simplement déclarer une "page" de données (quelle que soit la taille de votre choix) en tant que tableau, lire les données, les stocker dans le tableau, les compresser, puis lire d'autres données jusqu'à ce que vous ayez toutes les données en mémoire vouloir.

Ensuite, parcourez les pages, décompressez, reconvertissez en tableau et effectuez vos opérations selon les besoins. Par exemple:

def arrayToBlob(self, inArray):
    a = array.array('f', inArray)
    return a.tostring()

def blobToArray(self, blob, suppressWarning=False):
    try:
        out = array.array('f', [])
        out.fromstring(blob)
    except Exception, e:
        if not suppressWarning:
            msg = "Exception: blob2array, err: %s, in: %s" % (e, blob)
            self.log.warning(msg)
            raise Exception, msg
    return out

Une fois que vous avez des données en tant qu'objet blob, vous pouvez transmettre cet objet blob à zlib et compresser les données. Si vous avez beaucoup de valeurs répétées, ce blob peut être considérablement compressé.

Bien sûr, c'est plus lent que de tout garder non compressé, mais si vous ne pouvez pas le garder en mémoire, vos choix sont limités pour commencer.

Même avec la compression, il se peut que tout ne tienne pas en mémoire, auquel cas vous devrez peut-être écrire les pages compressées sous forme de chaînes ou de cornichons, etc.

Bonne chance!

3
Kevin J. Rice