Как создать хэш-код из массива байтов в С#?

Скажем, у меня есть объект, который хранит массив байтов, и я хочу иметь возможность эффективно генерировать хэш-код для него. Я использовал криптографические хеш-функции для этого в прошлом, потому что их легко реализовать, но они делают гораздо больше работы, чем должны быть криптографически в ожидании, и мне это неинтересно (я просто использую хэш-код в качестве ключа в хэш-таблицу).

Вот что у меня сегодня:

struct SomeData : IEquatable<SomeData>
{
    private readonly byte[] data;
    public SomeData(byte[] data)
    {
        if (null == data || data.Length <= 0)
        {
            throw new ArgumentException("data");
        }
        this.data = new byte[data.Length];
        Array.Copy(data, this.data, data.Length);
    }

    public override bool Equals(object obj)
    {
        return obj is SomeData && Equals((SomeData)obj);
    }

    public bool Equals(SomeData other)
    {
        if (other.data.Length != data.Length)
        {
            return false;
        }
        for (int i = 0; i < data.Length; ++i)
        {
            if (data[i] != other.data[i])
            {
                return false;
            }
        }
        return true;
    }
    public override int GetHashCode()
    {
        return BitConverter.ToInt32(new MD5CryptoServiceProvider().ComputeHash(data), 0);
    }
}

Любые мысли?


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

byte[] b1 = new byte[] { 1 };
byte[] b2 = new byte[] { 1 };
int h1 = b1.GetHashCode();
int h2 = b2.GetHashCode();

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

Ответ 1

Хэш-код объекта не обязательно должен быть уникальным.

Правило проверки:

  • Являются ли хэш-коды равными? Затем вызовите полный (медленный) метод Equals.
  • Являются ли хэш-коды не равными? Тогда два элемента, безусловно, не равны.

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

Как долго вы ожидаете, что данные будут? Как случайный? Если длины сильно различаются (скажем, для файлов), просто верните длину. Если длины, вероятно, будут одинаковыми, посмотрите на подмножество байтов, которое меняется.

GetHashCode должен быть намного быстрее, чем Equals, но не обязательно должен быть уникальным.

У двух одинаковых вещей никогда не должно быть разных хэш-кодов. Два разных объекта не должны иметь один и тот же хэш-код, но следует ожидать некоторых коллизий (в конце концов, есть больше перестановок, чем возможно 32-битных целых чисел).

Ответ 2

Не используйте криптографические хэши для хэш-таблицы, что смешно /overkill.

Здесь ya go... Измененный FNV Hash в С#

http://bretm.home.comcast.net/hash/6.html

    public static int ComputeHash(params byte[] data)
    {
        unchecked
        {
            const int p = 16777619;
            int hash = (int)2166136261;

            for (int i = 0; i < data.Length; i++)
                hash = (hash ^ data[i]) * p;

            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            return hash;
        }
    }

Ответ 3

Заимствуя код, созданный программным обеспечением JetBrains, я решил эту функцию:

    public override int GetHashCode()
    {
        unchecked
        {
            var result = 0;
            foreach (byte b in _key)
                result = (result*31) ^ b;
            return result;
        }
    }

Проблема с просто XOring байтами состоит в том, что 3/4 (3 байта) возвращаемого значения имеет только 2 возможных значения (все включено или все выключено). Это немного расширяет бит вокруг.

Установка точки останова в Equals была хорошим предложением. Добавив около 200 000 записей моих данных в словарь, вы увидите около 10 вызовов Equals (или 1/20000).

Ответ 4

По сравнению с методом SHA1CryptoServiceProvider.ComputeHash? Он принимает байтовый массив и возвращает хэш SHA1, и я считаю, что он довольно хорошо оптимизирован. Я использовал его в Identicon Handler, который довольно хорошо выполнялся при загрузке.

Ответ 5

Я нашел интересные результаты:

У меня есть класс:

public class MyHash : IEquatable<MyHash>
{        
    public byte[] Val { get; private set; }

    public MyHash(byte[] val)
    {
        Val = val;
    }

    /// <summary>
    /// Test if this Class is equal to another class
    /// </summary>
    /// <param name="other"></param>
    /// <returns></returns>
    public bool Equals(MyHash other)
    {
        if (other.Val.Length == this.Val.Length)
        {
            for (var i = 0; i < this.Val.Length; i++)
            {
                if (other.Val[i] != this.Val[i])
                {
                    return false;
                }
            }

            return true;
        }
        else
        {
            return false;
        }            
    }

    public override int GetHashCode()
    {            
        var str = Convert.ToBase64String(Val);
        return str.GetHashCode();          
    }
}

Затем я создал словарь с ключами типа MyHash, чтобы проверить, как быстро я могу вставить, и я также могу знать, сколько коллизий существует. Я сделал следующее

        // dictionary we use to check for collisions
        Dictionary<MyHash, bool> checkForDuplicatesDic = new Dictionary<MyHash, bool>();

        // used to generate random arrays
        Random rand = new Random();



        var now = DateTime.Now;

        for (var j = 0; j < 100; j++)
        {
            for (var i = 0; i < 5000; i++)
            {
                // create new array and populate it with random bytes
                byte[] randBytes = new byte[byte.MaxValue];
                rand.NextBytes(randBytes);

                MyHash h = new MyHash(randBytes);

                if (checkForDuplicatesDic.ContainsKey(h))
                {
                    Console.WriteLine("Duplicate");
                }
                else
                {
                    checkForDuplicatesDic[h] = true;
                }
            }
            Console.WriteLine(j);
            checkForDuplicatesDic.Clear(); // clear dictionary every 5000 iterations
        }

        var elapsed = DateTime.Now - now;

        Console.Read();

Каждый раз, когда я вставляю новый словарь в словарь, словарь вычисляет хэш этого объекта. Таким образом, вы можете сказать, какой метод наиболее эффективен, поставив несколько ответов, найденных здесь в методе public override int GetHashCode(). Метод, который был самым быстрым и имел наименьшее количество столкновений:

    public override int GetHashCode()
    {            
        var str = Convert.ToBase64String(Val);
        return str.GetHashCode();          
    }

для выполнения которого потребовалось 2 секунды. Метод

    public override int GetHashCode()
    {
        // 7.1 seconds
        unchecked
        {
            const int p = 16777619;
            int hash = (int)2166136261;

            for (int i = 0; i < Val.Length; i++)
                hash = (hash ^ Val[i]) * p;

            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            return hash;
        }
    }

также не имел коллизий, но потребовалось 7 секунд!

Ответ 6

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

Я вообще не знаю С#, и я не знаю, может ли он связываться с C, но здесь его реализация в C.

Ответ 7

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

Ответ 8

Создание хорошего хэша проще сказать, чем сделать. Помните, что вы в основном представляете n байтов данных с m бит информации. Чем больше ваш набор данных и чем меньше m, тем более вероятным будет столкновение... две части данных, разрешающие один и тот же хэш.

Самый простой хеш, который я когда-либо узнал, - это просто XORing все байты вместе. Это легко, быстрее, чем самые сложные алгоритмы хеширования, и наполовину приемлемый универсальный хэш-алгоритм для небольших наборов данных. На самом деле это алгоритм Bubble Sort of hash. Поскольку простая реализация оставила бы вас с 8 бит, то только 256 хешей... не так жарко. Вы могли бы использовать фрагменты XOR вместо отдельных байтов, но тогда алгоритм становится намного сложнее.

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

Итак, пока я не вижу причины не использовать консервированный алгоритм хэширования (производительность, возможно?), мне придется рекомендовать вам придерживаться того, что у вас есть.

Ответ 9

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

Быстрый способ увидеть, как ваша хеш-функция выполняет ваши данные, - это добавить все данные в хэш-таблицу и подсчитать количество раз, когда вызывается функция Equals, если слишком часто у вас больше работы над функцией, Если вы это сделаете, просто имейте в виду, что размер хэш-таблицы должен быть больше, чем ваш набор данных, когда вы начнете, в противном случае вы собираетесь перефразировать данные, которые будут вызывать повторные вставки и другие оценки Equals (хотя, возможно, более реалистично?)

Для некоторых объектов (а не для этого) быстрый код HashCode может быть сгенерирован ToString(). GetHashCode(), конечно, не является оптимальным, но полезным, поскольку люди склонны возвращать что-то близкое к идентичности объекта из ToString() и это именно то, что ищет GetHashcode

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

Ответ 10

private int? hashCode;

public override int GetHashCode()
{
    if (!hashCode.HasValue)
    {
        var hash = 0;
        for (var i = 0; i < bytes.Length; i++)
        {
            hash = (hash << 4) + bytes[i];
        }
        hashCode = hash;
    }
    return hashCode.Value;
}

Ответ 11

Если кто-то любит бегло:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp
{
    class Program
    {
        static void Main(string[] args) =>
            new List<byte[]>
            {
                new byte[] { 1, 2, 3, 4, 5, 6, 7 },
                new byte[] { 7, 6, 5, 4, 3, 2, 1 },
                new byte[] { 1, 2, 3, 4, 5, 6, 7 },
                new byte[] { 2, 3, 4, 5, 6, 7, 8 },
                new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }
            }
            .ForEach(Handle);

        static void Handle(IEnumerable<byte> data) =>
            Console.WriteLine($"bytes: {string.Join(", ", data)}{Environment.NewLine}hash: {GetHashCode(data)}");

        static int GetHashCode(IEnumerable<byte> data) =>
            new[] { 17 }.Concat(data.Select(b => b.GetHashCode())).Aggregate((i, j) => unchecked(23 * i + j));
    }
}

Выход:

bytes: 1, 2, 3, 4, 5, 6, 7
hash: -2085709989
bytes: 7, 6, 5, 4, 3, 2, 1
hash: -1171190757
bytes: 1, 2, 3, 4, 5, 6, 7
hash: -2085709989
bytes: 2, 3, 4, 5, 6, 7, 8
hash: -1930945196
bytes: 1, 2, 3, 4, 5, 6, 7, 8
hash: -726689483

Обратите внимание, что последовательно равные массивы имеют одинаковый хэш-код.

Ответ 12

RuntimeHelpers.GetHashCode может помочь:

Из Msdn:

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