463 votes

Performances des ensembles de hachages et des listes

Il est clair qu'une performance de recherche du générique HashSet<T> est plus élevée que celle de la classe générique List<T> classe. Il suffit de comparer la clé basée sur le hachage avec l'approche linéaire dans la classe List<T> classe.

Cependant, le calcul d'une clé de hachage peut lui-même prendre quelques cycles de CPU, de sorte que pour une petite quantité d'éléments, la recherche linéaire peut être une véritable alternative à l'algorithme de recherche linéaire. HashSet<T> .

Ma question : où se situe le seuil de rentabilité ?

Pour simplifier le scénario (et pour être juste), supposons que la List<T> utilise la classe Equals() pour identifier un article.

8 votes

Si vous voulez vraiment minimiser le temps de recherche, pensez également aux tableaux et aux tableaux triés. Pour répondre correctement à cette question, un benchmark est nécessaire, mais vous devez nous en dire plus sur T. De plus, les performances de HashSet peuvent être affectées par le temps d'exécution de T.GetHashCode().

909voto

innominate227 Points 2652

Beaucoup de gens disent qu'une fois qu'on a atteint une taille où la vitesse est un problème, il faut HashSet<T> battra toujours List<T> mais cela dépend de ce que vous faites.

Disons que vous avez un List<T> qui ne contiendra jamais que 5 éléments en moyenne. Sur un grand nombre de cycles, si un seul élément est ajouté ou supprimé à chaque cycle, il est préférable d'utiliser une méthode d'analyse de type List<T> .

J'ai fait un test à ce sujet sur ma machine, et, eh bien, il doit être très très petit pour obtenir un avantage de la part de l'entreprise. List<T> . Pour une liste de chaînes courtes, l'avantage disparaissait après la taille 5, pour les objets après la taille 20.

1 item LIST strs time: 617ms
1 item HASHSET strs time: 1332ms

2 item LIST strs time: 781ms
2 item HASHSET strs time: 1354ms

3 item LIST strs time: 950ms
3 item HASHSET strs time: 1405ms

4 item LIST strs time: 1126ms
4 item HASHSET strs time: 1441ms

5 item LIST strs time: 1370ms
5 item HASHSET strs time: 1452ms

6 item LIST strs time: 1481ms
6 item HASHSET strs time: 1418ms

7 item LIST strs time: 1581ms
7 item HASHSET strs time: 1464ms

8 item LIST strs time: 1726ms
8 item HASHSET strs time: 1398ms

9 item LIST strs time: 1901ms
9 item HASHSET strs time: 1433ms

1 item LIST objs time: 614ms
1 item HASHSET objs time: 1993ms

4 item LIST objs time: 837ms
4 item HASHSET objs time: 1914ms

7 item LIST objs time: 1070ms
7 item HASHSET objs time: 1900ms

10 item LIST objs time: 1267ms
10 item HASHSET objs time: 1904ms

13 item LIST objs time: 1494ms
13 item HASHSET objs time: 1893ms

16 item LIST objs time: 1695ms
16 item HASHSET objs time: 1879ms

19 item LIST objs time: 1902ms
19 item HASHSET objs time: 1950ms

22 item LIST objs time: 2136ms
22 item HASHSET objs time: 1893ms

25 item LIST objs time: 2357ms
25 item HASHSET objs time: 1826ms

28 item LIST objs time: 2555ms
28 item HASHSET objs time: 1865ms

31 item LIST objs time: 2755ms
31 item HASHSET objs time: 1963ms

34 item LIST objs time: 3025ms
34 item HASHSET objs time: 1874ms

37 item LIST objs time: 3195ms
37 item HASHSET objs time: 1958ms

40 item LIST objs time: 3401ms
40 item HASHSET objs time: 1855ms

43 item LIST objs time: 3618ms
43 item HASHSET objs time: 1869ms

46 item LIST objs time: 3883ms
46 item HASHSET objs time: 2046ms

49 item LIST objs time: 4218ms
49 item HASHSET objs time: 1873ms

Voici ces données affichées sous forme de graphique :

enter image description here

Voici le code :

static void Main(string[] args)
{
    int times = 10000000;

    for (int listSize = 1; listSize < 10; listSize++)
    {
        List<string> list = new List<string>();
        HashSet<string> hashset = new HashSet<string>();

        for (int i = 0; i < listSize; i++)
        {
            list.Add("string" + i.ToString());
            hashset.Add("string" + i.ToString());
        }

        Stopwatch timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            list.Remove("string0");
            list.Add("string0");
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item LIST strs time: " + timer.ElapsedMilliseconds.ToString() + "ms");

        timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            hashset.Remove("string0");
            hashset.Add("string0");
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item HASHSET strs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
        Console.WriteLine();
    }

    for (int listSize = 1; listSize < 50; listSize+=3)
    {
        List<object> list = new List<object>();
        HashSet<object> hashset = new HashSet<object>();

        for (int i = 0; i < listSize; i++)
        {
            list.Add(new object());
            hashset.Add(new object());
        }

        object objToAddRem = list[0];

        Stopwatch timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            list.Remove(objToAddRem);
            list.Add(objToAddRem);
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item LIST objs time: " + timer.ElapsedMilliseconds.ToString() + "ms");

        timer = new Stopwatch();
        timer.Start();
        for (int i = 0; i < times; i++)
        {
            hashset.Remove(objToAddRem);
            hashset.Add(objToAddRem);
        }
        timer.Stop();
        Console.WriteLine(listSize.ToString() + " item HASHSET objs time: " + timer.ElapsedMilliseconds.ToString() + "ms");
        Console.WriteLine();
    }

    Console.ReadLine();
}

9 votes

Merci beaucoup ! C'est une excellente explication, je cherchais quelque chose qui puisse ajouter et supprimer plus rapidement qu'une carte de crédit. List<T> pour un moteur de jeu, et comme j'aurai généralement un volume élevé d'objets, ce type de collection serait parfait.

26 votes

Il existe en fait une collection dans le cadre .NET qui bascule entre une liste et une implémentation de table de hachage en fonction du nombre d'éléments qu'elle contient : HybridDictionary .

10 votes

MS semble l'avoir abandonné, puisqu'il ne dispose que d'une version non générique.

84voto

nawfal Points 13500

Il est essentiellement inutile de comparer deux structures pour les éléments suivants performance qui se comportent différemment. Utilisez la structure qui traduit l'intention. Même si vous dites que votre List<T> n'aurait pas de doublons et l'ordre d'itération n'a pas d'importance, ce qui la rend comparable à une HashSet<T> mais c'est toujours un mauvais choix à utiliser List<T> car il est relativement moins tolérant aux pannes.

Cela dit, je vais inspecter quelques autres aspects de la performance,

+------------+--------+-------------+-----------+----------+----------+-----------+
| Collection | Random | Containment | Insertion | Addition |  Removal | Memory    |
|            | access |             |           |          |          |           |
+------------+--------+-------------+-----------+----------+----------+-----------+
| List<T>    | O(1)   | O(n)        | O(n)      | O(1)*    | O(n)     | Lesser    |
| HashSet<T> | O(n)   | O(1)        | n/a       | O(1)     | O(1)     | Greater** |
+------------+--------+-------------+-----------+----------+----------+-----------+
  • Même si l'addition est O(1) dans les deux cas, elle sera relativement plus lente dans le HashSet car elle implique un coût de précalcul du code de hachage avant de le stocker.

  • L'évolutivité supérieure de HashSet a un coût en mémoire. Chaque entrée est stockée comme un nouvel objet avec son code de hachage. Cet article pourrait vous donner une idée.

16 votes

Ma question (il y a six ans) ne portait pas sur la théorique performance.

1 votes

HashSet permet un accès aléatoire avec ElementAt(), et je pense que ce serait un temps O(n). De plus, vous pourriez peut-être indiquer dans votre tableau si chaque collection autorise les doublons (par exemple, les listes le font, mais pas les HashSets).

1 votes

@DanW dans le tableau, je compare uniquement les performances, pas les caractéristiques comportementales. Merci pour le conseil de ElementAt.

73voto

Eloff Points 5224

Vous regardez ça de travers. Oui, une recherche linéaire dans une liste est plus efficace qu'un ensemble de hachages pour un petit nombre d'éléments. Mais la différence de performance n'a généralement pas d'importance pour des collections aussi petites. C'est généralement pour les grandes collections qu'il faut s'inquiéter, et c'est là que la recherche linéaire est nécessaire. penser en termes de Big-O . Toutefois, si vous avez mesuré un véritable goulot d'étranglement sur les performances du HashSet, vous pouvez essayer de créer un hybride List/HashSet, mais vous y parviendrez en effectuant de nombreux tests de performance empiriques - et non en posant des questions sur SO.

6 votes

les grandes collections dont vous devez vous préoccuper . Nous pouvons redéfinir cette question en termes when small collection becomes large enough to worry about HashSet vs List? des dizaines, des dizaines de milliers, des milliards d'éléments ?

11 votes

Non, vous constaterez une différence de performance considérable au-delà de quelques centaines d'éléments. L'idée est de toujours utiliser un HashSet si vous effectuez les types d'accès pour lesquels le HashSet est bon (par exemple, l'élément X est-il dans l'ensemble ?) Si votre collection est si petite qu'une liste est plus rapide, il est très rare que ces recherches soient un goulot d'étranglement dans votre application. Si vous pouvez mesurer que c'est le cas, vous pouvez essayer de l'optimiser - mais sinon vous perdez votre temps.

19 votes

Que se passe-t-il si vous avez une petite collection qui est frappée plusieurs fois dans une boucle ? Ce n'est pas un scénario rare.

54voto

Chris Points 8576

Le choix d'utiliser un HashSet<> ou une List<> se résume à ceci comment vous devez accéder à votre collection . Si vous devez garantir l'ordre des éléments, utilisez une liste. Si ce n'est pas le cas, utilisez un HashSet. Laissez Microsoft s'occuper de la mise en œuvre de ses algorithmes et objets de hachage.

Un HashSet permettra d'accéder aux éléments sans avoir à énumérer la collection (complexité de O(1) ou presque), et parce qu'une liste garantit l'ordre, contrairement à un HashSet, certains éléments devront être énumérés (complexité de O(n)).

0 votes

La liste peut potentiellement calculer le décalage pour l'élément spécifique par son index (parce que tous les éléments sont du même type et occupent potentiellement la même taille mémoire). Il n'est donc pas nécessaire que la liste énumère ses éléments.

0 votes

@Lu55 - La question porte sur recherche pour un élément d'une collection. Un scénario typique est que la collection est dynamique - Des éléments peuvent avoir été ajoutés ou supprimés depuis la dernière fois que vous avez cherché un élément donné. indice n'est pas significative (car elle aura changé). Si vous avez un statique (qui ne changera pas pendant que vous effectuez vos calculs), ou que les éléments ne sont jamais supprimés, et sont toujours ajoutés à la fin, alors un fichier List est préférable, car vous pouvez vous souvenir d'un index - c'est la situation que vous décrivez.

0 votes

Vous pouvez utiliser un SortedSet si vous avez besoin de trier un HashSet. C'est toujours beaucoup plus rapide qu'une liste.

32voto

drzaus Points 3344

J'ai juste pensé que je pourrais apporter quelques points de repère pour différents scénarios afin d'illustrer les réponses précédentes :

  1. Quelques (12 - 20) petites chaînes de caractères (longueur entre 5 et 10 caractères)
  2. Beaucoup (~10K) de petites chaînes
  3. Quelques longues chaînes de caractères (longueur entre 200 et 1000 caractères)
  4. Beaucoup (~5K) de longues chaînes de caractères
  5. Quelques entiers
  6. Beaucoup (~10K) d'entiers

Et pour chaque scénario, recherche des valeurs qui apparaissent :

  1. Au début de la liste ("start", index 0)
  2. Près du début de la liste ("early", index 1)
  3. Au milieu de la liste ("milieu", indice compte/2)
  4. Près de la fin de la liste ("late", index count-2)
  5. A la fin de la liste ("end", index count-1)

Avant chaque scénario, j'ai généré des listes de chaînes de caractères aléatoires de taille aléatoire, puis j'ai introduit chaque liste dans un hashset. Chaque scénario a été exécuté 10 000 fois, essentiellement :

(pseudo-code de test)

stopwatch.start
for X times
    exists = list.Contains(lookup);
stopwatch.stop

stopwatch.start
for X times
    exists = hashset.Contains(lookup);
stopwatch.stop

Exemple de sortie

Testé sur Windows 7, 12GB Ram, 64 bit, Xeon 2.8GHz

---------- Testing few small strings ------------
Sample items: (16 total)
vgnwaloqf diwfpxbv tdcdc grfch icsjwk
...

Benchmarks:
1: hashset: late -- 100.00 % -- [Elapsed: 0.0018398 sec]
2: hashset: middle -- 104.19 % -- [Elapsed: 0.0019169 sec]
3: hashset: end -- 108.21 % -- [Elapsed: 0.0019908 sec]
4: list: early -- 144.62 % -- [Elapsed: 0.0026607 sec]
5: hashset: start -- 174.32 % -- [Elapsed: 0.0032071 sec]
6: list: middle -- 187.72 % -- [Elapsed: 0.0034536 sec]
7: list: late -- 192.66 % -- [Elapsed: 0.0035446 sec]
8: list: end -- 215.42 % -- [Elapsed: 0.0039633 sec]
9: hashset: early -- 217.95 % -- [Elapsed: 0.0040098 sec]
10: list: start -- 576.55 % -- [Elapsed: 0.0106073 sec]

---------- Testing many small strings ------------
Sample items: (10346 total)
dmnowa yshtrxorj vthjk okrxegip vwpoltck
...

Benchmarks:
1: hashset: end -- 100.00 % -- [Elapsed: 0.0017443 sec]
2: hashset: late -- 102.91 % -- [Elapsed: 0.0017951 sec]
3: hashset: middle -- 106.23 % -- [Elapsed: 0.0018529 sec]
4: list: early -- 107.49 % -- [Elapsed: 0.0018749 sec]
5: list: start -- 126.23 % -- [Elapsed: 0.0022018 sec]
6: hashset: early -- 134.11 % -- [Elapsed: 0.0023393 sec]
7: hashset: start -- 372.09 % -- [Elapsed: 0.0064903 sec]
8: list: middle -- 48,593.79 % -- [Elapsed: 0.8476214 sec]
9: list: end -- 99,020.73 % -- [Elapsed: 1.7272186 sec]
10: list: late -- 99,089.36 % -- [Elapsed: 1.7284155 sec]

---------- Testing few long strings ------------
Sample items: (19 total)
hidfymjyjtffcjmlcaoivbylakmqgoiowbgxpyhnrreodxyleehkhsofjqenyrrtlphbcnvdrbqdvji...
...

Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0018266 sec]
2: list: start -- 115.76 % -- [Elapsed: 0.0021144 sec]
3: list: middle -- 143.44 % -- [Elapsed: 0.0026201 sec]
4: list: late -- 190.05 % -- [Elapsed: 0.0034715 sec]
5: list: end -- 193.78 % -- [Elapsed: 0.0035395 sec]
6: hashset: early -- 215.00 % -- [Elapsed: 0.0039271 sec]
7: hashset: end -- 248.47 % -- [Elapsed: 0.0045386 sec]
8: hashset: start -- 298.04 % -- [Elapsed: 0.005444 sec]
9: hashset: middle -- 325.63 % -- [Elapsed: 0.005948 sec]
10: hashset: late -- 431.62 % -- [Elapsed: 0.0078839 sec]

---------- Testing many long strings ------------
Sample items: (5000 total)
yrpjccgxjbketcpmnvyqvghhlnjblhgimybdygumtijtrwaromwrajlsjhxoselbucqualmhbmwnvnpnm
...

Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0016211 sec]
2: list: start -- 132.73 % -- [Elapsed: 0.0021517 sec]
3: hashset: start -- 231.26 % -- [Elapsed: 0.003749 sec]
4: hashset: end -- 368.74 % -- [Elapsed: 0.0059776 sec]
5: hashset: middle -- 385.50 % -- [Elapsed: 0.0062493 sec]
6: hashset: late -- 406.23 % -- [Elapsed: 0.0065854 sec]
7: hashset: early -- 421.34 % -- [Elapsed: 0.0068304 sec]
8: list: middle -- 18,619.12 % -- [Elapsed: 0.3018345 sec]
9: list: end -- 40,942.82 % -- [Elapsed: 0.663724 sec]
10: list: late -- 41,188.19 % -- [Elapsed: 0.6677017 sec]

---------- Testing few ints ------------
Sample items: (16 total)
7266092 60668895 159021363 216428460 28007724
...

Benchmarks:
1: hashset: early -- 100.00 % -- [Elapsed: 0.0016211 sec]
2: hashset: end -- 100.45 % -- [Elapsed: 0.0016284 sec]
3: list: early -- 101.83 % -- [Elapsed: 0.0016507 sec]
4: hashset: late -- 108.95 % -- [Elapsed: 0.0017662 sec]
5: hashset: middle -- 112.29 % -- [Elapsed: 0.0018204 sec]
6: hashset: start -- 120.33 % -- [Elapsed: 0.0019506 sec]
7: list: late -- 134.45 % -- [Elapsed: 0.0021795 sec]
8: list: start -- 136.43 % -- [Elapsed: 0.0022117 sec]
9: list: end -- 169.77 % -- [Elapsed: 0.0027522 sec]
10: list: middle -- 237.94 % -- [Elapsed: 0.0038573 sec]

---------- Testing many ints ------------
Sample items: (10357 total)
370826556 569127161 101235820 792075135 270823009
...

Benchmarks:
1: list: early -- 100.00 % -- [Elapsed: 0.0015132 sec]
2: hashset: end -- 101.79 % -- [Elapsed: 0.0015403 sec]
3: hashset: early -- 102.08 % -- [Elapsed: 0.0015446 sec]
4: hashset: middle -- 103.21 % -- [Elapsed: 0.0015618 sec]
5: hashset: late -- 104.26 % -- [Elapsed: 0.0015776 sec]
6: list: start -- 126.78 % -- [Elapsed: 0.0019184 sec]
7: hashset: start -- 130.91 % -- [Elapsed: 0.0019809 sec]
8: list: middle -- 16,497.89 % -- [Elapsed: 0.2496461 sec]
9: list: end -- 32,715.52 % -- [Elapsed: 0.4950512 sec]
10: list: late -- 33,698.87 % -- [Elapsed: 0.5099313 sec]

7 votes

Intéressant. Merci de l'avoir fait. Malheureusement, je soupçonne que ces discussions déclenchent des refactorisations inutiles. J'espère que la plupart des gens retiendront que dans le pire des cas, List ne prend toujours que 0,17 millisecondes pour effectuer une seule recherche, et il est peu probable qu'elle nécessite une substitution de HashSet jusqu'à ce que la fréquence de consultation atteigne des niveaux absurdes. À ce moment-là, l'utilisation de List est généralement le moindre des problèmes.

0 votes

Ce n'est pas une information réelle pour le moment... Ou peut-être que c'est faux à l'origine... Je viens de vérifier les petites valeurs de 2 à 8 caractères. Liste / HashSet ont été créés pour chaque 10 valeurs... HashSet plus lent pour 30%... Si la capacité de la liste est utilisée, la différence est même de ~40%. HashSet devient plus rapide de 10% seulement si la liste n'a pas de capacité spécifiée et vérifie chaque valeur avant de l'ajouter à la liste entière.

0 votes

Si le nombre d'éléments est réduit à 4, la liste l'emporte à nouveau, même dans le pire des scénarios (avec une différence de 10%). Je ne recommande donc pas d'utiliser HashSet pour une petite collection de chaînes de caractères (disons < 20). Et c'est ce qui est différent de vos tests "quelques petits".

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X