Runtime: Предложение: добавьте System.HashCode, чтобы упростить создание хороших хэш-кодов.

Созданный на 9 дек. 2016  ·  182Комментарии  ·  Источник: dotnet/runtime

Обновление от 16.06.17: Ищем добровольцев

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

Обновление 13.06.17: Предложение принято!

Вот API, одобренный @terrajobst по адресу https://github.com/dotnet/corefx/issues/14354#issuecomment -308190321:

// Will live in the core assembly
// .NET Framework : mscorlib
// .NET Core      : System.Runtime / System.Private.CoreLib
namespace System
{
    public struct HashCode
    {
        public static int Combine<T1>(T1 value1);
        public static int Combine<T1, T2>(T1 value1, T2 value2);
        public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3);
        public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4);
        public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5);
        public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8);

        public void Add<T>(T value);
        public void Add<T>(T value, IEqualityComparer<T> comparer);

        [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
        [EditorBrowsable(Never)]
        public override int GetHashCode();

        public int ToHashCode();
    }
}

Исходный текст этого предложения следует.

Обоснование

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

class Person
{
    public override int GetHashCode() => FirstName.GetHashCode() + LastName.GetHashCode();
}

Предложение

Мы должны добавить тип HashCode чтобы ограничить создание хэш-кода и избежать принуждения разработчиков к путанице в деталях. Вот мое предложение, основанное на https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329, с несколькими незначительными изменениями.

// Will live in the core assembly
// .NET Framework : mscorlib
// .NET Core      : System.Runtime / System.Private.CoreLib
namespace System
{
    public struct HashCode
    {
        public static int Combine<T1>(T1 value1);
        public static int Combine<T1, T2>(T1 value1, T2 value2);
        public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3);
        public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4);
        public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5);
        public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8);

        public void Add<T>(T value);
        public void Add<T>(T value, IEqualityComparer<T> comparer);
        public void AddRange<T>(T[] values);
        public void AddRange<T>(T[] values, int index, int count);
        public void AddRange<T>(T[] values, int index, int count, IEqualityComparer<T> comparer);

        [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
        public override int GetHashCode();

        public int ToHashCode();
    }
}

Замечания

См @terrajobst «s комментарий в https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329 для целей данного API; все его замечания верны. Однако я хотел бы особо отметить эти:

  • API не нужно создавать надежный криптографический хеш.
  • API предоставит хэш-код, но не гарантирует конкретный алгоритм хеш-кода. Это позволяет нам использовать другой алгоритм позже или использовать другие алгоритмы на разных архитектурах.
  • API гарантирует, что в рамках данного процесса одни и те же значения дадут один и тот же хэш-код. Различные экземпляры одного и того же приложения, вероятно, будут создавать разные хэш-коды из-за рандомизации. Это позволяет нам гарантировать, что потребители не смогут сохранять хеш-значения и случайно полагаться на их стабильность во время выполнения (или, что еще хуже, версий платформы).
api-approved area-System.Numerics up-for-grabs

Самый полезный комментарий

Решения

  • Мы должны удалить все методы AddRange потому что сценарий неясен. Маловероятно, что массивы будут появляться очень часто. И когда задействованы более крупные массивы, возникает вопрос, следует ли кэшировать вычисления. Увидев цикл for на вызывающей стороне, становится ясно, что вам нужно подумать об этом.
  • Мы также не хотим добавлять IEnumerable перегрузки в AddRange потому что они будут выделены.
  • Мы не думаем, что нам нужна перегрузка для Add которая принимает string и StringComparison . Да, это, вероятно, более эффективно, чем вызов через IEqualityComparer , но мы можем исправить это позже.
  • Мы думаем, что отметка GetHashCode как устаревшая из-за ошибки - это хорошая идея, но мы пошли бы еще дальше и также спрячемся от IntelliSense.

Это оставляет нам:

`` С #
// Будет жить в основной сборке
// .NET Framework: mscorlib
// Ядро .NET: System.Runtime / System.Private.CoreLib
пространство имен System
{
общедоступная структура HashCode
{
public static int Combine(Значение T11);
public static int Combine(Значение T11, значение T22);
public static int Combine(Значение T11, значение T22, значение T33);
public static int Combine(Значение T11, значение T22, значение T33, значение T44);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66, значение T77);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66, значение T77, значение T88);

    public void Add<T>(T value);
    public void Add<T>(T value, IEqualityComparer<T> comparer);

    [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
    [EditorBrowsable(Never)]
    public override int GetHashCode();

    public int ToHashCode();
}

}
`` ''

Все 182 Комментарий

Предложение: добавить поддержку рандомизации хэша

public static HashCode Randomized<T> { get; } // or CreateRandomized<T>
or 
public static HashCode Randomized(Type type); // or CreateRandomized(Type type)

T или Type type необходимы для получения одного и того же рандомизированного хеша для того же типа.

Предложение: добавить поддержку коллекций

public HashCode Combine<T>(T[] values);
public HashCode Combine<T>(T[] values, IEqualityComparer<T> comparer);
public HashCode Combine<T>(Span<T> values);
public HashCode Combine<T>(Span<T> values, IEqualityComparer<T> comparer);
public HashCode Combine<T>(IEnumerable<T> values);
public HashCode Combine<T>(IEnumerable<T> IEqualityComparer<T> comparer);

Я думаю, что нет необходимости в перегрузках Combine(_field1, _field2, _field3, _field4, _field5) потому что следующий код HashCode.Empty.Combine(_field1).Combine(_field2).Combine(_field3).Combine(_field4).Combine(_field5); должен быть встроен в оптимизацию без вызовов Combine.

@AlexRadch

Предложение: добавить поддержку коллекций

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

Он хотел использовать другой алгоритм, например хеш Marvin32, который используется для строк в coreclr. Для этого потребуется увеличить размер HashCode до 8 байтов.

А как насчет типов Hash32 и Hash64, которые будут хранить внутри 4 или 8 байтов данных? Задокументируйте плюсы и минусы каждого из них. Hash64 хорош для X, но потенциально медленнее. Hash32 быстрее, но потенциально не так распределен (или что-то в этом роде на самом деле).

Он хотел рандомизировать начальное число хэша, чтобы хэши не были детерминированными.

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

Примечание: Roslyn была бы рада, если бы это можно было указать в Fx. Мы добавляем функцию выдачи пользователю GetHashCode. В настоящее время он генерирует такой код:

c# public override int GetHashCode() { var hashCode = -1923861349; hashCode = hashCode * -1521134295 + this.b.GetHashCode(); hashCode = hashCode * -1521134295 + this.i.GetHashCode(); hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.s); return hashCode; }

Это не лучший опыт, и он раскрывает множество уродливых концепций. Мы были бы счастливы получить Hash.With API, который мы могли бы использовать вместо этого.

Спасибо!

Что насчет MurmurHash? Он достаточно быстрый и имеет очень хорошие хеширующие свойства. Также есть две разные реализации: одна выдает 32-битные хэши, а другая - 128-битные.

Существуют также векторизованные реализации как для 32-битного, так и для 128-битного форматов.

@tannergooding MurmurHash работает быстро, но небезопасно, судя по звукам этого сообщения в блоге .

@jkotas , проводилась ли в JIT какая-либо работа по @CyrusNajmabadi :

А как насчет типов Hash32 и Hash64, которые будут хранить внутри 4 или 8 байтов данных? Задокументируйте плюсы и минусы каждого из них. Hash64 хорош для X, но потенциально медленнее. Hash32 быстрее, но потенциально не так распределен (или что-то в этом роде на самом деле).

Я все еще думаю, что этот тип был бы очень ценным, чтобы предложить разработчикам, и было бы здорово иметь его в 2.0.

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

Также эта статья относится к Murmur2. Проблема решена в алгоритме Murmur3.

JIT для создания более качественного кода для> 4-байтовых структур на 32-разрядных системах с момента наших прошлогодних обсуждений

Я ничего не знаю.

что вы думаете о предложении @CyrusNajmabadi

Типы фреймворков должны быть простыми, подходящими для более 95% случаев. Возможно, они не самые быстрые, но это нормально. Выбор между Hash32 и Hash64 - непростой выбор.

Я не против. Но можем ли мы, по крайней мере, найти достаточно хорошее решение для этих 95% случаев? Прямо сейчас ничего нет ...: - /

hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode (this.s);

@CyrusNajmabadi Почему вы вызываете EqualityComparer здесь, а не только this.s.GetHashCode ()?

Для неструктур: чтобы нам не нужно было проверять значение null.

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

Вызов EqualityComparer.Default.GetHashCode примерно в 10 раз дороже, чем проверка на null ....

Вызов EqualityComparer.Default.GetHashCode примерно в 10 раз дороже, чем проверка на null.

Похоже на проблему. если бы только был хороший API хеш-кода, который мы могли бы вызвать в Fx, на который я мог бы отложить :)

(Кроме того, у нас есть эта проблема в наших анонимных типах, поскольку это то, что мы также генерируем).

Не уверен, что мы делаем с кортежами, но я предполагаю, что это похоже.

Не уверен, что мы делаем с кортежами, но я предполагаю, что это похоже.

System.Tuple проходит через EqualityComparer<Object>.Default по историческим причинам. System.ValueTuple вызывает Object.GetHashCode с нулевой проверкой - https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/ValueTuple.cs#L809.

О нет. Похоже, кортеж может просто использовать HashHelpers. Может ли это быть раскрыто, чтобы пользователи могли получить такую ​​же выгоду?

Большой. Я счастлив сделать что-то подобное. Я начал с наших анонимных типов, потому что полагал, что это разумные передовые методы. Если нет, то ничего страшного. :)

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

Какая форма API, по вашему мнению, лучше всего подходит для кода, сгенерированного компилятором?

Меня бы устроило буквально любое из представленных ранее 32-битных решений. Черт, 64-битные решения меня устраивают. Просто какой-то API, который вы можете получить, который говорит: «Я могу комбинировать хэши каким-то разумным образом и выдавать разумно распределенный результат».

Я не могу согласовать эти утверждения:

У нас была неизменная структура HashCode размером 4 байта. У него был метод Combine (int), который смешивал предоставленный хэш-код с собственным хеш-кодом с помощью алгоритма, подобного DJBX33X, и возвращал новый HashCode.

@jkotas не считал алгоритм, подобный DJBX33X, достаточно надежным.

А также

Типы фреймворков должны быть простыми, подходящими для более 95% случаев.

Разве мы не можем придумать простой 32-битный накопительный хеш, который достаточно хорошо работает в 95% случаев? Какие случаи здесь не обрабатываются должным образом, и почему мы думаем, что они относятся к 95% случаев?

@jkotas , действительно ли производительность так важна для этого типа? Я думаю , что в среднем таких вещей , как в hashTable поиски и это заняло бы намного больше времени , чем несколько STRUCT копий. Если это окажется узким местом, было бы разумно попросить команду JIT оптимизировать 32-битные копии структур после выпуска API, чтобы у них был какой-то стимул, вместо того, чтобы блокировать этот API, когда никто не работает над оптимизацией копии?

Разве мы не можем придумать простой 32-битный накопительный хеш, который достаточно хорошо работает в 95% случаев?

По умолчанию 32-битный накапливающий хеш для строк был сильно сожжен, и поэтому хеш Марвина для строк в .NET Core - https://github.com/dotnet/corert/blob/87e58839d6629b5f90777f886a2f52d7a99c076f/src/System.Private.CoreLib src / System / Marvin.cs # L25. Я не думаю, что мы хотим повторить здесь ту же ошибку.

@jkotas , действительно ли производительность так важна для этого типа?

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

Мы очень сильно сгорели по умолчанию 32-битный накопительный хеш для строки

Это не похоже на 95% случай. Мы говорим о нормальных разработчиках, которым просто нужен «достаточно хороший» хеш для всех тех типов, в которых они сегодня делают что-то вручную.

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

Это не для использования компилятором Roslyn. Это используется IDE Roslyn, когда мы помогаем пользователям генерировать GetHashCodes для их типов. Это код, который пользователь увидит и должен будет поддерживать, и имеет что-то разумное, например:

`` С #
вернуть Hash.Combine (this.A? .GetHashCode () ?? 0,
this.B? .GetHashCode () ?? 0,
this.C? .GetHashCode () ?? 0);

is a lot nicer than a user seeing and having to maintain:

```c#
            var hashCode = -1923861349;
            hashCode = hashCode * -1521134295 + this.b.GetHashCode();
            hashCode = hashCode * -1521134295 + this.i.GetHashCode();
            hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.s);
            return hashCode;

Я имею в виду, что у нас уже есть этот код в Fx:

https://github.com/dotnet/roslyn/blob/master/src/Compilers/Test/Resources/Core/NetFX/ValueTuple/ValueTuple.cs#L5

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

Примечание: мы даже думали сделать это в roslyn:

c# return (this.A, this.B, this.C).GetHashCode();

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

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

Исходный строковый хеш был «достаточно хорошим» хешем, который хорошо работал для обычных разработчиков. Но затем было обнаружено, что веб-серверы ASP.NET уязвимы для DoS-атак, поскольку они, как правило, хранят полученные данные в хэш-таблицах. Таким образом, «достаточно хороший» хеш в основном превратился в плохую проблему безопасности.

Мы думаем, что этого достаточно для кортежей

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

     return Hash.Combine(this.A?.GetHashCode() ?? 0,
                         this.B?.GetHashCode() ?? 0,
                         this.C?.GetHashCode() ?? 0);

Мне это кажется разумным.

Я не понимаю твою позицию. Кажется, вы говорите две вещи:

Исходный строковый хеш был «достаточно хорошим» хешем, который хорошо работал для обычных разработчиков. Но затем было обнаружено, что веб-серверы ASP.NET уязвимы для DoS-атак, поскольку они, как правило, хранят полученные данные в хэш-таблицах. Таким образом, «достаточно хороший» хеш в основном превратился в плохую проблему безопасности.

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

Типы фреймворков должны быть простыми, подходящими для более 95% случаев.

Хорошо, если это так, давайте предоставим хэш-код, который подходит для 95% случаев. Люди, у которых есть проблемы с безопасностью / DoS, могут использовать специальные формы, задокументированные для этой цели.

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

Ok. Можем ли мы раскрыть это, чтобы пользователи могли использовать тот же механизм.

-
Мне здесь очень тяжело, потому что это звучит так, как будто мы говорим: «Потому что мы не можем сделать универсальное решение, каждый должен выбирать свое». Это кажется одним из худших мест, где можно побывать. Потому что, конечно, большинство наших клиентов не думают о том, чтобы использовать свой собственный «хэш Марвина» для защиты от DoS-атак. Они просто добавляют, отслеживают или иным образом плохо объединяют хэши полей в один окончательный хеш.

Если нас интересует 95% -ный случай, мы должны просто сделать в целом хороший enogh-хеш. ЕСЛИ нас волнует случай 5%, мы можем предоставить для этого специализированное решение.

Мне это кажется разумным.

Отлично :) Можем ли мы тогда выставить:

`` С #
пространство имен System.Numerics.Hashing
{
внутренний статический класс HashHelpers
{
общедоступные статические только для чтения int RandomSeed = new Random (). Next (Int32.MinValue, Int32.MaxValue);

    public static int Combine(int h1, int h2)
    {
        // RyuJIT optimizes this to use the ROL instruction
        // Related GitHub pull request: dotnet/coreclr#1830
        uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
        return ((int)rol5 + h1) ^ h2;
    }
}
Roslyn could then generate:

```c#
     return Hash.Combine(Hash.RandomSeed,
                         this.A?.GetHashCode() ?? 0,
                         this.B?.GetHashCode() ?? 0,
                         this.C?.GetHashCode() ?? 0);

Это будет действительно «достаточно хорошим» для подавляющего большинства случаев, а также приведет людей к правильному пути инициализации со случайными значениями, чтобы они не зависели от неслучайных хешей.

Люди, у которых есть проблемы с безопасностью / DoS, могут использовать специальные формы, задокументированные для этой цели.

Каждое приложение ASP.NET имеет проблемы с безопасностью / DoS.

Отлично :) Можем ли мы тогда выставить:

Это отличается от того, что я сказал разумно.

Что вы думаете о https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs . Это то, что сегодня используется внутри ASP.NET во многих местах, и это то, чем я был бы очень доволен (за исключением того, что функция комбинирования должна быть более сильной - детали реализации, которые мы можем продолжать настраивать).

@jkotas Я слышал это: p

Итак, проблема здесь в том, что разработчики не знают, когда они уязвимы для DoS-атак, потому что они не думают об этом, поэтому мы переключили строки на использование Marvin32.

Мы не должны идти по пути, говоря «95% случаев не имеют значения», потому что у нас нет возможности доказать это, и мы должны проявлять осторожность, даже если это сказывается на производительности. Если вы собираетесь отойти от этого, тогда реализация хэш-кода требует проверки Crypto Board, а не только нас, решающих: «Выглядит достаточно хорошо».

Каждое приложение ASP.NET имеет проблемы с безопасностью / DoS.

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

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

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

Каждое приложение ASP.NET имеет проблемы с безопасностью / DoS.

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

Итак, как это применимо к остальным типам, которые не используются в этом сценарии?

У нас есть такие наборы типов:

  1. Типы пользователей, которые должны быть защищены от DoS-атак. Прямо сейчас мы не предлагаем ничего, чтобы помочь, так что мы уже в плохом положении, поскольку люди, вероятно, делают что-то неправильно.
  2. Типы пользователей, которым не требуется защита от DoS-атак. Прямо сейчас мы не предлагаем ничего, чтобы помочь, так что мы уже в плохом положении, поскольку люди, вероятно, делают что-то неправильно.
  3. Типы фреймворков, которые должны быть безопасными для DoS-атак. Прямо сейчас мы сделали их безопасными для DoS-атак, но через API мы их не раскрываем.
  4. Типы фреймворков, которые не должны быть безопасными для DoS-атак. Прямо сейчас мы предоставили им хеши, но через API мы их не раскрываем.

По сути, мы думаем, что эти случаи важны, но недостаточно важны, чтобы на самом деле предоставить пользователям решение для обработки «1» или «2». Поскольку мы обеспокоены тем, что решение для «2» не подойдет для «1», мы даже не будем его предоставлять. И если мы не готовы даже предоставить решение для «1», кажется, что мы находимся в невероятно странном положении. Мы беспокоимся о DoSing и ASP, но не беспокоимся о том, чтобы реально помочь людям. И поскольку мы не будем помогать людям в этом, мы даже не готовы помогать в случаях, не связанных с DoS.

-

Если эти два случая важны (с чем я готов согласиться), то почему бы просто не предоставить два API? Задокументируйте их. Дайте им понять, для чего они нужны. Если люди используют их правильно, отлично . Если люди не используют их должным образом, это все еще нормально. В конце концов, они, скорее всего, сегодня все равно не работают должным образом, так как дела обстоят хуже?

О чем вы думаете

У меня нет того или иного мнения. Если это API, который клиенты могут использовать, который работает приемлемо и который предоставляет простой API с понятным кодом на своей стороне, то я думаю, что это нормально.

Я думаю, было бы неплохо иметь простую статическую форму, которая обрабатывает 99% случаев желания объединить набор полей / свойств упорядоченным образом. Вроде бы такую ​​штуку можно было бы довольно просто добавить к этому типу.

Я думаю, было бы неплохо иметь простую статическую форму

Согласен.

Я думаю, было бы неплохо иметь простую статическую форму, которая обрабатывает 99% случаев желания объединить набор полей / свойств упорядоченным образом. Вроде бы такую ​​штуку можно было бы довольно просто добавить к этому типу.

Согласен.

Я готов встретиться с вами на полпути в этом вопросе, потому что я действительно хочу увидеть, как будет реализован какой-то API. @jkotas Я до сих пор не понимаю, что вы против добавления неизменяемого API на основе экземпляров; сначала вы сказали, что это потому, что 32-битные копии будут медленными, затем потому, что изменяемый API будет более кратким (что неверно; h.Combine(a).Combine(b) (неизменяемая версия) короче, чем h.Combine(a); h.Combine(b); (изменяемая версия)).

Тем не менее, я готов вернуться к:

public static class HashCode
{
    public static int Combine<T>(T value1, Tvalue2);
    public static int Combine<T>(T value1, Tvalue2, IEqualityComparer<T> comparer);
    public static int Combine<T>(T value1, Tvalue2, T value3);
    public static int Combine<T>(T value1, Tvalue2, T value3, IEqualityComparer<T> comparer);
    public static int Combine<T>(T value1, Tvalue2, T value3, T value4);
    public static int Combine<T>(T value1, Tvalue2, T value3, T value4, IEqualityComparer<T> comparer);
    // ... All the way until value8
}

Это кажется разумным?

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

Если эти два случая важны (с чем я готов согласиться), то почему бы просто не предоставить два API? Задокументируйте их. Дайте им понять, для чего они нужны. Если люди используют их правильно, отлично. Если люди не используют их должным образом, это все еще нормально. В конце концов, они, скорее всего, сегодня все равно не работают должным образом, так как же дела обстоят хуже?

Потому что люди не используют вещи должным образом, когда они есть. Возьмем простой пример XSS. С самого начала даже веб-формы имели возможность кодировать выходные данные HTML. Однако разработчики не знали о риске, не знали, как это сделать должным образом, и узнали только когда было слишком поздно, их приложение было опубликовано, и, к сожалению, теперь их cookie аутентификации был отменен.

Предоставление людям возможности выбора безопасности предполагает, что они

  1. Узнай о проблеме.
  2. Поймите, каковы риски.
  3. Могу оценить эти риски.
  4. Может легко найти то, что нужно делать.

Эти предположения обычно не верны для большинства разработчиков, они узнают о проблеме только тогда, когда становится слишком поздно. Разработчики не ходят на конференции по безопасности, не читают официальные документы и не понимают решений. Итак, в сценарии ASP.NET HashDoS мы сделали выбор за них, мы защищали их по умолчанию, потому что это было правильным поступком и имело наибольшее влияние. Однако мы применили его только к строкам, и это оставило людей, которые создавали собственные классы на основе пользовательского ввода, в плохом месте. Мы должны поступить правильно и помочь защитить этих клиентов прямо сейчас и сделать это по умолчанию, имея успех, а не провал. Дизайн API для обеспечения безопасности иногда связан не с выбором, а с тем, чтобы помочь пользователю, независимо от того, знают он об этом или нет.

Пользователь всегда может создать хэш, не ориентированный на безопасность; так что с учетом двух вариантов

  1. Утилита хеширования по умолчанию не связана с безопасностью; пользователь может создать хэш-функцию с учетом безопасности
  2. Утилита хеширования по умолчанию осведомлена о безопасности; пользователь может создать настраиваемую хеш-функцию, не связанную с безопасностью

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

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

Я хотел бы предложить несколько вариантов решения проблемы «лучшего алгоритма»:

  1. Явный выбор: я планирую в ближайшее время разослать предложение API для набора некриптографических хэшей (например, xxHash, Marvin32 и SpookyHash). Такой API используется несколько иначе, чем типы HashCode или HashCodeHelper, но для обсуждения предположим, что мы сможем устранить эти различия. Если мы используем этот API для GetHashCode:

    • Сгенерированный код четко описывает, что он делает - если Roslyn генерирует Marvin32.Create(); , это позволяет опытным пользователям узнать, что он решил сделать, и они могут легко изменить его на другой алгоритм в наборе, если захотят.

    • Это означает, что нам не нужно беспокоиться о нарушении изменений. Если мы начнем с алгоритма без рандомизации / плохой энтропии / медленности, мы можем просто обновить Roslyn, чтобы начать генерировать что-то еще в новом коде. Старый код будет продолжать использовать старый хеш, а новый код будет использовать новый хеш. Разработчики (или исправление кода Roslyn) могут изменить старый код, если захотят.

    • Самый большой недостаток, о котором я могу думать, заключается в том, что некоторые из оптимизаций, которые мы могли бы пожелать для GetHashCode, могут быть вредными для других алгоритмов. Например, в то время как 32-битное внутреннее состояние прекрасно работает с неизменяемыми структурами, 256-битное внутреннее состояние (скажем) в CityHash может тратить кучу времени на копирование.

  1. Рандомизация: начните с правильно рандомизированного алгоритма (код @CyrusNajmabadi, показанный со случайным начальным значением, не учитывается, поскольку, вероятно, можно смыть случайность). Это гарантирует, что мы можем изменить реализацию без проблем с совместимостью. Нам все равно придется очень внимательно относиться к изменениям производительности, если мы изменим алгоритм. Однако это также было бы потенциальным преимуществом, поскольку мы могли бы делать выбор для каждой архитектуры (или даже для каждого устройства). Например, этот сайт показывает, что xxHash является самым быстрым на Mac x64, а SpookyHash - самым быстрым на Xbox и iPhone. Если мы пойдем по этому пути с намерением в какой-то момент изменить алгоритмы, нам, возможно, придется подумать о разработке API, который по-прежнему будет иметь разумную производительность при наличии 64-битного внутреннего состояния.

CC @bartonjs , @terrajobst

@morganbr Не существует единого идеального алгоритма, но я думаю, что наличие некоторого алгоритма, который довольно хорошо работает большую часть времени, представленного с использованием простого, легкого для понимания API, - это наиболее полезная вещь, которую можно сделать. Наличие набора алгоритмов в дополнение к этому для расширенного использования - это нормально. Но это не должен быть единственный вариант, мне не нужно узнавать, кто такой Марвин, просто чтобы я мог поместить свои объекты в Dictionary .

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

Мне нравится, как вы это выразили. Мне также нравится, что вы упомянули сам словарь. IDictionary - это то, что может иметь множество различных имплицитов с самыми разными качествами (см. API-интерфейсы коллекций на многих платформах). Тем не менее, мы по-прежнему просто предоставляем базовый «Словарь», который в целом выполняет достойную работу, даже если он может отличаться не во всех категориях.

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

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

Другими словами, люди, пишущие хэш-коды сегодня, не будут знать или заботиться о том, зачем им Spooky vs Marvin vs Murmur. Только тот, у кого есть особая потребность в одном из этих хэш-кодов, пойдет искать. Но многим людям нужно сказать: «вот состояние моего объекта, дайте мне способ создать хорошо распределенный хэш, который быстро, который я затем могу использовать со словарями, и который, как я полагаю, не позволяет мне попасть в DOS, если я случится. принимать ненадежный ввод, хэшировать и хранить ".

@CyrusNajmabadi Проблема в том, что если мы расширим наши нынешние представления о совместимости на будущее, мы обнаружим, что, как только этот тип будет выпущен, он уже не может измениться (если только мы не обнаружим, что алгоритм ужасно нарушен, "он делает все приложения уязвимыми") ).

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

Согласно предположению Моргана, код, который вы пишете сегодня, всегда будет иметь одни и те же характеристики производительности. Это прискорбно для приложений, которые могли стать лучше. Для приложений, которые стали бы хуже, это фантастика. Но когда мы находим новый алгоритм, мы проверяем его и меняем Roslyn (и предлагаем изменение в ReSharper / и т. Д.), Чтобы начать генерировать вещи с помощью NewAwesomeThing2019 вместо SomeThingThatWasConsideredAwesomeIn2018.

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

Итак, конечно, вы можете не знать, почему Roslyn / ReSharper / etc автоматически написал GetHashCode для вас, используя Marvin32, или Murmur, или FastHash, или комбинацию / условное обозначение на основе IntPtr.Size. Но у вас есть возможность разобраться в этом. И у вас есть возможность изменить его в ваших типах позже, по мере появления новой информации ... но мы также дали вам возможность сохранить его неизменным. (Было бы грустно, если бы мы напишем это, и через 3 года Roslyn / ReSharper / и т. Д. Будут явно избегать его вызова, потому что новый алгоритм намного лучше ... Обычно).

@bartonjs Чем отличается хеширование от всех тех мест, где .Net предоставляет вам алгоритм или структуру данных черного ящика? Например, сортировка (внутренняя сортировка), Dictionary (отдельная цепочка на основе массива), StringBuilder (связанный список из 8 тыс. Фрагментов), большая часть LINQ.

Сегодня мы более подробно рассмотрели это. Приносим извинения за задержку и разговоры по этому вопросу.

Требования

  • Для кого предназначен API?

    • API не нужно создавать надежный криптографический хеш.

    • Но: API должен быть достаточно хорошим, чтобы мы могли использовать его в самой структуре (например, в BCL и ASP.NET).

    • Однако это не означает, что мы должны использовать API везде. Ничего страшного, если есть части FX, в которых мы хотим использовать настраиваемый либо из-за рисков безопасности / DOS, либо из-за производительности. Исключения всегда будут .

  • Каковы желаемые свойства этого хеша?

    • Все биты на входе используются

    • Результат хорошо распределен

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

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

Форма API

`` С #
// Будет жить в основной сборке
// .NET Framework: mscorlib
// Ядро .NET: System.Runtime / System.Private.CoreLib
пространство имен System
{
общедоступная структура HashCode
{
public static int Combine(Значение T11);
public static int Combine(Значение T11, значение T22);
public static int Combine(Значение T11, значение T22, значение T33);
public static int Combine(Значение T11, значение T22, значение T33, значение T44);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66, значение T77);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66, значение T77, значение T88);

    public void Add<T>(T value);
    public void Add<T>(T value, IEqualityComparer<T> comparer);
    public void Add<T>(T[] value);
    public void Add<T>(T[] value, int index, int length);
    public void Add(byte[] value);
    public void Add(byte[] value, int index, int length);
    public void Add(string value);
    public void Add(string value, StringComparison comparisonType);

    public int ToHashCode();
}

}

Notes:

* We decided to not override `GetHashCode()` to produce the hash code as this would be weird, both naming-wise as well as from a behavioral standpoint (`GetHashCode()` should return the object's hash code, not the one being computed).
* We decided to use `Add` for the builder patter and `Combine` for the static construction
* We decided to use not provide a static initialization method. Instead, `Add` will do this on first use.
* The struct is mutable, which is unfortunate but we feel the best compromise between making `GetHashCode()` very cheap & not cause any allocations while allowing the structure to be bigger than 32-bit so that the hash code algorithm can use more bits during accumulation.
* `Combine` will just call `<value>.GetHashCode()`, so it has the behavior of the value's type `GetHashCode()` implementation
    - For strings that means different casing will produce different hash codes
    - For arrays, that means the hash code doesn't look at the contents but uses reference semantics for the hash code
    - If that behavior is undesired, the developer needs to use the builder-style approach

### Usage

The simple case is when someone just wants to produce a good hash code for a given type, like so:

```C#
public class Customer
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override int GetHashCode() => HashCode.Combine(Id, FirstName, LastName);
}

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

`` С #
общедоступный клиент частичного класса
{
публичное переопределение int GetHashCode () =>
HashCode.Combine (
Идентификатор,
StringComparer.OrdinalIgnoreCase.GetHashCode (Имя),
StringComparer.OrdinalIgnoreCase.GetHashCode (LastName),
);
}

And lastly, if the developer needs more flexibility, such as producing a hash code for more than eight values, we also provide a builder-style approach:

```C#
public partial class Customer
{
    public override int GetHashCode()
    {
        var hashCode = new HashCode();
        hashCode.Add(Id);
        hashCode.Add(FirstName, StringComparison.OrdinalIgnoreCase);
        hashCode.Add(LastName, StringComparison.OrdinalIgnoreCase);
        return hashCode.ToHashCode();
    }
}

Следующие шаги

Этот вопрос останется открытым. Чтобы реализовать API, нам нужно решить, какой алгоритм использовать.

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

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

@jamesqo , вам все еще интересно работать в этой области? В этом случае, пожалуйста, обновите предложение соответствующим образом.

@terrajobst , нам также может понадобиться public static int Combine<T1>(T1 value); . Я знаю, что это выглядит немного забавно, но это обеспечит способ распространения битов из чего-то с ограниченным входным хеш-пространством. Например, многие перечисления имеют только несколько возможных хешей, использующих только несколько нижних битов кода. Некоторые коллекции построены на предположении, что хэши распределены по большему пространству, поэтому распространение битов может помочь коллекции работать более эффективно.

public void Add(string value, StrinComparison comparison);

Nit: Параметр StringComparison должен иметь имя comparisonType чтобы соответствовать именованию, используемому везде, где еще StringComparison используется в качестве параметра.

Критерии, которые помогут нам выбрать алгоритмы, будут следующими:

  1. Есть ли у алгоритма хороший лавинный эффект? То есть, имеет ли каждый бит ввода 50% шанс перевернуть каждый бит вывода? На этом сайте есть исследование нескольких популярных алгоритмов.
  2. Является ли алгоритм быстрым для небольших входов? Поскольку HashCode.Combine обычно обрабатывает 8 или меньше int, время запуска может быть более важным, чем пропускная способность. На этом сайте для начала есть интересный набор данных. Здесь также могут потребоваться разные ответы для разных архитектур или других опорных точек (ОС, AoT против JIT и т. Д.).

Что мы действительно хотели бы видеть, так это показатели производительности кандидатов, написанных на C #, чтобы мы могли быть достаточно уверены в том, что их характеристики будут соответствовать .NET. Если вы напишете кандидата, а мы не выберем его для этого, это все равно будет полезной работой всякий раз, когда я действительно соберу предложение API для некриптографического хеш-API.

Вот некоторые кандидаты, которые, на мой взгляд, заслуживают оценки (но не стесняйтесь предлагать других):

  • Marvin32 ( у нас уже есть реализация C # здесь ). Мы знаем, что это достаточно быстро для String.GetHashCode, и считаем, что он устойчив к HashDoS
  • xxHash32 (быстрый по алгоритму на x86 здесь , который имеет высокое качество в соответствии с SMHasher)
  • FarmHash ( здесь самый быстрый на x64. Я не нашел для него хорошего индикатора качества. Его может быть сложно написать на C #)
  • xxHash64 (усечено до 32 бит) (это не явный победитель скорости, но может быть легко сделать, если у нас уже есть xxHash32)
  • SpookyHash (как правило, хорошо работает с большими наборами данных)

Жаль, что методы Add не могут иметь возвращаемый тип ref HashCode и возвращать ref this чтобы их можно было использовать свободно,

Допустит ли это возврат readonly ref ? / cc @jaredpar @VSadov

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

Если лицензия несовместима, нам может потребоваться написать алгоритм с нуля.

ИМО, использование методов Add должно быть чрезвычайно редким. Это будет для очень сложных сценариев, и необходимость уметь «бегло говорить» на самом деле не будет.

Для общих случаев использования для 99% всех случаев пользовательского кода нужно иметь возможность использовать => HashCode.Combine(...) и все будет в порядке.

@morganbr

нам также может понадобиться public static int Combine<T1>(T1 value); . Я знаю, что это выглядит немного забавно, но это обеспечит способ распространения битов из чего-то с ограниченным входным хеш-пространством.

Имеет смысл. Я добавил это.

@justinvp

Nit: Параметр StringComparison должен иметь имя comparisonType чтобы соответствовать именованию, используемому везде, где еще StringComparison используется в качестве параметра.

Фиксированный.

@CyrusNajmabadi

ИМО, использование методов Add должно быть чрезвычайно редким. Это будет для очень сложных сценариев, и необходимость уметь «бегло говорить» на самом деле не будет.

Согласовано.

@benaadams - re: ref, возвращающий this из Add - нет, this не может быть возвращен ref в методах структуры, так как это может быть rValue или temp.

`` С #
ref var r = (новый T ()). ReturnsRefThis ();

// здесь r относится к некоторой переменной. Который из? Каков объем / срок службы?
r = SomethingElse ();
`` ''

Если это полезно для сравнения, несколько лет назад я портировал хеш-функцию Jenkins lookup3 ( источник C ) на C # здесь .

Интересно насчет коллекций:

@terrajobst

c# public void Add<T>(T[] value);

Почему существует перегрузка для массивов, но не для общих коллекций (например, IEnumerable<T> )?

Кроме того, вас не сбивает с толку, что HashCode.Combine(array) и hashCode.Add((object)array) ведут себя одним способом (используйте ссылочное равенство), а hashCode.Add(array) ведет себя иначе (объединяет хеш-коды значений в массив)?

@CyrusNajmabadi

Для общих случаев использования для 99% всех случаев пользовательского кода можно просто использовать => HashCode.Combine(...) и все будет в порядке.

Если цель действительно состоит в том, чтобы иметь возможность использовать Combine в 99% случаев использования (а не, скажем, в 80%), тогда не следует Combine каким-либо образом поддерживать коллекции хеширования на основе значений. в коллекции? Может быть, должен быть отдельный метод, который это делает (либо метод расширения, либо статический метод на HashCode )?

Если Add является мощным сценарием, следует ли предполагать, что пользователь должен выбирать между Object.GetHashCode и объединением отдельных элементов коллекций? Если это поможет, мы могли бы рассмотреть возможность переименования версий массива (и потенциальных IEnumerable). Что-то вроде:
c# public void AddEnumerableHashes<T>(IEnumerable<T> enumerable); public void AddEnumerableHashes<T>(T[] array); public void AddEnumerableHashes<T>(T[] array, int index, int length);
Интересно, понадобятся ли нам также перегрузки с IEqualityComparers.

Предложение: Сделайте структуру построителя реализуемой IEnumerable для поддержки синтаксиса инициализатора коллекции:

C# return new HashCode { SomeField, OtherField, { SomeString, StringComparer.UTF8 }, { SomeHashSet, HashSet<int>.CreateSetComparer() } }.GetHashCode()

Это намного элегантнее, чем вызов Add() вручную (в частности, вам не нужна временная переменная), и при этом по-прежнему нет выделений.

подробнее

@SLaks Может быть, этот более приятный синтаксис может подождать https://github.com/dotnet/csharplang/issues/455 (при условии, что это предложение было поддержано), так что HashCode не придется реализовывать поддельный IEnumerable ?

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

Мне кажется странным, что GetHashCode не возвращает вычисленный хэш-код. Я думаю, это запутает разработчиков. Например, @SLaks уже использовал его в своем предложении вместо ToHashCode .

@justinvp Если GetHashCode() не собирается возвращать вычисленный хэш-код, его, вероятно, следует пометить как [Obsolete] и [EditorBrowsable(Never)] .

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

@terrajobst

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

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

@terrajobst Я только что видел ваш комментарий. Простите меня за задержку ответа, я не спешил изучать уведомление, потому что думал, что это будет только то, что взад и вперед никуда не денется. Рад видеть, что это не так! : тада:

Я был бы рад взять это в руки и провести измерение пропускной способности / распределения (я предполагаю, что это то, что вы имели в виду, говоря «заинтересованы в работе в этой области»). Однако дайте мне секунду, чтобы дочитать все комментарии здесь.

@terrajobst

Можем ли мы изменить

public void Add<T>(T[] value);
public void Add<T>(T[] value, int index, int length);
public void Add(byte[] value);
public void Add(byte[] value, int index, int length);

к

public void AddRange<T>(T[] values);
public void AddRange<T>(T[] values, int index, int count);
public void AddRange<T>(T[] values, int index, int count, IEqualityComparer<T> comparer);

? Я переименовал Add -> AddRange чтобы избежать поведения, упомянутого @svick . Я удалил byte перегрузки, так как мы можем специализироваться, используя typeof(T) == typeof(byte) внутри метода, если нам нужно сделать что-либо, зависящее от байта. Также я изменил value -> values и length -> count . Также имеет смысл иметь перегрузку компаратора.

@terrajobst Не могли бы вы напомнить мне, почему

        public void Add(string value);
        public void Add(string value, StringComparison comparisonType);

необходимо, когда у нас есть

        public void Add<T>(T value);
        public void Add<T>(T value, IEqualityComparer<T> comparer);

?

@svick

@justinvp Если GetHashCode () не собирается возвращать вычисленный хэш-код, его, вероятно, следует пометить [Устарело] и [EditorBrowsable (Никогда)].

: +1:

@terrajobst Можем ли мы вернуться к неявному преобразованию из HashCode -> int , чтобы не было метода ToHashCode ? изменить: ToHashCode в порядке. См. Ответ @CyrusNajmabadi ниже.

@jamesqo StringComparison - это перечисление.
Однако вместо этого люди могут использовать эквивалент StringComparer .

Можем ли мы вернуться к неявному преобразованию HashCode -> int, чтобы не использовать метод ToHashCode?

Мы обсудили это и на встрече отказались. Проблема в том, что когда пользователь получает последнее int, часто выполняется дополнительная работа. т.е. внутренние компоненты хэш-кода часто выполняют этап завершения и могут сбрасывать себя в новое состояние. Было бы странно, если бы это произошло с неявным преобразованием. Если вы сделали это:

HashCode hc = ...

int i1 = hc;
int i2 = hc;

Тогда можно было получить другие результаты.

По этой причине нам также не нравится явное преобразование (поскольку люди не думают о преобразованиях как об изменении внутреннего состояния).

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

Я удалил байтовые перегрузки, так как мы можем специализироваться, используя typeof (T) == typeof (byte)

IIRC были некоторые опасения по поводу того, что это не нормально с точки зрения JIT. Но это могло быть только для случаев, когда "typeof ()" не является типом значения. Пока jit будет эффективно делать правильные вещи для случаев с типом значения typeof (), это должно быть хорошо.

@CyrusNajmabadi Я не знал, что преобразование в int может включать изменение состояния. ToHashCode это тогда.

Для тех, кто думает о перспективах криптовалюты - http://tuprints.ulb.tu-darmstadt.de/2094/1/thesis.lehmann.pdf

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

@blowdart , какую конкретную часть вы хотели бы выделить?

Возможно, я не был слишком откровенен об этом выше, но единственные некриптографические хэши, которые я не знаю о взломах HashDoS, - это Marvin и SipHash. То есть, даже заполнение (скажем) Murmur случайным значением может быть нарушено и использовано для DoS-атаки.

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

Решения

  • Мы должны удалить все методы AddRange потому что сценарий неясен. Маловероятно, что массивы будут появляться очень часто. И когда задействованы более крупные массивы, возникает вопрос, следует ли кэшировать вычисления. Увидев цикл for на вызывающей стороне, становится ясно, что вам нужно подумать об этом.
  • Мы также не хотим добавлять IEnumerable перегрузки в AddRange потому что они будут выделены.
  • Мы не думаем, что нам нужна перегрузка для Add которая принимает string и StringComparison . Да, это, вероятно, более эффективно, чем вызов через IEqualityComparer , но мы можем исправить это позже.
  • Мы думаем, что отметка GetHashCode как устаревшая из-за ошибки - это хорошая идея, но мы пошли бы еще дальше и также спрячемся от IntelliSense.

Это оставляет нам:

`` С #
// Будет жить в основной сборке
// .NET Framework: mscorlib
// Ядро .NET: System.Runtime / System.Private.CoreLib
пространство имен System
{
общедоступная структура HashCode
{
public static int Combine(Значение T11);
public static int Combine(Значение T11, значение T22);
public static int Combine(Значение T11, значение T22, значение T33);
public static int Combine(Значение T11, значение T22, значение T33, значение T44);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66, значение T77);
public static int Combine(Значение T11, значение T22, значение T33, значение T44, значение T55, значение T66, значение T77, значение T88);

    public void Add<T>(T value);
    public void Add<T>(T value, IEqualityComparer<T> comparer);

    [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
    [EditorBrowsable(Never)]
    public override int GetHashCode();

    public int ToHashCode();
}

}
`` ''

Следующие шаги: проблема решаемая - для реализации необходимого нам API с несколькими подходящими алгоритмами в качестве экспериментов - см. Список https://github.com/dotnet/corefx/issues/14354#issuecomment -305028686, чтобы мы могли решить, какой алгоритм выбрать (на основе измерений пропускной способности и распределения, вероятно, разные ответы для разных архитектур ЦП).

Сложность: Большой

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

@karelz Несмотря на мой комментарий выше , я передумал, потому что не думаю, что у меня есть квалификация, чтобы выбрать лучший алгоритм хеширования. Я просмотрел некоторые из перечисленных библиотек @morganbr и понял, что реализация довольно сложна , поэтому я не могу легко перевести ее на C #, чтобы проверить себя. У меня небольшой опыт работы с C ++, поэтому мне было бы сложно просто установить библиотеку и написать тестовое приложение.

Однако я не хочу, чтобы это навсегда оставалось в списке желаемых. Если через неделю никто не займется этим, я рассмотрю возможность размещения вопроса на Programmers SE или Reddit.

Я не тестировал его (и не оптимизировал иным образом), но вот базовая реализация хеш-алгоритма Murmur3, который я использую в нескольких своих личных проектах: https://gist.github.com/tannergooding/0a12559d1a912068b9aeb4b9586aad7f

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

Пример: Mumur3 (и другие) очень быстрые для больших наборов данных и обеспечивают отличное распределение, но они могут работать «плохо» (с точки зрения скорости, а не с точки зрения распределения) для небольших наборов данных.

Я полагаю, что мы должны сделать что-то вроде: если общее количество байтов меньше X, выполните алгоритм A; в противном случае используйте алгоритм B. Он по-прежнему будет детерминированным (для каждого прогона), но позволит нам обеспечить скорость и распределение на основе фактического размера входных данных.

Вероятно, также стоит отметить, что некоторые из упомянутых алгоритмов имеют реализации, специально разработанные для инструкций SIMD, поэтому наиболее производительное решение, вероятно, будет включать FCALL на каком-то уровне (как это сделано с некоторыми реализациями BufferCopy) или может включать в себя зависимость на System.Numerics.Vector .

@jamesqo , мы рады помочь с выбором; больше всего нам нужна информация о производительности для возможных реализаций (в идеале C #, хотя, как указывает @tannergooding , некоторые алгоритмы нуждаются в специальной поддержке компилятора). Как я упоминал выше, если вы создадите кандидата, который не выбран, мы, вероятно, воспользуемся им позже, поэтому не беспокойтесь о том, что работа будет потрачена впустую.

Я знаю, что существуют тесты для различных реализаций, но я думаю, что важно провести сравнение с использованием этого API и вероятного диапазона входных данных (например, структур с 1-10 полями).

@tannergooding , такая адаптивность может быть наиболее эффективной, но я не понимаю, как она будет работать с методом Add, поскольку он не знает, сколько раз он будет вызван. Хотя мы могли бы сделать это с помощью Combine, это означало бы, что серия вызовов Add может дать другой результат, чем соответствующий вызов Combine.

Кроме того, учитывая, что наиболее вероятный диапазон входных данных составляет 4-32 байта ( Combine`1 - Combine`8 ), мы надеемся, что в этом диапазоне не будет больших изменений производительности.

такая адаптивность может быть наиболее эффективной, но я не понимаю, как она будет работать с методом Add, поскольку он не знает, сколько раз он будет вызван.

Я лично не уверен, что форма API вполне подходит для хеширования общего назначения (однако она близка) ...

В настоящее время мы предоставляем методы Combine для статического построения. Если они предназначены для объединения всех входных данных и создания окончательного хэш-кода, тогда имя будет «плохим» и что-то вроде Compute может быть более подходящим.

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

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

Для тех, кто ищет менее сложную отправную точку, попробуйте xxHash32. Это, вероятно, довольно легко перевести на C # ( люди это сделали ).

Все еще тестирую локально, но я вижу следующие показатели пропускной способности для моей реализации Murmur3 на C #.

Это для статических методов объединения для 1-8 входов:

1070.18 mb/s
1511.49 mb/s
1674.89 mb/s
1957.65 mb/s
2083.24 mb/s
2140.94 mb/s
2190.27 mb/s
2245.53 mb/s

Моя реализация предполагает, что GetHashCode должен вызываться для каждого ввода и что вычисленное значение должно быть завершено перед возвратом.

Я объединил значения int , так как их проще всего проверить.

Чтобы вычислить пропускную способность, я выполнил 10 001 итерацию, отбросив первую итерацию как прогон «разминки».

На каждой итерации я запускаю 10 000 под-итераций, в которых вызываю HashCode.Combine , передавая результат предыдущей под-итерации в качестве первого входного значения в следующей итерации.

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

Очистим код и немного поделимся им.

@tannergooding , похоже, большой прогресс. Чтобы убедиться, что вы получаете правильные измерения, цель API состоит в том, что вызов HashCode.Combine(a, b) эквивалентен вызову

HashCode hc = new HashCode();
hc.Add(a); // Initializes the hash state, calls a.GetHashCode() and feeds the result into the hash state
hc.Add(b); // Calls b.GetHashCode() and feeds the result into the hash state
return hc.ToHashCode(); // Finalizes the hash state, truncates it to an int, resets the internal state and returns the int

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

👍

Именно это и делает код, который я написал. Единственное отличие состоит в том, что я эффективно встраиваю весь код (нет необходимости выделять new HashCode() и отслеживать количество объединенных байтов, поскольку оно является постоянным).

@morganbr. Реализация + тест пропускной способности для Murmur3: https://gist.github.com/tannergooding/89bd72f05ab772bfe5ad3a03d6493650

MurmurHash3 основан на алгоритме, описанном здесь: https://github.com/aappleby/smhasher/wiki/MurmurHash3 , в репо говорится, что это MIT

Работа над xxHash32 (пункт BSD-2 - https://github.com/Cyan4973/xxHash/blob/dev/xxhash.c) и SpookyHash (общественное достояние - http://www.burtleburtle.net/bob/hash /spooky.html) варианты

@tannergooding Опять же, не эксперт по хешам, но я вспомнил [читал статью] [1], в которой говорилось, что Murmur не устойчив к DoS, поэтому просто укажем на это, прежде чем мы выберем это.

@jamesqo , я могу ошибаться, но я почти уверен, что уязвимость применима к Murmur2, а не к Murmur3.

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

Упс, забыл ссылку на статью: http://emboss.github.io/blog/2012/12/14/breaking-murmur-hash-flooding-dos-reloaded/.

@tannergooding ОК. Звучит неплохо: +1:

@tannergooding , я взглянул на вашу реализацию Murmur3, и в целом она выглядит правильно и, вероятно, довольно хорошо оптимизирована. Чтобы убедиться, что я правильно понял, используете ли вы тот факт, что комбинированное значение и внутреннее состояние Murmur являются 32-битными? Вероятно, это довольно хорошая оптимизация для данного случая и объясняет некоторые из моих прежних недоразумений.

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

  • Комбинироватьдолжен по-прежнему вызывать CombineValue на значении 1
  • Первые вызовы CombineValue должны принимать случайное начальное число
  • ToHashCode должен сбросить _bytesCombined и _combinedValue

А пока я тоскую по этому API, насколько плохо для меня реализовать GetHashCode через (field1, field2, field3).GetHashCode() ?

@ jnm2 , объединитель хэш-кода ValueTuple стремится упорядочить ваши входные данные в хеш-коде (и отбрасывать самые последние из них). Вы можете не заметить пару полей и хеш-таблицу, которая делится на простое число. Для большого количества полей или хеш-таблицы, которая делится на степень двойки, энтропия последнего поля, которое вы вставляете, будет иметь наибольшее влияние на то, есть ли у вас коллизии (например, если ваше последнее поле является логическим или маленьким int, вы наверняка будет много коллизий, если это гид, скорее всего, не будет).

ValueTuple также не работает с полями, которые все равны 0.

Кстати, мне пришлось прекратить работу над другими реализациями (иметь более высокий приоритет). Не уверен, когда смогу забрать его обратно.

Итак, если этого недостаточно для структурированного типа, почему этого достаточно для кортежа?

@ jnm2 , это одна из причин, по которой эту функцию стоит

Большая таблица хеш-функций с характеристиками производительности и качества:
https://github.com/leo-yuriev/t1ha

@arespr Я думаю, что команда ищет реализацию хеш-функций на C #. Спасибо, что поделились.

@tannergooding Вы все еще не можете восстановить эту проблему? Если да, то я напишу на Reddit / Twitter, что мы ищем эксперта по хешированию.

edit: Сделал сообщение на Reddit. https://www.reddit.com/r/csharp/comments/6qsysm/looking_for_hash_expert_to_help_net_core_team/?ref=share&ref_source=link

@jamesqo , у меня есть несколько

Кроме того, текущие измерения будут ограничены тем, что мы можем в настоящее время закодировать на C #, однако, если / когда это станет чем-то вроде (https://github.com/dotnet/designs/issues/13), измерения, вероятно, несколько изменятся ;)

Кроме того, текущие измерения будут ограничены тем, что мы можем в настоящее время кодировать на C #, однако, если / когда это станет чем-то вроде (dotnet / designs # 13), измерения, скорее всего, несколько изменятся;)

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

@jamesqo , спасибо, что ищете людей, которые могут помочь. Мы были бы счастливы, если бы над этим поработал кто-то, кто не является экспертом по хешированию - нам действительно просто нужен кто-то, кто может перенести некоторые алгоритмы на C # с других языков или проектов, а затем провести измерения производительности. После того, как мы выбрали кандидатов, наши эксперты сделают то же, что и мы, в случае любого изменения - проверит код на правильность, производительность, безопасность и т. Д.

Привет! Я только что прочитал обсуждение, и, по крайней мере, мне кажется, что дело решительно закрыто в пользу murmur3-32 PoC. Что, кстати, кажется мне очень хорошим выбором, и я бы рекомендовал не тратить больше ненужной работы (но, возможно, даже отказаться от .Add() участников ...).

Но в том маловероятном случае, если кто-то захочет продолжить работу по повышению производительности, я мог бы предоставить некоторый код для xx32, xx64, hsip13 / 24, seahash, murmur3-x86 / 32 (и я интегрировал marvin32 impl сверху) и (пока неоптимизировано) sip13 / 24, spookyv2. Некоторые версии City выглядят достаточно простыми для переноса, если возникнет такая необходимость. Этот наполовину заброшенный проект имел в виду несколько иной вариант использования, поэтому не существует класса HashCode с предлагаемым API; но для сравнительного анализа это не должно иметь большого значения.

Определенно не готов к производству: код применяет большое количество грубой силы, такой как копирование-макароны, злокачественное разрастание агрессивных встроенных и небезопасных; байтов не существует, равно как и невыровненных чтений. Даже тесты против тестовых векторов ref-impl эвфемистически называются «неполными».

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

@gimpf

Я только что прочитал обсуждение, и, по крайней мере, мне кажется, что дело решительно закрыто в пользу murmur3-32 PoC. Что, кстати, кажется мне очень хорошим выбором, и я бы рекомендовал больше не тратить ненужную работу.

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

Но в том маловероятном случае, если кто-то захочет продолжить работу по повышению производительности, я мог бы предоставить некоторый код для xx32, xx64, hsip13 / 24, seahash, murmur3-x86 / 32 (и я интегрировал marvin32 impl сверху) и (пока неоптимизировано) sip13 / 24, spookyv2. Некоторые версии City выглядят достаточно простыми для переноса, если возникнет такая необходимость.

Да, пожалуйста! Мы хотим собрать код для как можно большего числа алгоритмов, с которыми можно было бы протестировать. Каждый новый алгоритм, который вы можете внести, ценен. Было бы очень признательно, если бы вы могли перенести и алгоритмы City.

Определенно не готов к производству: код применяет большое количество грубой силы, такой как копирование-паста, злокачественное разрастание агрессивных встроенных и небезопасных; байтов не существует, равно как и невыровненных чтений. Даже тесты против тестовых векторов ref-impl эвфемистически называются «неполными».

Это нормально. Просто принесите код, и кто-нибудь другой сможет его найти, если возникнет необходимость.

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

Да, это было бы отлично!

@jamesqo Хорошо, я

@gimpf , звучит действительно здорово, и мы хотели бы услышать о вашем прогрессе по ходу дела (не нужно ждать, пока вы проработаете каждый алгоритм!). Не готово к производству - это нормально, если вы уверены, что код дает правильные результаты, а производительность хорошо отображает то, что мы увидим в реализации, готовой к производству. После того, как мы выберем кандидатов, мы сможем работать с вами над тем, чтобы добиться высокого качества внедрения.

Я не видел анализа того, как энтропия seahash сравнивается с другими алгоритмами. У вас есть какие-нибудь указания по этому поводу? У него есть интересные компромиссы с производительностью ... векторизация звучит быстро, но модульная арифметика звучит медленно.

@morganbr У меня готов тизер.

О SeaHash : Нет, пока не знаю о качестве; на случай, если производительность интересна, добавлю в SMHasher. По крайней мере, автор утверждает, что это хорошо (используя его для контрольных сумм в файловой системе), а также утверждает, что во время микширования не теряется энтропия.

О хэшах и тестах : Project Haschisch.Kastriert , wiki-страница с первыми результатами тестирования, сравнивающими xx32, xx64, hsip13, hsip24, marvin32, sea и murmur3-32.

Некоторые важные предостережения:

  • Это был очень быстрый тест с низкими настройками точности.
  • Реализации еще не закончены, и некоторые претенденты все еще отсутствуют. Реализации Streaming (такая вещь может понадобиться для разумной поддержки .Add ()) нуждаются в фактической оптимизации.
  • В настоящее время SeaHash не использует сид.

Первые впечатления:

  • для больших сообщений xx64 - самая быстрая из перечисленных реализаций (около 3,25 байта на цикл, насколько я понимаю, или 9,5 ГиБ / с на моем ноутбуке)
  • для коротких сообщений ничего хорошего нет, но у murmur3-32 и (что удивительно) есть преимущество, но последнее, вероятно, объясняется тем, что в seahash еще не используется seed.
  • «эталонный тест» для доступа к HashSet<> требует доработки, так как все находится почти в пределах погрешности измерения (я видел большие различия, но все же не стоит о нем говорить)
  • при объединении хэш-кодов PoC murmur-3A примерно в 5-20 раз быстрее, чем у нас здесь
  • некоторые абстракции в C # очень дороги; это делает сравнение хеш-алгоритмов более раздражающим, чем необходимо.

Я напишу вам еще раз, как только немного поправлю ситуацию.

@gimpf , это

  1. Ваши результаты показывают, что SimpleMultiplyAdd примерно в 5 раз медленнее, чем Murmur3a @tannergooding. Это кажется странным, поскольку у Мурмура больше работы, чем умножение + сложение (хотя я признаю, что вращение - более быстрая операция, чем сложение). Возможно ли, что ваши реализации имеют общую неэффективность, которой нет в этой реализации Murmur, или мне следует читать это как пользовательские реализации, имеющие большое преимущество перед универсальными?
  2. Получение результатов для 1, 2 и 4 комбинаций - это хорошо, но этот API поднимается до 8. Можно ли получить результаты и для этого, или это вызывает слишком много дублирования?
  3. Я видел, что вы работали на X64, поэтому эти результаты должны помочь нам в выборе нашего алгоритма X64, но другие тесты показывают, что алгоритмы могут сильно различаться между X86 и X64. Легко ли вам также получить результаты для X86? (В какой-то момент нам также понадобятся ARM и ARM64, но они определенно могут подождать)

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

@morganbr Эти выходные были более

По поводу ваших вопросов:

  1. Ваши результаты показывают, что SimpleMultiplyAdd примерно в 5 раз медленнее, чем Murmur3a @tannergooding. Это кажется странным ...

Я сам подумал. Это была ошибка копирования / вставки, SimpleMultiplyAdd всегда объединял четыре значения ... Кроме того, переупорядочив некоторые операторы, объединитель умножения-сложения стал немного быстрее (примерно на 60% выше пропускная способность).

Возможно ли, что ваши реализации имеют общую неэффективность, которой нет в этой реализации Murmur, или мне следует читать это как пользовательские реализации, имеющие большое преимущество перед универсальными?

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

Однако даже эти реализации остаются слишком медленными; необходима дальнейшая работа. Для меня производительность .NET в этой области абсолютно непонятна; добавление или удаление копии локальной переменной может легко изменить производительность в два раза. Скорее всего, я не смогу предоставить достаточно хорошо оптимизированные реализации для выбора наилучшего варианта.

  1. Получение результатов для комбинаций 1, 2 и 4 - это хорошо, но этот API достигает 8.

Я расширил тесты комбайнов. Никаких сюрпризов на этом фронте.

  1. Я видел, что вы работали на X64 (...). Легко ли вам также получить результаты для X86?

Когда-то было, но потом я портировал на .NET Standard. Теперь я нахожусь в аду зависимостей, и работают только тесты .NET Core 2 и CLR 64bit. Эту проблему можно решить достаточно легко, как только я решу текущие проблемы.

Как вы думаете, это будет в версии 2.1?

@gimpf Вы давно не

@jamesqo Я исправил тест, который приводил к странным результатам, и добавил City32, SpookyV2, Sip13 и Sip24 в список доступных алгоритмов. Sips работают так же быстро, как и ожидалось (относительно пропускной способности xx64), а City и Spooky - нет (то же самое верно и для SeaHash).

Для комбинирования хэш-кодов Murmur3-32 по-прежнему выглядит неплохим вариантом, но мне еще предстоит провести более исчерпывающее сравнение.

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

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

@gimpf , похоже, большой прогресс. У вас есть таблица результатов, чтобы мы могли увидеть, достаточно ли ее для принятия решения и продвижения вперед?

@morganbr Я обновил результаты тестирования .

На данный момент у меня есть только 64-битные результаты для .NET Core 2. Для этой платформы City64 без начального числа является самым быстрым во всех размерах. Включая семя, XX-32 связан с Murmur-3-32. К счастью, это те же самые алгоритмы, которые имеют репутацию быстрых для 32-битных платформ, но, очевидно, нам нужно проверить, что это верно и для моей реализации. Результаты кажутся типичными для реальной производительности, за исключением того, что Sea и SpookyV2 кажутся необычно медленными.

Вам нужно будет подумать, насколько вам действительно нужна защита от хеширования для комбайнеров хеш-кода. Если заполнение необходимо только для того, чтобы сделать хэш явно непригодным для сохранения, то city64 после XOR с 32-битным семенем будет улучшением. Поскольку эта утилита предназначена только для комбинирования хешей (и не заменяет, например, хеш-код для строк, или может быть добавляемым хешером для целочисленных массивов и т. Д.), Этого может быть достаточно.

Если вы считаете, что OTOH вам нужен, вы будете рады увидеть, что Sip13 обычно менее чем на 50% медленнее, чем XX-32 (на 64-битных платформах), но этот результат, вероятно, будет значительно отличаться для 32-битных приложений.

Не знаю, насколько это актуально для corefx, но я добавил результаты LegacyJit 32bit (w / FW 4.7).

Хочу сказать, что результаты смехотворно медленные. Однако, например, при 56 Мбайт / с против 319 Мбайт / с я не смеюсь (это Sip, ему больше всего не хватает оптимизации поворота влево). Думаю, я помню, почему в январе отменил свой проект хеш-алгоритма .NET ...

Итак, RyuJit-32bit по-прежнему отсутствует и (будем надеяться) даст совсем другие результаты, но для LegacyJit-x86 Murmur-3-32 легко побеждает, и только City-32 и xx-32 могут приблизиться. У Murmur по-прежнему плохая производительность: всего около 0,4–1,1 ГБ / с вместо 0,6–2 ГБ / с (на той же машине), но, по крайней мере, она находится на правильном уровне.

Сегодня вечером я собираюсь запустить тесты на нескольких своих боксах и опубликовать результаты (Ryzen, i7, Xeon, A10, i7 Mobile и, думаю, еще несколько).

@tannergooding @morganbr Несколько приятных и важных обновлений.

Важно прежде всего:

  • Я исправил некоторые комбинации-реализации, которые выдавали неправильные значения хеш-функции.
  • Набор тестов теперь работает усерднее, чтобы избежать постоянного сворачивания. City64 был восприимчивым (как и murmur-3-32 в прошлом). Это не значит, что теперь я понимаю все результаты, но они гораздо более правдоподобны.

Хорошие вещи:

  • Реализации объединителя теперь доступны для всех перегрузок аргументов от 1 до 8, включая несколько более громоздкие, развернутые вручную реализации для xx / city.
  • Тесты и тесты тоже проверяют их. Поскольку многие хеш-алгоритмы имеют специальные младшие байтовые сообщения, эти измерения могут представлять интерес.
  • Упрощенный запуск тестов для нескольких целей (Core vs. FW).

Чтобы запустить набор на всех основных реализациях для объединения хэш-кодов, включая «Пустой» (чистые накладные расходы) и «умножение-сложение» (оптимизированная по скорости версия известного ответа SO):

bin\Release\net47\Haschisch.Benchmarks.Net47.exe -j:clr_x86 -j:clr_x64_legacy -j:clr_x64 -j:core_x64 -- CombineHashCode --allcategories=prime

(_Для удобства выполнения 32-битных тестов Core требуется предварительная версия BenchmarkDotNet (или, возможно, установка только для 32-битной версии плюс использование тестовой программы на основе Core). Затем, надеюсь, он должен работать с использованием -j: core_x86) _

Результаты : После исправления всех ошибок xx32, кажется, выигрывает для всех перегрузок с 64-битным RyuJIT в Windows 10 на мобильном Haswell i7 в "быстром" запуске. Между Sips и marvin32 всегда побеждает Sip-1-3. Sip-1-3 примерно в 4 раза медленнее, чем xx32, что опять же примерно в 2 раза медленнее, чем примитивный сумматор умножения и сложения. Результаты 32-битного ядра все еще отсутствуют, но я более или менее жду стабильной версии BenchmarkDotNet, которая решит эту проблему за меня.

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

Еще раз спасибо @gimpf за фантастические данные! Посмотрим, сможем ли мы превратить это в решение.

Для начала я бы разделил алгоритмы так:
Быстро + Хорошая энтропия (по скорости):

  1. xxHash32
  2. City64 (это, вероятно, будет медленным на x86, поэтому нам, вероятно, придется выбрать что-то другое для x86)
  3. Murmur3A

Устойчивость к HashDoS:

  • Marvin32
  • SipHash. Если мы склоняемся к этому, нам нужно, чтобы он был рассмотрен крипто-экспертами Microsoft, чтобы подтвердить, что результаты исследования приемлемы. Нам также нужно будет выяснить, какие параметры достаточно безопасны. В документе предлагается что-то среднее между Сип-2-4 и Сип-4-8.

Вне конкуренции (медленно):

  • SpookyV2
  • Город32
  • xxHash64
    * SeaHash (а данных по энтропии у нас нет)

Вне конкуренции (плохая энтропия):

  • Умножить Добавить
  • HSip

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

@morganbr Кажется, у вас все в порядке. В качестве точки данных в раундах SipHash проект Rust попросил Жана-Филиппа Аумассона ,

(См. PR ржавчины: # 33940 и сопутствующий выпуск ржавчины: # 29754 ).

Основываясь на данных и комментариях, предлагаю использовать xxHash32 на всех архитектурах. Следующим шагом будет его реализация. @gimpf , ты заинтересован в создании пиара для этого?

Для тех, кто обеспокоен HashDoS, я скоро внесу предложение об универсальном API хеширования, которое должно включать Marvin32 и может включать SipHash. Это также будет подходящим местом для других реализаций, над которыми работали @gimpf и @tannergooding .

@morganbr Я могу составить PR, если позволит время. Кроме того, я лично предпочел бы xx32, если это не снижает принятия.

@gimpf , как твоё время? Если у вас действительно нет времени, мы также можем посмотреть, не захочет ли кто-нибудь еще попробовать.

@morganbr Я планировал сделать это до 5 ноября, и все еще выглядит неплохо, что я найду время в следующие две недели.

@gimpf , отлично звучит. Спасибо за обновления!

@terrajobst - Я немного опоздал на вечеринку (извините), но нельзя ли изменить тип возвращаемого значения метода Add?

`` С #
public HashCode Добавить(Значение T);
public HashCode Добавить(Значение T, IEqualityComparerкомпаратор);

The params code is clearly there for scenarios where you have multiple fields, e.g.

```c#
        public override int GetHashCode() => new HashCode().Add(Name, Surname).ToHashCode();

Однако точно так же можно добиться того же, хотя и с меньшим расходом на выделение массива:

c# public override int GetHashCode() => new HashCode().Add(Name).Add(Surname).Add(Age).ToHashCode();

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

Изменить: implicit operator int также было бы неплохо для DRY, но не совсем критично.

@jcdickinson

мы не можем изменить тип возвращаемого значения метода Add?

Мы уже обсуждали это в старом предложении, и оно было отклонено.

почему вообще существует бесполезная перегрузка параметров?

Мы не добавляем никаких перегрузок params? Нажмите Ctrl + F для "params" на этой веб-странице, и вы увидите, что ваш комментарий - единственное место, где появляется это слово.

Неявный оператор int также был бы хорош для DRY, но не совсем критично.

Я думаю, что это тоже обсуждалось где-то выше ...

@jamesqo спасибо за объяснение.

параметры перегрузки

Я имел в виду AddRange , но, думаю, это не вызовет никакого интереса.

@jcdickinson AddRange был в исходном предложении, но не в текущей версии. Он был отклонен проверкой API (см. Https://github.com/dotnet/corefx/issues/14354#issuecomment-308190321 от @terrajobst):

Мы должны удалить все методы AddRange потому что сценарий неясен. Маловероятно, что массивы будут появляться очень часто. И когда задействованы более крупные массивы, возникает вопрос, следует ли кэшировать вычисления. Увидев цикл for на вызывающей стороне, становится ясно, что вам нужно подумать об этом.

@gimpf Я пошел дальше и наполнил предложение xxHash32 . Не стесняйтесь брать эту реализацию. Он имеет тесты против реальных векторов xxHash32.

Редактировать

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

Я поигрался с интерфейсом и теперь понимаю, почему свободный интерфейс был отклонен; это значительно медленнее.

BenchmarkDotNet=v0.10.9, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i7-4800MQ CPU 2.70GHz (Haswell), ProcessorCount=8
Frequency=2630626 Hz, Resolution=380.1377 ns, Timer=TSC
.NET Core SDK=2.0.2
  [Host]     : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT

Использование не встроенного метода в качестве источника хэш-кода; 50 вызовов Add против свободного метода расширения:

| Метод | Среднее | Ошибка | StdDev | Масштабированный |
| ------- | ---------: | ---------: | ---------: | -------: |
| Добавить | 401,6 нс | 1.262 нс | 1.180 нс | 1.00 |
| Tally | 747,8 нс | 2.329 нс | 2.178 нс | 1.86 |

Однако следующий шаблон действительно работает:

`` С #
общедоступная структура HashCode: System.Collections.IEnumerable
{
[EditorBrowsable (EditorBrowsableState.Never)]
[Устарело ("Этот метод предусмотрен для синтаксиса инициализатора коллекции.", Error: true)]
общедоступный IEnumerator GetEnumerator () => выбросить новое NotImplementedException ();
}

public override int GetHashCode() => new HashCode()
{
    Age, // int
    { Name, StringComparer.Ordinal }, // use Comparer
    Hat // some arbitrary object
}.ToHashCode();

`` ''

Он также имеет характеристики, идентичные текущему предложению:

| Метод | Среднее | Ошибка | StdDev | Масштабированный |
| ------------ | ---------: | ---------: | ---------: | --- ----: |
| Добавить | 405.0 нс | 2.130 нс | 1.889 нс | 1.00 |
| Инициализатор | 400,8 нс | 4.821 нс | 4.274 нс | 0,99 |

К сожалению, это в некотором роде взлом, так как IEnumerable необходимо реализовать, чтобы компилятор оставался довольным. При этом Obsolete выдаст ошибку даже при foreach - вам действительно нужно что-то сломать, чтобы столкнуться с исключением. MSIL в обоих случаях по существу идентичен.

@jcdickinson благодарит за то, что

Совет: как только вы примете решение, GitHub автоматически подпишет вас на получение всех уведомлений из репозитория (500+ в день), я бы рекомендовал изменить его на просто «Не смотрю», который будет отправлять вам все ваши упоминания и уведомления о проблемах. вы подписались на.

@jcdickinson , меня определенно интересуют способы избежать надоедливого повторения (хотя я понятия не имею, как люди отнесутся к синтаксису инициализатора). Я припоминаю, что с беглым языком было две проблемы:

  1. Проблема с перфомансом, которую вы отметили
  2. Возвращаемое значение из свободных методов - это копия структуры. Слишком легко случайно потерять ввод, делая такие вещи, как:
var hc = new HashCode();
var newHc = hc.Add(foo);
hc.Add(bar);
return newHc.ToHashCode();

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

@karelz Я полагаю, что @gimpf уже рассмотрел эту проблему заранее. Поскольку он больше знаком с реализацией, вместо этого назначьте эту проблему @gimpf . ( редактировать: nvm)

@terrajobst Один из видов запросов API в последнюю минуту для этого. Поскольку мы отметили GetHashCode устаревшим, мы неявно сообщаем пользователю, что HashCode s не являются значениями, предназначенными для сравнения, несмотря на то, что это структуры, которые обычно неизменяемы / сопоставимы. В таком случае следует ли нам также пометить Equals устаревшее?

[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
[EditorBrowsable(Never)]
// If this is too harsh, base.Equals() is fine as long as the [Obsolete] stays
public override bool Equals(object obj) => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes.");

Думаю, что-то подобное было сделано с Span .

Если это будет принято, то я думаю ...

  1. Я бы подумал об использовании should not или may not вместо cannot в устаревшем сообщении.
  2. При условии, что исключение остается, я бы поместил ту же строку в его сообщение, на всякий случай, если метод будет вызван через приведение или открытый общий.

@ Joe4evr Меня GetHashCode , тогда:

public override int GetHashCode() => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes.");

@morganbr Почему вы снова

PR, чтобы выставить его в CoreFX, еще не прошел.

@gimpf у вас есть доступный код, который вы

@JonHanna Он разместил это здесь: https://github.com/gimpf/Haschisch.Kastriert

@JonHanna , мне было бы интересно узнать, как проходит ваше тестирование, чтобы мы могли начать думать о том, что было бы полезно в некриптографическом хеширующем API общего назначения.

@morganbr Где был бы подходящий форум для обсуждения такого API? Я ожидаю, что такой API будет состоять не только из наименьшего общего знаменателя, и, возможно, хороший API также потребует улучшенной JIT-обработки более крупных структур. Обсуждение всего того, что можно было бы сделать, в отдельном выпуске ...

@gimpf Открыл для вас. dotnet / corefx # 25666

@morganbr - Можем ли мы получить имя пакета и номер версии, которая будет включать этот коммит?

@karelz , не могли бы вы помочь @smitpatel с информацией о пакете / версии?

Я бы попробовал ежедневную сборку .NET Core - подождал бы до завтра.
Я не думаю, что есть пакет, от которого можно просто зависеть.

Вопрос к участникам здесь. Roslyn IDE позволяет пользователям создавать GetHashCode impl на основе набора полей / свойств в их классе / структуре. В идеале люди могли бы использовать новый HashCode.Combine, который был добавлен в https://github.com/dotnet/corefx/pull/25013 . Однако у некоторых пользователей не будет доступа к этому коду. Итак, мы хотели бы по-прежнему иметь возможность генерировать GetHashCode, который будет работать для них.

Недавно мы обратили внимание на то, что генерируемая нами форма проблематична. А именно потому, что VB компилируется с проверкой переполнения по умолчанию, и наш impl вызовет переполнение. Кроме того, VB не имеет возможности отключить проверку переполнения для области кода. Он либо включен, либо выключен полностью для всей сборки.

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

  1. Одна / две строки в GetHashCode на каждое используемое поле / свойство.
  2. Нет переполнения.
  3. Достаточно хорошее хеширование. Мы не ждем потрясающих результатов. Но что-то, что, как мы надеемся, уже было проверено на предмет приличия и отсутствия проблем, которые вы обычно получаете с a + b + c + d или a ^ b ^ c ^ d .
  4. Никаких дополнительных зависимостей / требований по коду.

Например, один из вариантов для VB - создать что-то вроде:

return (a, b, c, d).GetHashCode()

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

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

-

Примечание: наш существующий выданный код:

        Dim hashCode = -252780983
        hashCode = hashCode * -1521134295 + i.GetHashCode()
        hashCode = hashCode * -1521134295 + j.GetHashCode()
        Return hashCode

Ясно, что это может вылиться через край.

Это также не проблема для C #, поскольку мы можем просто добавить unchecked { } вокруг этого кода. Такой мелкозернистый контроль невозможен в VB.

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

Что ж, вы могли бы сделать Tuple.Create(...).GetHashCode() . Очевидно, это влечет за собой выделение памяти, но кажется лучше, чем выбросить исключение.

Есть ли причина, по которой вы не можете просто сказать пользователю установить System.ValueTuple ? Поскольку это встроенная функция языка, я уверен, что пакет System.ValueTuple очень совместим практически со всеми платформами, верно?

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

да. было бы неплохо, чтобы это не приводило к выделению памяти.

Есть ли причина, по которой вы не можете просто сказать пользователю установить System.ValueTuple?

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

На самом деле кажется, что у пользователей VB должен быть способ решить эту проблему разумным образом :) Но такой подход ускользает от меня :)

@CyrusNajmabadi , если вам действительно нужно выполнить свой собственный расчет хэша в коде пользователя, CRC32 может работать, поскольку это комбинация поиска в таблице и XOR (но не арифметики, которая может переполняться). Однако есть и недостатки:

  1. CRC32 не обладает большой энтропией (но, вероятно, все же лучше, чем то, что сейчас излучает Roslyn).
  2. Вам нужно будет поместить таблицу поиска на 256 записей где-нибудь в коде или выдать код для создания таблицы поиска.

Если вы еще этого не делаете, я надеюсь, что вы сможете определить тип HashCode и использовать его, когда это возможно, поскольку XXHash должен быть намного лучше.

@morganbr См. https://github.com/dotnet/roslyn/pull/24161

Мы делаем следующее:

  1. Используйте System.HashCode, если он доступен. Выполнено.
  2. В противном случае, если в C #:
    2а. Если не в проверенном режиме: сгенерировать развернутый хеш.
    2b. Если в проверенном режиме: генерировать развернутый хэш, заключенный в unchecked {}.
  3. В противном случае, если в VB:
    3b. Если не в проверенном режиме: сгенерировать развернутый хеш.
    3c. Если в проверенном режиме, но имеет доступ к System.ValueTuple: сгенерировать Return (a, b, c, ...).GetHashCode()
    3d. Если в проверенном режиме без доступа к System.ValueTuple. Сгенерируйте развернутый хеш, но добавьте комментарий в VB, что переполнение очень вероятно.

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

Вам нужно будет поместить таблицу поиска на 256 записей где-нибудь в коде

Это было бы совершенно неприятно :)

Неужели код создания таблиц тоже неприятен? По крайней мере, исходя из примера Википедии , это не так много кода (но он все равно должен быть где-то в исходном коде пользователя).

Насколько ужасно было бы добавить источник HashCode в проект, как это делает Roslyn (с IL), с (гораздо более простыми) определениями классов атрибутов компилятора, когда они недоступны через какую-либо сборку, на которую есть ссылки?

Насколько ужасно было бы добавить источник HashCode в проект, как это делает Roslyn, с (гораздо более простыми) определениями классов атрибутов компилятора, когда они недоступны через какую-либо сборку, на которую есть ссылки?

  1. Не требует ли исходный код HashCode поведения переполнения?
  2. Я просмотрел исходный код HashCode. Это не тривиально. Создание всей этой глупости в проекте пользователя было бы довольно тяжеловесным делом.

Я просто удивлен, что нет никаких хороших способов заставить математику переполнения работать в VB :(

Итак, как минимум, даже если бы мы хешировали два значения вместе, кажется, что нам нужно было бы создать:

`` С #
var hc1 = (uint) (значение1? .GetHashCode () ?? 0); // может переполниться
var hc2 = (uint) (значение2? .GetHashCode () ?? 0); // может переполниться

        uint hash = MixEmptyState();
        hash += 8; // can overflow

        hash = QueueRound(hash, hc1);
        hash = QueueRound(hash, hc2);

        hash = MixFinal(hash);
        return (int)hash; // can overflow
Note that this code already has 4 lines that can overflow.  It also has two helper functions you need to call (i'm ignoring MixEmptyState as that seems more like a constant).  MixFinal can *definitely* overflow:

```c#
        private static uint MixFinal(uint hash)
        {
            hash ^= hash >> 15;
            hash *= Prime2;
            hash ^= hash >> 13;
            hash *= Prime3;
            hash ^= hash >> 16;
            return hash;
        }

как и QueueRound:

c# private static uint QueueRound(uint hash, uint queuedValue) { hash += queuedValue * Prime3; return Rol(hash, 17) * Prime4; }

Так что я, честно говоря, не понимаю, как это будет работать :(

Как ужасно было бы добавить исходный код HashCode в проект, как это делает Roslyn (с IL) с

Как вы себе представляете, как это работает? Что напишут заказчики и что в ответ сделают компиляторы?

Кроме того, все это можно было бы решить, если .Net уже имеет общедоступных помощников, представленных в поверхностном API, которые конвертируют из uint в int32 (и наоборот) без переполнения.

Они существуют? Если это так, я могу легко написать версии VB, просто используя их для ситуаций, когда нам нужно переходить между типами без переполнения.

Неужели код создания таблиц тоже неприятен?

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

Вы можете получить примерно правильное поведение при переполнении, выполняя приведение к некоторой комбинации знаковых и беззнаковых 64-битных типов. Что-то вроде этого (непроверено, и я не знаю синтаксиса литья VB):

Dim hashCode = -252780983
hashCode = (Int32)((Int32)((Unt64)hashCode * -1521134295) + (UInt64)i.GetHashCode())

Как вы знаете, что следующее не выходит за рамки?

c# (Int32)((Unt64)hashCode * -1521134295)

Или финальное приведение (int32), если на то пошло?

Я не понимал, что он будет использовать операции conv с проверкой переполнения. Я думаю, вы могли бы замаскировать его до 32 бит перед кастингом:

(Int32)(((Unt64)hashCode * -1521134295) & 0xFFFFFFFF)

предположительно 31 бит, так как значение uint32.Max также будет переполняться при преобразовании в Int32 :)

Это определенно возможно. Уродливо ... но возможно :) В этом коде много приведений.

Ok. Думаю, у меня есть работоспособное решение. Ядро алгоритма, который мы генерируем сегодня:

c# hashCode = hashCode * -1521134295 + j.GetHashCode();

Допустим, мы выполняем 64-битную математику, но hashCode ограничен 32-битным кодом. Тогда <largest_32_bit> * -1521134295 + <largest_32_bit> не переполняет 64 бита. Таким образом, мы всегда можем выполнить вычисления в 64-битном формате, а затем сократить его до 32 (или 32-битных), чтобы гарантировать, что следующий раунд не будет переполнен.

Спасибо!

@ MaStr11 @morganbr @sharwell и все присутствующие. Я обновил свой код, чтобы сгенерировать следующее для VB:

        Dim hashCode As Long = 2118541809
        hashCode = (hashCode * -1521134295 + a.GetHashCode()) And Integer.MaxValue
        hashCode = (hashCode * -1521134295 + b.GetHashCode()) And Integer.MaxValue
        Return CType(hashCode And Integer.MaxValue, Integer)

Может ли кто-нибудь проверить меня, чтобы убедиться, что это имеет смысл и не должно переполняться даже при включенном отмеченном режиме?

@CyrusNajmabadi , это не переполнится (потому что Int64.Max = Int32.Max * Int32.Max и ваши константы намного меньше этого), но вы маскируете старший бит до нуля, поэтому это всего лишь 31-битный хеш. Считается ли оставление высокого бита включенным переполнением?

@CyrusNajmabadi hashCode - это Long которое может принимать значения от 0 до Integer.MaxValue . Почему я получаю это?

image

Но нет, он не может переполниться.

Кстати, я бы предпочел, чтобы Roslyn добавляла пакет NuGet, чем добавляла неоптимальный хеш.

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

Неплохо подмечено. Думаю, я думал о другом алгоритме, который использовал uints. Поэтому, чтобы безопасно преобразовать long в uint, мне нужно было не включать бит знака. Однако, поскольку все это математика со знаком, я думаю, было бы неплохо просто замаскировать против 0xffffffff, гарантируя, что мы сохраняем только нижние 32 бита после добавления каждой записи.

Я бы предпочел, чтобы Roslyn добавляла пакет NuGet, чем добавляла неоптимальный хеш.

Пользователи уже могут это сделать, если захотят. Речь идет о том, что делать, когда пользователи не могут или не могут добавить эти зависимости. Речь также идет о предоставлении пользователям достаточно «достаточно хорошего» хеша. то есть что-то лучше, чем обычный подход «x + y + z», который часто используют люди. Он не предназначен для того, чтобы быть «оптимальным», потому что нет четкого определения того, что «оптимально», когда дело доходит до хеширования для всех пользователей. Обратите внимание, что используемый здесь подход уже реализован компилятором для анонимных типов. Он демонстрирует достаточно хорошее поведение, не усложняя пользовательский код. Со временем, когда все больше и больше пользователей смогут двигаться вперед, они могут постепенно исчезнуть и быть заменены HashCode.Combine для большинства людей.

Я немного поработал над этим и придумал следующее, что, как мне кажется, решает все проблемы:

        Dim hashCode As Long = 2118541809
        hashCode = (hashCode * -1521134295 + a.GetHashCode()).GetHashCode()
        hashCode = (hashCode * -1521134295 + b.GetHashCode()).GetHashCode()
        Return CType(hashCode, Integer)

Интересный фрагмент - это вызов .GetHashCode() для значения int64, созданного (hashCode * -1521134295 + a.GetHashCode()) . Вызов .GetHashCode для этого 64-битного значения имеет два хороших свойства для наших нужд. Во-первых, он гарантирует, что hashCode всегда хранит в себе только допустимое значение int32 (что делает последнее возвращаемое приведение всегда безопасным для выполнения). Во-вторых, это гарантирует, что мы не потеряем никакой ценной информации в верхних 32 битах временного значения int64, с которым мы работаем.

@CyrusNajmabadi На самом деле я спрашивал о предложении установить пакет. Спасает меня от необходимости это делать.

Если вы наберете HashCode, а затем, если System.HashCode предоставлен в пакете MS nuget, Roslyn предложит его.

Я хочу, чтобы он сгенерировал несуществующую перегрузку GetHashCode и установил пакет в той же операции.

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

Примечание: в какой пакет nuget включен этот api, чтобы мы могли добавить ссылку на него?

Реализация находится в System.Private.CoreLib.dll, поэтому она будет частью пакета времени выполнения. Контракт - это System.Runtime.dll.

Ok. Если это так, то похоже, что пользователь получит это, если / когда перейдет на более новую Target Framework. Подобные вещи - совсем не тот шаг, который я бы сделал для проекта пользователя с помощью команды «сгенерировать равно + хэш-код».

Была ли эта страница полезной?
0 / 5 - 0 рейтинги