2009-05-05 9 views
74

Quelqu'un pourrait-il m'expliquer pourquoi la fonction Contains() de la générique est si lente?
J'ai une liste avec environ un million de numéros, et le code qui vérifie constamment s'il y a un nombre spécifique dans ces numéros.
J'ai essayé de faire la même chose en utilisant Dictionary et la fonction ContainsKey(), et c'était environ 10-20 fois plus rapide qu'avec la liste.
Bien sûr, je ne veux pas vraiment utiliser Dictionary à cette fin, car il n'était pas destiné à être utilisé de cette façon.
Donc, la vraie question ici est, existe-t-il une alternative à la List.Contains(), mais pas aussi compliqué que Dictionary.ContainsKey()?
Merci d'avance!C#, Liste <T> .Contains() - trop lent?

+2

Quel est le problème avec le dictionnaire? Il est destiné à être utilisé dans un cas comme le vôtre. – Kamarey

+4

@Kamarey: HashSet peut être une meilleure option. –

+0

HashSet est ce que je cherchais. – DSent

Répondre

134

Si vous en train de vérifier l'existence, HashSet<T> dans .NET 3.5 est votre meilleure option - performance comme le dictionnaire, mais pas de paire clé/valeur - seulement les valeurs:

HashSet<int> data = new HashSet<int>(); 
    for (int i = 0; i < 1000000; i++) 
    { 
     data.Add(rand.Next(50000000)); 
    } 
    bool contains = data.Contains(1234567); // etc 
5

Le dictionnaire n'est pas si mauvais, car les clés d'un dictionnaire sont conçues pour être trouvées rapidement. Pour trouver un nombre dans une liste, il doit parcourir toute la liste.

Bien sûr, le dictionnaire ne fonctionne que si vos numéros sont uniques et non commandés.

Je pense qu'il existe également une classe HashSet<T> dans .NET 3.5, elle ne permet également que des éléments uniques.

+0

Un dictionnaire peut également stocker efficacement des objets non-uniques - utiliser l'entier pour compter le nombre de doublons. Par exemple, vous stockez la liste {a, b, a} sous la forme {a = 2, b = 1}. Il perd l'ordination, bien sûr. – MSalters

25

List.Contains est une opération O (n). Dictionary.ContainsKey est une opération O (1), car il utilise le hashcode des objets comme une clé, ce qui vous donne une capacité de recherche plus rapide.

Je ne pense pas que ce soit une bonne idée d'avoir une liste qui contient un million d'entrées. Je ne pense pas que la classe List ait été conçue à cette fin. :)

N'est-il pas possible de sauvegarder ces entités millons dans un SGBDR par exemple, et d'effectuer des requêtes sur cette base de données?

Si ce n'est pas possible, j'utiliserais quand même un dictionnaire.

+12

Je ne pense pas qu'il y ait quelque chose d'inapproprié dans une liste avec un million d'éléments, c'est juste que vous ne voulez probablement pas continuer à faire des recherches linéaires à travers. –

+0

J'ai fini par utiliser HashSet pour ma tâche, mais merci quand même! – DSent

+0

D'accord, il n'y a rien de mal avec une liste ou un tableau avec autant d'entrées. Il suffit de ne pas rechercher les valeurs. –

2

Un SortedList sera plus rapide à la recherche (mais plus lent pour insérer des éléments)

1

Pourquoi un dictionnaire inapproprié?

Pour voir si une valeur particulière figure dans la liste, vous devez parcourir la liste entière. Avec un dictionnaire (ou un autre conteneur basé sur le hachage), il est beaucoup plus rapide d'affiner le nombre d'objets que vous devez comparer. La clé (dans votre cas, le nombre) est hachée et cela donne au dictionnaire le sous-ensemble fractionnaire d'objets à comparer.

0

J'utilise ce dans le Compact Framework où il n'y a pas de support pour HashSet, j'ai opté pour un Dictionary où les deux chaînes sont la valeur que je recherche.

Cela signifie que j'obtiens la liste <> fonctionnalité avec les performances du dictionnaire. C'est un peu hacky, mais ça marche.

+1

Si vous utilisez un dictionnaire à la place d'un HashSet, vous pouvez également définir la valeur sur "" plutôt que sur la même chaîne que la clé. De cette façon, vous utiliserez moins de mémoire. Vous pouvez également utiliser le dictionnaire et les définir tous sur true (ou false). Je ne sais pas qui utiliserait moins de mémoire, une chaîne vide ou un booléen. Ma conjecture serait bool. – TTT

+0

Dans le dictionnaire, une référence 'chaîne' et une valeur' bool' font une différence de 3 ou 7 octets, respectivement pour les systèmes 32 ou 64 bits. Notez, cependant, que la taille de chaque entrée est arrondie à des multiples de 4 ou 8, respectivement. Le choix entre 'string' et' bool' pourrait donc ne faire aucune différence dans la taille du tout. La chaîne vide '" "' existe toujours en mémoire sous la forme de la propriété statique 'string.Empty', donc cela ne fait aucune différence que vous l'utilisiez ou non dans le dictionnaire. (Et il est utilisé ailleurs de toute façon.) – Wormbo

2

Ce n'est pas exactement une réponse à votre question, mais j'ai une classe qui augmente les performances de Contains() sur une collection. J'ai sous-classé une file d'attente et ajouté un dictionnaire qui mappe des hashcodes aux listes d'objets.La fonction Dictionary.Contains() est O (1) alors que List.Contains(), Queue.Contains() et Stack.Contains() sont O (n).

Le type de valeur du dictionnaire est une file d'attente contenant des objets avec le même code de hachage. L'appelant peut fournir un objet de classe personnalisé qui implémente IEqualityComparer. Vous pouvez utiliser ce modèle pour les piles ou les listes. Le code aurait besoin de quelques changements.

/// <summary> 
/// This is a class that mimics a queue, except the Contains() operation is O(1) rather  than O(n) thanks to an internal dictionary. 
/// The dictionary remembers the hashcodes of the items that have been enqueued and dequeued. 
/// Hashcode collisions are stored in a queue to maintain FIFO order. 
/// </summary> 
/// <typeparam name="T"></typeparam> 
private class HashQueue<T> : Queue<T> 
{ 
    private readonly IEqualityComparer<T> _comp; 
    public readonly Dictionary<int, Queue<T>> _hashes; //_hashes.Count doesn't always equal base.Count (due to collisions) 

    public HashQueue(IEqualityComparer<T> comp = null) : base() 
    { 
     this._comp = comp; 
     this._hashes = new Dictionary<int, Queue<T>>(); 
    } 

    public HashQueue(int capacity, IEqualityComparer<T> comp = null) : base(capacity) 
    { 
     this._comp = comp; 
     this._hashes = new Dictionary<int, Queue<T>>(capacity); 
    } 

    public HashQueue(IEnumerable<T> collection, IEqualityComparer<T> comp = null) :  base(collection) 
    { 
     this._comp = comp; 

     this._hashes = new Dictionary<int, Queue<T>>(base.Count); 
     foreach (var item in collection) 
     { 
      this.EnqueueDictionary(item); 
     } 
    } 

    public new void Enqueue(T item) 
    { 
     base.Enqueue(item); //add to queue 
     this.EnqueueDictionary(item); 
    } 

    private void EnqueueDictionary(T item) 
    { 
     int hash = this._comp == null ? item.GetHashCode() :  this._comp.GetHashCode(item); 
     Queue<T> temp; 
     if (!this._hashes.TryGetValue(hash, out temp)) 
     { 
      temp = new Queue<T>(); 
      this._hashes.Add(hash, temp); 
     } 
     temp.Enqueue(item); 
    } 

    public new T Dequeue() 
    { 
     T result = base.Dequeue(); //remove from queue 

     int hash = this._comp == null ? result.GetHashCode() : this._comp.GetHashCode(result); 
     Queue<T> temp; 
     if (this._hashes.TryGetValue(hash, out temp)) 
     { 
      temp.Dequeue(); 
      if (temp.Count == 0) 
       this._hashes.Remove(hash); 
     } 

     return result; 
    } 

    public new bool Contains(T item) 
    { //This is O(1), whereas Queue.Contains is (n) 
     int hash = this._comp == null ? item.GetHashCode() : this._comp.GetHashCode(item); 
     return this._hashes.ContainsKey(hash); 
    } 

    public new void Clear() 
    { 
     foreach (var item in this._hashes.Values) 
      item.Clear(); //clear collision lists 

     this._hashes.Clear(); //clear dictionary 

     base.Clear(); //clear queue 
    } 
} 

Mon test simple montre que mes HashQueue.Contains() tourne beaucoup plus vite que Queue.Contains(). L'exécution du code de test avec le nombre défini sur 10 000 prend 0,00045 secondes pour la version HashQueue et 0,37 secondes pour la version de la file d'attente. Avec un compte de 100 000, la version HashQueue prend 0,0031 secondes alors que la file d'attente prend 36,38 secondes!

Voici mon code de test:

static void Main(string[] args) 
{ 
    int count = 10000; 

    { //HashQueue 
     var q = new HashQueue<int>(count); 

     for (int i = 0; i < count; i++) //load queue (not timed) 
      q.Enqueue(i); 

     System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); 
     for (int i = 0; i < count; i++) 
     { 
      bool contains = q.Contains(i); 
     } 
     sw.Stop(); 
     Console.WriteLine(string.Format("HashQueue, {0}", sw.Elapsed)); 
    } 

    { //Queue 
     var q = new Queue<int>(count); 

     for (int i = 0; i < count; i++) //load queue (not timed) 
      q.Enqueue(i); 

     System.Diagnostics.Stopwatch sw = System.Diagnostics.Stopwatch.StartNew(); 
     for (int i = 0; i < count; i++) 
     { 
      bool contains = q.Contains(i); 
     } 
     sw.Stop(); 
     Console.WriteLine(string.Format("Queue,  {0}", sw.Elapsed)); 
    } 

    Console.ReadLine(); 
} 
+0

Je viens d'ajouter 3 cas de test pour HashSet qui semble obtenir des résultats encore meilleurs que votre solution: 'HashQueue, 00: 00: 00.0004029' ' file d'attente, 00: 00: 00,3901439 ' ' HashSet, 00: 00: 00.0001716' – psulek

7

je pense avoir la réponse! Oui, il est vrai que Contient() sur une liste (tableau) est O (n), mais si le tableau est court et que vous utilisez des types de valeurs, cela devrait être assez rapide. Mais en utilisant le CLR Profiler [téléchargement gratuit de Microsoft], j'ai découvert que Contains() est des valeurs de boxe afin de les comparer, ce qui nécessite une allocation de tas, ce qui est très cher (lent). [Note: Ceci est .Net 2.0; autres versions .Net non testées.]

Voici l'histoire complète et la solution. Nous avons une énumération appelée "VI" et avons créé une classe appelée "ValueIdList" qui est un type abstrait pour une liste (tableau) d'objets VI. L'implémentation originale était dans l'ancien .Net 1.1 jours, et il a utilisé un ArrayList encapsulé. Nous avons récemment découvert dans http://blogs.msdn.com/b/joshwil/archive/2004/04/13/112598.aspx qu'une liste générique (Liste < VI>) fonctionne beaucoup mieux que ArrayList sur les types de valeurs (comme notre VI enum) car les valeurs n'ont pas à être encadrées. C'est vrai et ça a marché ... presque.

Le profileur CLR a révélé une surprise. Voici une partie de l'allocation Graphique:

  • ValueIdList :: Contient bool (VI) 5.5MB (34,81%)
  • Generic.List :: Contient bool (< UNKNOWN>) 5.5MB (34,81%)
  • Generic.ObjectEqualityComparer < T> :: Equals bool (< UNKNOWN> < UNKNOWN>) 5.5MB (34,88%)
  • Values.VI 7.7MB (49,03%)

Comme vous pouvez le voir, Contient() surpri appelle simplement Generic.ObjectEqualityComparer.Equals(), ce qui nécessite apparemment la mise en boîte d'une valeur de VI, ce qui nécessite une allocation de tas coûteuse. Il est étrange que Microsoft élimine la boxe sur la liste, seulement pour l'exiger à nouveau pour une opération simple comme celle-ci.

Notre solution consistait à réécrire l'implémentation de Contains(), ce qui était facile dans notre cas puisque nous encapsulions déjà l'objet liste générique (_items). Voici le code simple:

public bool Contains(VI id) 
{ 
    return IndexOf(id) >= 0; 
} 

public int IndexOf(VI id) 
{ 
    int i, count; 

    count = _items.Count; 
    for (i = 0; i < count; i++) 
    if (_items[i] == id) 
     return i; 
    return -1; 
} 

public bool Remove(VI id) 
{ 
    int i; 

    i = IndexOf(id); 
    if (i < 0) 
    return false; 
    _items.RemoveAt(i); 

    return true; 
} 

La comparaison des valeurs VI est maintenant fait dans notre propre version de IndexOf() qui ne nécessite pas la boxe, et il est très rapide. Notre programme a accéléré de 20% après cette simple réécriture. O (n) ... pas de problème! Évitez simplement l'utilisation de la mémoire gaspillée!

+0

Merci pour le conseil, j'ai été pris par de mauvaises performances de boxe moi-même. Une implémentation 'Contains' personnalisée est beaucoup plus rapide pour mon cas d'utilisation. –

Questions connexes