web-dev-qa-db-fra.com

Mémoire supplémentaire de Java HashMap par rapport à ArrayList

Je me demande quelle est la surcharge de mémoire de Java HashMap par rapport à ArrayList?

Mettre à jour:

Je voudrais améliorer la vitesse de recherche de valeurs spécifiques d'un gros paquet (6 Millions +) d'objets identiques.

Ainsi, je pense utiliser un ou plusieurs HashMap au lieu d’utiliser ArrayList. Mais je me demande quel est le surcoût de HashMap.

Autant que je sache, la clé n'est pas stockée, mais uniquement le hachage de la clé. Elle doit donc être quelque chose comme taille du hachage de l'objet + un pointeur.

Mais quelle fonction de hachage est utilisée? Est-ce celui proposé par Object ou un autre?

34
elhoim

Si vous comparez HashMap à ArrayList, je suppose que vous effectuez une sorte de recherche/indexation de ArrayList, telle qu'une recherche binaire ou une table de hachage personnalisée ...? Parce qu'une recherche. (Clé) à travers 6 millions d'entrées serait impossible en utilisant une recherche linéaire.

En utilisant cette hypothèse, j’ai effectué des tests empiriques et conclu que: "Vous pouvez stocker 2,5 fois plus de petits objets dans la même quantité de RAM si vous utilisez ArrayList avec une recherche binaire ou une implémentation de carte de hachage personnalisée. , versus HashMap ". Mon test était basé sur de petits objets ne contenant que 3 champs, dont l'un est la clé et la clé est un entier. J'ai utilisé un jdk 1.6 32 bits. Voir ci-dessous les mises en garde sur cette figure de "2,5".

Les points clés à noter sont:

(a) Ce n'est pas l'espace requis pour les références ou le "facteur de charge" qui vous tue, mais plutôt le temps système nécessaire à la création d'un objet. Si la clé est un type primitif ou une combinaison de 2 valeurs primitives ou de référence ou plus, chaque clé nécessitera son propre objet, ce qui entraînera une surcharge de 8 octets.

(b) D'après mon expérience, vous avez généralement besoin de la clé en tant que valeur (par exemple, pour stocker des enregistrements de clients, indexés par identifiant client, vous souhaitez toujours que cet identifiant soit intégré à l'objet Client). Cela signifie un peu de gaspillage de la part de l'OMI qu'un HashMap stocke séparément les références aux clés et aux valeurs.

Mises en garde:

  1. Le type le plus couramment utilisé pour les clés HashMap est String. La surcharge de création d'objet ne s'applique pas ici, donc la différence serait moindre.

  2. J'ai obtenu un chiffre de 2,8, soit 8880502 entrées insérées dans ArrayList, par rapport à 3148004 dans HashMap sur la machine virtuelle Java -Xmx256M, mais le facteur de charge de ArrayList était de 80% et mes objets étaient plutôt petits: 12 octets plus 8 octets en tête.

  3. Ma figure et mon implémentation requièrent que la clé soit contenue dans la valeur. Sinon, le même problème de temps de création d'objet surviendrait et il ne s'agirait que d'une autre implémentation de HashMap.

Mon code:

public class Payload {
    int key,b,c;
    Payload(int _key) { key = _key; }
}


import org.junit.Test;

import Java.util.HashMap;
import Java.util.Map;


public class Overhead {
    @Test
    public void useHashMap()
    {
        int i=0;
        try {
            Map<Integer, Payload> map = new HashMap<Integer, Payload>();
            for (i=0; i < 4000000; i++) {
                int key = (int)(Math.random() * Integer.MAX_VALUE);
                map.put(key, new Payload(key));
            }
        }
        catch (OutOfMemoryError e) {
            System.out.println("Got up to: " + i);
        }
    }

    @Test
    public void useArrayList()
    {
        int i=0;
        try {
            ArrayListMap map = new ArrayListMap();
            for (i=0; i < 9000000; i++) {
                int key = (int)(Math.random() * Integer.MAX_VALUE);
                map.put(key, new Payload(key));
            }
        }
        catch (OutOfMemoryError e) {
            System.out.println("Got up to: " + i);
        }
    }
}


import Java.util.ArrayList;


public class ArrayListMap {
    private ArrayList<Payload> map = new ArrayList<Payload>();
    private int[] primes = new int[128];

    static boolean isPrime(int n)
    {
        for (int i=(int)Math.sqrt(n); i >= 2; i--) {
            if (n % i == 0)
                return false;
        }
        return true;
    }

    ArrayListMap()
    {
        for (int i=0; i < 11000000; i++)    // this is clumsy, I admit
            map.add(null);
        int n=31;
        for (int i=0; i < 128; i++) {
            while (! isPrime(n))
                n+=2;
            primes[i] = n;
            n += 2;
        }
        System.out.println("Capacity = " + map.size());
    }

    public void put(int key, Payload value)
    {
        int hash = key % map.size();
        int hash2 = primes[key % primes.length];
        if (hash < 0)
            hash += map.size();
        do {
            if (map.get(hash) == null) {
                map.set(hash, value);
                return;
            }
            hash += hash2;
            if (hash >= map.size())
                hash -= map.size();
        } while (true);
    }

    public Payload get(int key)
    {
        int hash = key % map.size();
        int hash2 = primes[key % primes.length];
        if (hash < 0)
            hash += map.size();
        do {
            Payload payload = map.get(hash);
            if (payload == null)
                return null;
            if (payload.key == key)
                return payload;
            hash += hash2;
            if (hash >= map.size())
                hash -= map.size();
        } while (true);
    }
}
42
Tim Cooper

Le plus simple serait de regarder la source et de la résoudre de cette façon. Cependant, vous comparez vraiment des pommes et des oranges - les listes et les cartes sont conceptuellement bien distinctes. Il est rare que vous choisissiez entre eux en fonction de l'utilisation de la mémoire.

Quel est l'arrière-plan de cette question?

15
Jon Skeet

Tout ce qui est stocké dans l'un ou l'autre est des pointeurs. Selon votre architecture, un pointeur doit avoir 32 ou 64 bits (ou plus ou moins).

Une liste de tableau de 10 a tendance à allouer au moins 10 "pointeurs" (et quelques tâches ponctuelles).

Une carte doit allouer deux fois plus (20 pointeurs) car elle stocke deux valeurs à la fois. Ensuite, il doit stocker le "hachage". qui devrait être plus grand que la carte, à un chargement de 75%, il DEVRAIT être autour de 13 valeurs 32 bits (hachages).

donc si vous voulez une réponse désinvolte, le rapport devrait être d'environ 1: 3,25 ou plus, mais vous ne parlez que du stockage de pointeur - très petit, sauf si vous stockez un nombre considérable d'objets - et si oui, l'utilité de pouvoir référencer instantanément (HashMap) vs iterate (array) devrait être BEAUCOUP plus important que la taille de la mémoire.

Oh, aussi: Les tableaux peuvent être ajustés à la taille exacte de votre collection. HashMaps peut aussi bien si vous spécifiez la taille, mais s'il "grandit" au-delà de cette taille, il ré-affectera un tableau plus grand et n'en utilisera pas une partie, de sorte qu'il peut également y avoir un peu de gaspillage.

8
Bill K

Je n'ai pas de réponse pour vous non plus, mais une recherche rapide sur Google a mis en place une fonction en Java qui pourrait aider.

Runtime.getRuntime (). FreeMemory ();

Je propose donc que vous remplissiez un HashMap et un ArrayList avec les mêmes données. Enregistrez la mémoire libre, supprimez le premier objet, enregistrez de la mémoire, supprimez le deuxième objet, enregistrez la mémoire, calculez les différences, ..., profitez !!! 

Vous devriez probablement le faire avec des magnitudes de données. Par exemple, commencez par 1000, puis 10000, 100000, 1000000.

EDIT: Corrigé, merci à amischiefr.

EDIT: Désolé pour l'édition de votre post, mais c'est assez important si vous voulez utiliser ceci (et c'est un peu beaucoup pour un commentaire) . FreeMemory ne fonctionne pas comme vous le pensez. Tout d'abord, sa valeur est modifiée par la récupération de place. Deuxièmement, sa valeur est modifiée lorsque Java alloue plus de mémoire. Le simple fait d'appeler l'appel freeMemory ne fournit pas de données utiles.

Essaye ça:

public static void displayMemory() {
    Runtime r=Runtime.getRuntime();
    r.gc();
    r.gc(); // YES, you NEED 2!
    System.out.println("Memory Used="+(r.totalMemory()-r.freeMemory()));
}

Vous pouvez également retourner la mémoire utilisée et la stocker, puis la comparer à une valeur ultérieure. Dans les deux cas, rappelez-vous les 2 gcs et en soustrayant de totalMemory ().

Encore une fois, désolé de modifier votre message!

7
sanscore

Les cartes de hachage essaient de maintenir un facteur de charge (généralement plein à 75%), vous pouvez considérer une carte de hachage comme une liste de tableaux peu remplis. Le problème dans une comparaison directe en taille est que ce facteur de charge de la carte augmente pour correspondre à la taille des données. En revanche, ArrayList augmente pour répondre à ses besoins en doublant la taille de son tableau interne. Pour des tailles relativement petites, elles sont comparables. Toutefois, à mesure que vous intégrez de plus en plus de données sur la carte, de nombreuses références vides sont nécessaires pour maintenir les performances de hachage.

Dans les deux cas, je vous recommande d’amorcer la taille attendue des données avant de commencer l’ajout. Cela donnera aux implémentations un meilleur réglage initial et consommera probablement moins dans l'ensemble dans les deux cas.

Mettre à jour:

en fonction de votre problème mis à jour, consultez Listes vitrées . Il s’agit d’un petit outil bien conçu par certaines personnes travaillant sur Google pour effectuer des opérations similaires à celle que vous décrivez. C'est aussi très rapide. Permet le regroupement, le filtrage, la recherche, etc.

3
reccles

HashMap contient une référence à la valeur et une référence à la clé.

ArrayList tient juste une référence à la valeur.

Donc, en supposant que la clé utilise la même mémoire que la valeur, HashMap utilise 50% de mémoire supplémentaire (bien que, à proprement parler, ce ne soit pas HashMap qui utilise cette mémoire parce qu'il en garde une référence) 

Par ailleurs, HashMap fournit performances à temps constant pour les opérations de base (get et put) Ainsi, bien qu'il puisse utiliser plus de mémoire, obtenir un élément peut être beaucoup plus rapide avec un HashMap qu'un ArrayList.

Donc, la prochaine chose que vous devriez faire est ne pas se soucier de qui utilise plus de mémoire mais que sont-ils bon pour

L'utilisation de la structure de données appropriée pour votre programme permet d'économiser plus de ressources processeur/mémoire que la manière dont la bibliothèque est implémentée.

MODIFIER 

Après que Grant Welch ait répondu, j'ai décidé de mesurer 2 000 000 d'entiers.

Voici le code source

C'est la sortie 

$
$javac MemoryUsage.Java  
Note: MemoryUsage.Java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$Java -Xms128m -Xmx128m MemoryUsage 
Using ArrayListMemoryUsage@8558d2 size: 0
Total memory: 133.234.688
Initial free: 132.718.608
  Final free: 77.965.488

Used: 54.753.120
Memory Used 41.364.824
ArrayListMemoryUsage@8558d2 size: 2000000
$
$Java -Xms128m -Xmx128m MemoryUsage H
Using HashMapMemoryUsage@8558d2 size: 0
Total memory: 133.234.688
Initial free: 124.329.984
  Final free: 4.109.600

Used: 120.220.384
Memory Used 129.108.608
HashMapMemoryUsage@8558d2 size: 2000000
3
OscarRyz

Fondamentalement, vous devriez utiliser le "bon outil pour le travail". Puisqu'il existe différentes instances où vous aurez besoin d'une paire clé/valeur (où vous pouvez utiliser un HashMap) et d'autres instances où vous aurez simplement besoin d'une liste de valeurs (où vous pouvez utiliser un ArrayList), la question " on utilise plus de mémoire ", à mon avis, est discutable, car il ne s'agit pas de choisir l'un sur l'autre.

Mais pour répondre à cette question, étant donné que HashMap stocke les paires clé/valeur alors que ArrayList stocke uniquement les valeurs, je suppose que l’ajout de clés au HashMap signifie qu’il nécessite plus de mémoire, en supposant bien entendu que nous les comparons par même valeur type (par exemple, où les valeurs dans les deux sont des chaînes).

2
Avrom

Je ne connais pas le nombre exact, mais les cartes de hachage sont beaucoup plus lourdes. En comparant les deux, la représentation interne de ArrayList est évidente, mais HashMaps conserve les objets Entry (Entry) qui peuvent gonfler votre consommation de mémoire.

Ce n'est pas beaucoup plus grand, mais c'est plus grand. Un excellent moyen de visualiser cela serait avec un profileur dynamique tel que YourKit qui vous permet de voir toutes les allocations de tas. C'est joli Nice.

1
Malaxeur

Ce post donne beaucoup d’informations sur la taille des objets en Java.

1
elhoim

Ce site répertorie la consommation de mémoire de plusieurs structures de données couramment utilisées (et moins fréquemment). À partir de là, on peut voir que la HashMap prend environ 5 fois l’espace d’une ArrayList. La carte allouera également un objet supplémentaire par entrée.

Si vous avez besoin d'un ordre d'itération prévisible et que vous utilisez une LinkedHashMap, la consommation de mémoire sera encore plus importante.

Vous pouvez effectuer vos propres mesures de mémoire avec Memory Measurer .

Il convient toutefois de noter deux faits importants:

  1. De nombreuses structures de données (y compris ArrayList et HashMap) allouent plus d’espace qu’elles n’ont actuellement besoin, car sinon, elles devraient fréquemment exécuter une opération de redimensionnement coûteuse. Ainsi, la consommation de mémoire par élément dépend du nombre d'éléments contenus dans la collection. Par exemple, une ArrayList avec les paramètres par défaut utilise la même mémoire pour 0 à 10 éléments.
  2. Comme d'autres l'ont dit, les clés de la carte sont également stockées. Ainsi, s’ils ne sont de toute façon pas en mémoire, vous devrez également ajouter ce coût en mémoire. Un objet supplémentaire prendra généralement 8 octets de surcharge seulement, plus la mémoire pour ses champs et éventuellement un remplissage. Donc, ce sera aussi beaucoup de mémoire.
0
Philipp Wendler

Comme Jon Skeet l'a noté, ces structures sont complètement différentes. Un mappage (tel que HashMap) est un mappage d'une valeur à une autre - c’est-à-dire que vous avez une clé mappée sur une valeur, dans le type de relation Clé-> Valeur. La clé est hachée et est placée dans un tableau pour une recherche rapide.

Une liste, en revanche, est une collection d’éléments avec ordre. ArrayList utilise parfois un tableau comme mécanisme de stockage principal, mais cela n’est pas pertinent. Chaque élément indexé est un élément unique de la liste.

edit: en fonction de votre commentaire, j'ai ajouté les informations suivantes:

La clé est stockée dans une table de hachage. En effet, il n’est pas garanti que le hachage soit unique pour deux éléments différents. Ainsi, la clé doit être stockée dans le cas de collisions de hachage. Si vous voulez simplement voir si un élément existe dans un ensemble d’éléments, utilisez un ensemble (l’implémentation standard de cela étant HashSet). Si l'ordre a de l'importance, mais vous avez besoin d'une recherche rapide, utilisez un LinkedHashSet, car il conserve l'ordre dans lequel les éléments ont été insérés. Le temps de recherche est O(1) pour les deux, mais le temps d'insertion est légèrement plus long sur un LinkedHashSet. Utilisez une carte uniquement si vous effectuez le mappage d'une valeur à une autre. Si vous disposez simplement d'un ensemble d'objets uniques, utilisez un ensemble, si vous avez commandé des objets, utilisez une liste.

0
aperkins

Si vous envisagez deux ArrayLists vs un Hashmap, c'est indéterminé; les deux sont des structures de données partiellement complètes. Si vous compariez Vector vs Hashtable, Vector est probablement plus efficace en termes de mémoire, car il n'alloue que l'espace utilisé, alors que Hashtables en alloue plus.

Si vous avez besoin d'une paire clé-valeur et que vous ne faites pas un travail incroyablement gourmand en mémoire, utilisez simplement Hashmap.

0
Dean J