Как сгенерировать хэш-код из массива байтов в C#?

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

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

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();

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
51
0
43 513
11

Ответы 11

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

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

Вы можете вызывать функции c из C# с помощью pinvoke. Он имеет некоторое влияние на производительность (например, закрепление и маршалинг переданных параметров - как зависит от фактического используемого типа), но им можно пренебречь, если не вызывать их слишком часто (что означает, например,> тысячи раз в цикле). Даже некоторые фреймворки для графического рендеринга (а именно OpenTK, SkiaSharp) используют много вызовов pinvoke, и производительность по-прежнему приличная.

Daniel Bişar 11.05.2020 13:55

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

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

  • Равны ли хэш-коды? Затем вызовите полный (медленный) метод Equals.
  • Не равны ли хэш-коды? Тогда эти два предмета точно не равны.

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

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

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

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

+1 Это было одно из самых ясных объяснений, которые я когда-либо слышал, почему выгодно переопределить Equals и GetHashcode.

Andrew Hare 04.05.2009 19:57

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

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

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

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

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

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

SHA1 медленнее, чем MD5. Если вас не беспокоит безопасность, используйте MD5.

Jonathan C Dickinson 22.01.2009 08:12

Спасибо, Джон .. Метод SHA1CryptoServiceProvider.ComputeHash сработал для меня .. !!

Deepak 18.12.2012 15:15

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

From Msdn:

Serves as a hash function for a particular type, suitable for use in hashing algorithms and data structures such as a hash table.

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

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

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

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

Заимствуя код, сгенерированный программой 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/20 000).

для IList<byte> определенно используйте цикл for на основе индексации, чем foreach. Возможно, это не большая разница для byte[], поскольку foreach будет преобразован в for внутренне.

nawfal 15.12.2013 09:08

Циклы foreach иногда компилируются в циклы for при переходе по списку, не уверен, может ли это также произойти при цикле по IList (который всегда должен быть немного медленнее, не имеет такого значения для больших массивов, но для маленьких => foreach имеет больше инициализаций, чем для).

Daniel Bişar 11.05.2020 13:56

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

Вот и все ... Модифицированный хеш FNV на C#

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;
        }
    }

Это даст довольно уникальные хэши, но на самом деле не будет работать для GetHashCode. Идея состоит в том, что хэш позволяет коллекции иметь быстрый метод проверки совпадения двух byte[] перед использованием более медленного Equals. В этой реализации вы зацикливаете весь массив, поэтому для очень больших массивов проверка равенства может быть намного быстрее. Это хороший способ вычислить хэш общего назначения, но из-за того, как .Net на самом деле использует GetHashCode, это может действительно замедлить сборку.

Keith 17.05.2012 17:06

@Keith: GetHashCode позволяет классам, использующим этот метод, получать целочисленное значение для объекта, чего Equals не предоставляет. С этим значением он может делать другие вещи, кроме простого сравнения (например, получать индекс корзины в хеш-таблице). Таким образом, зацикливание всего массива в GetHashCode может быть преимуществом, даже если то же самое сделано в Equals.

tigrou 21.08.2012 16:51

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

Keith 22.08.2012 01:29

@Keith - ты здесь не прав. Ключевым моментом является то, что GetHashCode () должен вызываться только один раз, а Equals () должен вызываться для каждого сравнения. Так что для вычисления хэша вполне нормально иметь более продолжительное время выполнения, чем равное. Фактически, встроенное хеширование строк .NET делает именно это.

kaalus 08.09.2012 02:02

@Keith: Каалус прав. Хороший хэш-код должен включать информацию обо всем хешируемом объекте, включая все значения свойств и полей. Невозможно избежать сканирования этой информации при каждом вызове, если только рассматриваемый объект не является неизменяемым и не кэширует хэш-код при создании.

Frank Hileman 15.03.2013 23:36

Стоит отметить, что связанная страница (здесь кешированная версия - archive.is/MnmRY) на самом деле использует uint, поэтому будет создавать разные хеши.

sclarke81 01.09.2015 11:02

@Keith в современном ООП мы рассматриваем объект как его внешнюю форму (или лицо, или контракт). Изменения во внутренней, не являющейся внешней спецификацией, не считаются "изменениями в объекте", т.е. изменение поля long cachedHash не считается изменением. Так что иногда мы можем кэшировать номер хеша в поле. Таким образом, хеш кэшируется в большинстве (неизменяемых) классов Java.

Jacek Cz 06.10.2015 18:17

Распределение хеширования здесь очень хорошее, наша тема обсуждает «как использовать этот код». Лично я знаю, что создал несколько плохих алгоритмов хеширования;)

Jacek Cz 06.10.2015 18:18

Я смотрел на алгоритм FNV .. Я не вижу такого же сдвига бит в коде c. Это украшение или я не смотрю правильный код FNV 1a?

T McKeown 26.08.2018 23:40

Какова цель битового сдвига? Спасибо.

masterwok 31.08.2019 04:24

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

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

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 секунд!

Не могли бы вы объяснить свой алгоритм хеширования

nicolay.anykienko 23.01.2018 04:17

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;
}

Другие вопросы по теме