Новый KeyValuePair <UInt32, UInt32> (i, j).GetHashCode(); Высокая скорость дублирования

В поисках быстрого составного ключа для словаря я столкнулся с аномалией, которую я не могу понять и не оправдать.

В ограниченном тестировании

Dictionary<KeyValuePair<UInt32, UInt32>, string>

значительно медленнее (200: 1), чем

Dictionary<KeyValuePair<UInt16, UInt16>, string>

Тест на две петли от 0 до 1000 Заполните и затем ContainsKey

         Poplulate     ContainsKey  
UInt32    92085         86578  
UInt16     2201           431

Проблема заключается в том, что

new KeyValuePair<UInt32, UInt32>(i, j).GetHashCode();

дает МНОГИЕ дубликаты.
В циклах я и j 1024 создается только 1024 уникальных значения хэша.

На основе лавинного комментария от CasperOne пробовали я * 31 и j * 97 (два простых числа), и это привело к тому, что 105280 уникально на 1024X1024. Еще много дубликатов. CasperOne Я знаю, что это не то же самое, что и случайное. Но это не моя работа по рандомизации ввода. GetHashCode() предполагается рандомизировать вывод.

Почему большое количество дубликатов?

Тот же цикл на

new KeyValuePair<UInt16, UInt16>(i, j).GetHashCode();

дает 1024 X 1024 уникальных хэш-кодов (отлично).

Int32 имеет ту же проблему.

Эти повторяющиеся значения хеша убивают

Dictionary<KeyValuePair<UInt32, UInt32>, string> 

Tuple также генерирует много дубликатов, которые он не деградирует в Int32 по сравнению с Int16.

Время генерации исходного KVP и исходного кода KPV.GetHashCode аналогично.

Такая же аномалия с HashSet.

Dictionary<KeyValuePair<UInt32, UInt32>, string> dKVPu32 = new Dictionary<KeyValuePair<UInt32, UInt32>, string>();
Dictionary<KeyValuePair<UInt16, UInt16>, string> dKVPu16 = new Dictionary<KeyValuePair<UInt16, UInt16>, string>();
KeyValuePair<UInt32, UInt32> kvpUint32;
KeyValuePair<UInt16, UInt16> kvpUint16;
int range = 1000;
Int32 hashCode;
HashSet<Int32> kvpUint32Hash = new HashSet<Int32>();
HashSet<Int32> kvpUint16Hash = new HashSet<Int32>();

Stopwatch sw = new Stopwatch();
sw.Start();
for (UInt32 i = 0; i < range; i++)
{
    for (UInt32 j = 0; j < range; j++)
    {
        kvpUint32 = new KeyValuePair<UInt32, UInt32>(i, j);
    }
}
Console.WriteLine("UInt32  raw " + sw.ElapsedMilliseconds.ToString());
//  7
sw.Restart();
for (UInt16 i = 0; i < range; i++)
{
    for (UInt16 j = 0; j < range; j++)
    {
        kvpUint16 = new KeyValuePair<UInt16, UInt16>(i, j);
    }
}
Console.WriteLine("UInt16  raw " + sw.ElapsedMilliseconds.ToString());
//  6
sw.Restart();
for (UInt32 i = 0; i < range; i++)
{
    for (UInt32 j = 0; j < range; j++)
    {
        hashCode = new KeyValuePair<UInt32, UInt32>(i, j).GetHashCode();
        kvpUint32Hash.Add(hashCode);
    }
}
Console.WriteLine("UInt32  GetHashCode " + sw.ElapsedMilliseconds.ToString() + "  unique count " + kvpUint32Hash.Count.ToString());
//  285   1024
sw.Restart();
for (UInt16 i = 0; i < range; i++)
{
    for (UInt16 j = 0; j < range; j++)
    {
        hashCode = new KeyValuePair<UInt16, UInt16>(i, j).GetHashCode();
        kvpUint16Hash.Add(hashCode);
    }
}
Console.WriteLine("UInt16  GetHashCode " + sw.ElapsedMilliseconds.ToString() + "  unique count " + kvpUint16Hash.Count.ToString());
//  398 1000000
sw.Restart();
Console.ReadLine();
for (UInt32 i = 0; i < range; i++)
{
    for (UInt32 j = 0; j < range; j++)
    {
        dKVPu32.Add(new KeyValuePair<UInt32, UInt32>(i, j), String.Format("{0} {1}", i.ToString(), j.ToString()));
    }
}
Console.WriteLine("hsKVPu32 pop " + sw.ElapsedMilliseconds.ToString());
//  92085
sw.Restart();
for (UInt32 i = 0; i < range; i++)
{
    for (UInt32 j = 0; j < range; j++)
    {
        if (!dKVPu32.ContainsKey(new KeyValuePair<UInt32, UInt32>(i, j))) Debug.WriteLine("Opps"); ;
    }
}
Console.WriteLine("hsKVPu32 find " + sw.ElapsedMilliseconds.ToString());
//  86578
dKVPu32.Clear();
dKVPu32 = null;
GC.Collect();
sw.Restart();
for (UInt16 i = 0; i < range; i++)
{
    for (UInt16 j = 0; j < range; j++)
    {
        dKVPu16.Add(new KeyValuePair<UInt16, UInt16>(i, j), String.Format("{0} {1}", i.ToString(), j.ToString()));
    }
}
Console.WriteLine("hsKVPu16 pop " + sw.ElapsedMilliseconds.ToString());
//   2201
sw.Restart();
for (UInt16 i = 0; i < range; i++)
{
    for (UInt16 j = 0; j < range; j++)
    {
        if (!dKVPu16.ContainsKey(new KeyValuePair<UInt16, UInt16>(i, j))) Debug.WriteLine("Opps"); ;
    }
}
sw.Stop();
Console.WriteLine("hsKVPu16 find " + sw.ElapsedMilliseconds.ToString());
//  431

P.S. Самый быстрый - упаковать .E.G. ((UInt32) int1 < 16) | int2;

Хэш первого столбца UInt32 равен хэшу KVP следующих двух.

2281371105 8 992
2281371104 8 993
2281371107 8 994

2281371145 0 0
2281371147 0 2
2281371149 0 4
2281371151 0 6
2281371137 0 8

2281371144 0 1
2281371146 0 3
2281371148 0 5
2281371150 0 7
2281371136 0 9

2281371144 1 0
2281371145 1 1
2281371146 1 2
2281371147 1 3
2281371148 1 4
2281371149 1 5
2281371150 1 6
2281371151 1 7
2281371136 1 8
2281371137 1 9

2281371147 2 0
2281371146 2 1
2281371144 2 3
2281371151 2 4
2281371150 2 5
2281371149 2 6
2281371148 2 7
2281371139 2 8

Единственный шаблон, который я нашел, заключается в том, что либо сумма, либо разница или соответствие KVP.
Но не удалось найти шаблон для того, когда суммировать и когда вычесть.
Это плохой хеш, поэтому зная, что это такое, мало имеет значения.

Ответ 1

Во-первых, мы можем обойтись без хронометража этого вопроса - мне кажется, что это действительно касается хеш-коллизий, так как очевидно, что они убьют производительность.

Итак, вопрос в том, почему существует больше хэш-коллизий для KeyValuePair<uint, uint>, чем KeyValuePair<ushort, ushort>. Чтобы узнать об этом немного подробнее, я написал следующую короткую программу:

using System;
using System.Collections.Generic;

class Program
{
    const int Sample1 = 100;
    const int Sample2 = 213;

    public static void Main()
    {
        Display<uint, ushort>();
        Display<ushort, ushort>();
        Display<uint, uint>();
        Display<ushort, uint>();
    }

    static void Display<TKey, TValue>()
    {
        TKey key1 = (TKey) Convert.ChangeType(Sample1, typeof(TKey));
        TValue value1 = (TValue) Convert.ChangeType(Sample1, typeof(TValue));
        TKey key2 = (TKey) Convert.ChangeType(Sample2, typeof(TKey));
        TValue value2 = (TValue) Convert.ChangeType(Sample2, typeof(TValue));

        Console.WriteLine("Testing {0}, {1}", typeof(TKey).Name, typeof(TValue).Name);
        Console.WriteLine(new KeyValuePair<TKey, TValue>(key1, value1).GetHashCode());
        Console.WriteLine(new KeyValuePair<TKey, TValue>(key1, value2).GetHashCode());
        Console.WriteLine(new KeyValuePair<TKey, TValue>(key2, value1).GetHashCode());
        Console.WriteLine(new KeyValuePair<TKey, TValue>(key2, value2).GetHashCode());
        Console.WriteLine();
    }
}

Выход на моей машине:

Testing UInt32, UInt16
-1888265981
-1888265981
-1888265806
-1888265806

Testing UInt16, UInt16
-466800447
-459525951
-466800528
-459526032

Testing UInt32, UInt32
958334947
958334802
958334802
958334947

Testing UInt16, UInt32
-1913331935
-1913331935
-1913331935
-1913331935

Очевидно, вы можете попробовать изменить значения выборки, чтобы увидеть, где происходят столкновения.

Результаты KeyValuePair<ushort, uint> особенно тревожны, а результаты KeyValuePair<ushort, ushort> на удивление хороши.

На самом деле, KeyValuePair<ushort, uint> не просто плохой - это смехотворно плохо, насколько я вижу - мне не нужно находить никакого значения, которое не имеет того же хэш-кода -1913331935 при запуске 64-битного CLR. Запуск 32-битного CLR я получаю другой хеш-код, но все тот же хэш-код для всех значений.

Похоже, что в .NET 4.5 (это то, что я запускаю) реализация по умолчанию GetHashCode не просто принимает поле первого экземпляра структуры, как ранее документировано. Я подозреваю, что по крайней мере для некоторых типов он просто использует первые 4 байта памяти за заголовком в коробке (и там будет бокс для каждого вызова здесь), и это заканчивается иногда просто первым полем (если это поле является uint), иногда более чем одним полем (например, для ushort, ushort, где оба поля могут входить "внутри" 4 байта) и иногда вообще не являются полями (ushort, uint).

(На самом деле это не объясняет, почему вы получаете 1024 разных хеш-кода в случае uint, uint вместо 1000. Я все еще не уверен в этом.)

В конечном счете использование типа значения, которое не переопределяет GetHashCode в качестве словарного ключа, похоже, что это просто плохая идея, если вы не протестировали ее, чтобы убедиться, что она подходит для ваших конкретных требований. Там слишком много, что есть черная магия, чтобы быть уверенным в этом, ИМО.

Ответ 2

Так как GetHashCode возвращает Int32, каждая пара Int16 (или UInt16 s) может легко вернуть уникальное значение. С парой Int32 s вам нужно будет каким-то образом совместить значения, чтобы быть совместимыми с вашим дизайном.

KeyValuePair не переопределяет GetHashCode(), поэтому вы используете стандартную реализацию ValueType.GetHashCode(), а в документации для нее указано следующее:

(from: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx)

Если вы вызываете метод GetHashCode производного типа, возвращаемое значение вряд ли подходит для использования в качестве ключа в хеш-таблице. Кроме того, если значение одного или нескольких из этих изменений полей, возвращаемое значение может оказаться непригодным для использования в качестве ключа в хеш-таблица. В любом случае рассмотрите возможность написания собственной реализации GetHashCode метод, который более точно представляет концепцию хеш-кода для типа.

Так как KeyValuePair не переопределяет GetHashCode(), я предполагаю, что он не предназначен для использования в качестве ключа Dictionary.

Далее, в соответствии с этот вопрос и этот код С#, значение по умолчанию реализация ValueType.GetHashCode() просто выбирает первое нестатическое поле и возвращает результат его метода GetHashCode(). Это объясняет большое количество дубликатов для KeyValuePair<UInt32, UInt32>, хотя это не объясняет отсутствие дубликатов для KeyValuePair<UInt16, UInt16>.

Я предполагаю, что для KeyValuePair<UInt32, UInt32> GetHashCode() просто возвращает GetHashCode() первого значения, а для KeyValuePair<UInt16, UInt16>, GetHashCode() объединяет значения, приводящие к уникальному хешу для каждой пары значения, так как это возможно и прямолинейно.

Ответ 3

Как отмечали другие ответчики, KeyValuePair не переопределяет GetHashCode, а реализация по умолчанию GetHashCode для structs не самая лучшая. Вместо этого вы можете использовать двухэлементные кортежи, например

var dict = new Dictionary<Tuple<uint, uint>, string>();
dict.Add(Tuple.Create(1u, 2u),"xxx"); // Tuples override GetHashCode

Заметьте, однако это добавит дополнительные накладные расходы для дополнительного распределения кучи Tuple. (частично это компенсировалось, поскольку, когда вы вызываете GetHashCode в структуре, которая не переопределяет ее, вы неявно вставляете ее)

Ответ 4

Нижнее правило состоит в том, чтобы всегда переопределять GetHashCode, если вы хотите, чтобы много ваших собственных вещей, помещенных в a, использовало структуру, такую ​​как словарь. Вы можете использовать это расширение, чтобы узнать, насколько хорошо заполнен словарь. Он будет сообщать о пустых слотах, дублирующих ключах и т.д. Оставить его на sourceforge, но вот он:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

// This unit is Freeware. It was developed by Jerremy Koot & Ivo Tops. July 2011
//
// Version  By    Changes
// =======  ===== ==============================================================
// v1.02    Ivo   Removed not-working Hashtable support and simplified code
// v1.01    Ivo   Lowered memory usage
// v1.00    I&J   First Version

namespace FastLibrary
{
/// <summary>
/// Static Extension Methods for Dictionary, ConcurrentDictionary and HashSet
/// </summary>
public static class ExtHashContainers
{
    /// <summary>
    /// Checks a dictionary for performance statistics
    /// </summary>
    public static string Statistics<TKey, TValue>(this Dictionary<TKey, TValue> source)
    {
        return ExamineData(source.Keys, source);
    }

    /// <summary>
    /// Checks a concurrent dictionary for performance statistics
    /// </summary>
    public static string Statistics<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> source)
    {
        return ExamineData(source.Keys, source);
    }

    /// <summary>
    /// Checks a HashSet for performance statistics
    /// </summary>
    public static string Statistics<TKey>(this HashSet<TKey> source)
    {
        return ExamineData(source, source);
    }

    private static string ExamineData<TKey>(ICollection<TKey> source, Object hashContainer)
    {
        if (!source.Any()) return "No Data found.";

        // Find Buckets
        var b = GetBuckets(hashContainer);
        if (b < 0) return ("Unable to get Buckets Field for HashContainer");

        // Create our counting temp dictionaries
        var d = new int[b];
        var h = new Dictionary<int, int>(source.Count);

        // Find Hash Collisions and Bucket Stats
        foreach (var k in source)
        {
            var hash = k.GetHashCode() & 0x7FFFFFFF; // Hashes are stripped of sign bit in HashContainers
            int bucket = hash%b; // .NET Hashers do not use negative hashes, and use % voor bucket selection
            // Bucket Stats
            d[bucket]++;

            // Hashing Stats
            int c;
            if (h.TryGetValue(hash, out c)) h.Remove(hash);
            else c = 0;
            c++;
            h.Add(hash, c);
        }

        // Do some math
        var maxInBucket = d.Max(q => q);
        var maxSameHash = h.Values.Max(q => q);
        var emptyBuckets = d.Count(q => q == 0);
        var emptyStr = b == 0 ? "0" : ((float) (emptyBuckets)/b*100).ToString("0.0");
        var worstHash = (from i in h where i.Value == maxSameHash select i.Key).FirstOrDefault();

        // Report our findings
        var r = Environment.NewLine + hashContainer.GetType().Name + " has " + b + " buckets with " + source.Count +
                " items. " +
                Environment.NewLine + "The Largest bucket contains " + maxInBucket + " items. " +
                Environment.NewLine + "It has " + (emptyBuckets) +
                " empty buckets (" + emptyStr + "%)" + Environment.NewLine + "Each non-empty bucket has on average " +
                ((source.Count/(float) (b - emptyBuckets))).ToString("0.0") + " items." + "The " + source.Count +
                " items share " + h.Count +
                " unique hashes. ";
        if (maxSameHash > 1)
            r += Environment.NewLine + "The largest collision has " + maxSameHash +
                 " items sharing the same hash, which == " + worstHash;
        return r;
    }

    private static Int32 GetBuckets(object dictionary)
    {
        var type = dictionary.GetType();
        while (type != null && !type.IsGenericType) type = type.BaseType;
        if (type == null) return -1;

        string field = null;
        if (type.GetGenericTypeDefinition() == typeof (Dictionary<,>)) field = "buckets";
        if (type.GetGenericTypeDefinition() == typeof (ConcurrentDictionary<,>)) field = "m_buckets";
        if (type.GetGenericTypeDefinition() == typeof (HashSet<>)) field = "m_buckets";
        if (field == null) return -1;

        var bucketsField = type.GetField(field, BindingFlags.NonPublic | BindingFlags.Instance);
        if (bucketsField == null) return -1;

        var buckets = bucketsField.GetValue(dictionary);
        if (buckets == null) return -1;

        var length = buckets.GetType().GetProperty("Length");
        return (int) length.GetGetMethod().Invoke(buckets, null);
    }
}
}