web-dev-qa-db-fra.com

Union vs Unionwith dans HashSet

Quelle est la différence entre HashSet.Union contre HashSet.Unionwith quand je combine 2 hashsets.

J'essaye de combiner comme ça:

HashSet<EngineType> enginesSupportAll = _filePolicyEvaluation.EnginesSupportAll;
        enginesSupportAll = enginesSupportAll != null ? new HashSet<EngineType>(engines.Union(enginesSupportAll)) : enginesSupportAll;

quelle est la meilleure méthode pour cet exemple et pourquoi?

12
Max.Futerman

Eh bien, ce n'est pas HashSet.Union mais Enumerable.Union , vous utilisez donc une méthode d'extension LINQ qui fonctionne avec tout type de IEnumerable<> tandis que HashSet.UnionWith est une véritable méthode HashSet qui modifie l'instance actuelle.

  • Union renvoie un IEnumerable<TSource>
  • UnionWith est void, il modifie l'instance actuelle de HashSet
  • peut-être que UnionWith est légèrement plus efficace car il peut être optimisé

Si vous ne voulez prendre en charge aucun type de séquence dans votre méthode, donc HashSet est fixe et vous pouvez le modifier, utilisez-le, sinon utilisez l'extension LINQ. Si vous créez l'instance HashSet juste à cet effet, cela n'a pas vraiment d'importance et je préférerais que LINQ soit plus flexible et puisse enchaîner ma requête.

14
Tim Schmelter

Étant donné HashSet<T> [~ # ~] un [~ # ~] et HashSet<T> [~ # ~] b [~ # ~] il y a quatre façons de [~ # ~] a [~ # ~] [~ # ~] b [~ # ~]:

  1. new HashSet<t>(A.Union(B))
    (voir HashSet<T&>(IEnumerable<T>) et Enumerable.Union<T>(IEnumerable<T>, IEnumerable<T>) )
  2. A.UnionWith(B)
  3. HashSet<T> C = new HashSet<T>(A); C.UnionWith(B);
  4. new HashSet<t>(A.Concat(B))
    (voir Enumerable.Concat<T>(IEnumerable<T>, IEnumerable<T>) )

Chacun a ses avantages et désavantages:

  • 1 et 4 sont des expressions qui aboutissent à un HashSet tandis que 2 et 3 sont des instructions ou des blocs d'instructions.
    Les expressions 1 et 4 peuvent être utilisées à plus d'endroits que 2 et 3. Par exemple, l'utilisation de 2 ou 3 dans une expression de syntaxe de requête linq est lourde:
    from x in setofSetsA as IEnumerable<HashSet<T>> from y in setOfSetsB as IEnumerable<HashSet<T>> select x.UnionWith(y) ne fonctionnera pas puisque UnionWith renvoie void.
  • 1, 3 et 4 conservent [~ # ~] a [~ # ~] et [~ # ~] b [~ # ~] comme ils sont et retournent un nouvel ensemble tandis que 2 modifie [~ # ~] a [~ # ~].
    Il existe des situations où la modification d'un des ensembles d'origine est mauvaise et il existe des situations où au moins l'un des ensembles d'origine peut être modifié sans conséquences négatives.
  • Coût de calcul:

    A.UnionWith(B)
    (≈ O ((log (| A∪B |) - log (| A |)) * | A∪B |) + O (| B |))

    HashSet<T> C = new HashSet<T>(A); C.UnionWith(B);
    (≈ O ((log (| A∪B |) - log (| A |)) * | A∪B |) + O (| A | + | B |))

    HashSet<T>(A.Concat(B))
    (≈ O (log (| A∪B |) * | A∪B |) + O (| A | + | B |))

    HashSet<T>(A.Union(B))
    (≈ 2 * O (log (| A∪B |) * | A∪B |) + O (| A | + | B | + | A∪B |))

    La section suivante se penche sur la source de référence pour voir la base de ces estimations de performance.

Performance

HashSet<T>

Dans les options d'union 1, 3 et 4, le constructeur HashSet<T>(IEnumerable<T>, IEqualityComparer<T>) est utilisé pour créer un HashSet<T> À partir d'un IEnumerable<T>. Si le IEnumerable<T> Passé a une propriété Count —i.e. s'il s'agit d'un ICollection<T> -, cette propriété est utilisée pour définir la taille du nouveau HashSet:

int suggestedCapacity = 0;
ICollection<T> coll = collection as ICollection<T>;
if (coll != null) {
    suggestedCapacity = coll.Count;
}
Initialize(suggestedCapacity);

- HashSet.cs Ligne 136–141

La méthode [Count()][10] n'est jamais appelée. Ainsi, si le nombre de IEnumerable peut être récupéré sans effort, il est utilisé pour réserver la capacité; sinon, le HashSet grandit et se réalloue lorsque de nouveaux éléments sont ajoutés.
Dans l'option 1 A.Union(B) et l'option 4 A.Concat(B) ne sont pas ICollection<T> Donc le HashSet créé va grandir et réaffecter quelques-uns (≈ log (| A∪B |)) fois. L'option 3 peut utiliser le Count de [~ # ~] a [~ # ~].

Le constructeur appelle UnionWith pour remplir le nouveau HashSet vide:

this.UnionWith(collection);

- HashSet.cs Ligne 14

UnionWith(IEnumerable<T>) itère sur les éléments du IEnumerable<T> passé en argument et appelle AddIfNotPresent(T) pour chacun.

AddIfNotPresent(T) insère des éléments et garantit que les doublons ne sont jamais insérés dans l'ensemble.
HashSet<T> Est implémenté comme un tableau d'emplacements, m_slots , et un tableau de compartiments, m_buckets . Un compartiment contient juste un index int dans le tableau m_slots. Par compartiment, les Slot s dans m_slots Forment une liste liée avec un index vers le Slot suivant dans m_slots.

AddIfNotPresent(T) saute dans le compartiment correct puis parcourt sa liste chaînée pour vérifier si l'élément est déjà présent:

for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) {
    if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, value)) {
        return false;
    }
}

- HashSet.cs Ligne 968–975

Ensuite, un index gratuit est trouvé et un emplacement est réservé. Tout d'abord, la liste des emplacements libres, m_freelist , est vérifiée. Lorsqu'il n'y a pas d'emplacements dans la liste libre, l'emplacement suivant vide dans le tableau m_slots Est utilisé. Plus de capacité est réservée (via IncreaseCapacity() ) s'il n'y a pas de slots dans la liste libre et pas de slots vides:

int index;
if (m_freeList >= 0) {
    index = m_freeList;
    m_freeList = m_slots[index].next;
}
else {
    if (m_lastIndex == m_slots.Length) {
        IncreaseCapacity();
        // this will change during resize
        bucket = hashCode % m_buckets.Length;
    }
    index = m_lastIndex;
    m_lastIndex++;
}

- HashSet.cs Ligne 977–99

AddIfNotPresent(T) a trois opérations qui demandent un certain calcul: l'appel à object.GetHashCode() , appelant object.Equals(object) quand il est une collision et IncreaseCapacity(). L'ajout réel d'un élément n'entraîne que le coût de la mise en place de certains pointeurs et de certains pouces.

Lorsque HashSet<T> Doit IncreaseCapacity() la capacité est au moins doublée. Nous pouvons donc conclure qu'en moyenne un HashSet<T> Est rempli à 75%. Si les hachages sont répartis uniformément, l'espérance d'une collision de hachage est également de 75%.

SetCapacity(int, bool) , appelé par IncreaseCapacity(), est le plus cher: il alloue de nouveaux tableaux, il copie l'ancien tableau d'emplacement dans le nouveau tableau, et il recalcule le listes de compartiments:

Slot[] newSlots = new Slot[newSize];
if (m_slots != null) {
    Array.Copy(m_slots, 0, newSlots, 0, m_lastIndex);
}

...

int[] newBuckets = new int[newSize];
for (int i = 0; i < m_lastIndex; i++) {
    int bucket = newSlots[i].hashCode % newSize;
    newSlots[i].next = newBuckets[bucket] - 1;
    newBuckets[bucket] = i + 1;
}
m_slots = newSlots;
m_buckets = newBuckets;

- HashSet.cs Ligne 929–949

Les options 1 et 4 (new HashSet<T>(A.Union(B))) entraîneront un peu plus d'appels à IncreaseCapacity(). Le coût —sans le coût de A.Union(B) ou A.Concat(B)— est d'environ O (log (| A∪B |) * | A∪B | ) .
Alors que lorsque vous utilisez l'option 2 (A.UnionWith(B)) ou l'option 3 (HashSet<T> C = new HashSet<T>(A); C.UnionWith(B)), nous obtenons une "remise" de log (| A |) sur le coût: O ((log (| A∪B |) - log (| A |)) * | A∪B |) . Il est payant (légèrement) d'utiliser le plus grand ensemble comme cible dans laquelle l'autre est fusionné.

Enumerable<T>.Union(IEnumerable<T>)

Enumerable<T>.Union(IEnumerable<T>) est implémentée via UnionIterator<T>(IEnumerable<T>, IEnumerable<T>, IEqualityComparer<T>) .
Le UnionIterator utilise Set<T> —une classe interne dans Enumerable.cs - qui est très similaire à HashSet<T>. UnionIterator paresseusement Add(T) s éléments de [~ # ~] a [~ # ~] et [~ # ~] b [~ # ~] à ce Set<T> et yields les éléments s'ils peuvent être ajoutés. Le travail se fait dans Find(T, bool) qui est similaire à HashSet<T>.AddIfNotPresent(T). Vérifiez si l'élément est déjà présent:

int hashCode = InternalGetHashCode(value);
for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next) {
    if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, value)) return true;
}

- Enumerable.cs Ligne 2423–2426

Trouvez un index gratuit et réservez un emplacement:

int index;
if (freeList >= 0) {
    index = freeList;
    freeList = slots[index].next;
}
else {
    if (count == slots.Length) Resize();
    index = count;
    count++;
}
int bucket = hashCode % buckets.Length;
slots[index].hashCode = hashCode;
slots[index].value = value;
slots[index].next = buckets[bucket] - 1;
buckets[bucket] = index + 1;

- Enumerable.cs Ligne 2428–2442

Resize() est similaire à IncreaseCapacity(). La plus grande différence entre les deux est que Resize() n'utilise pas nombre premier pour le nombre de compartiments, donc avec une mauvaise GetHashCode() il y a une chance légèrement plus élevée pour les collisions. Code de Resize():

int newSize = checked(count * 2 + 1);
int[] newBuckets = new int[newSize];
Slot[] newSlots = new Slot[newSize];
Array.Copy(slots, 0, newSlots, 0, count);
for (int i = 0; i < count; i++) {
    int bucket = newSlots[i].hashCode % newSize;
    newSlots[i].next = newBuckets[bucket] - 1;
    newBuckets[bucket] = i + 1;
}
buckets = newBuckets;
slots = newSlots;

- Enumerable.cs Ligne 2448–2458

Le coût de performance de A.Union(B) n'est pas significativement différent de celui de HashSet<T> C = new HashSet<T>(); C.UnionWith(A); C.UnionWith(B);. Dans l'option 1 (new HashSet<T>(A.Union(B))), le même HashSet est créé deux fois, ce qui entraîne un 2 * O très coûteux (log (| A∪B |) * (| A∪B |)). L'option 4 résulte de la connaissance de l'implémentation de HashSet<T>(IEnumerable<T>) et Enumerable.Union(IEnumerable<T>, IEnumerable<T>). Il évite la A.Union(B) redondante conduisant à un coût de O (log (| A∪B |) * | A∪B |).

13