Почему хэш-наборы структур с нулевыми значениями невероятно медленные?
Я исследовал снижение производительности и отследил его до медленных хэш-наборов.
У меня есть структуры с нулевыми значениями, которые используются в качестве первичного ключа. Например:
public struct NullableLongWrapper
{
private readonly long? _value;
public NullableLongWrapper(long? value)
{
_value = value;
}
}
я заметил, что создание HashSet<NullableLongWrapper> - Это очень медленно.
вот пример использования BenchmarkDotNet: (Install-Package BenchmarkDotNet)
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
public class Program
{
static void Main()
{
BenchmarkRunner.Run<HashSets>();
}
}
public class Config : ManualConfig
{
public Config()
{
Add(Job.Dry.WithWarmupCount(1).WithLaunchCount(3).WithTargetCount(20));
}
}
public struct NullableLongWrapper
{
private readonly long? _value;
public NullableLongWrapper(long? value)
{
_value = value;
}
public long? Value => _value;
}
public struct LongWrapper
{
private readonly long _value;
public LongWrapper(long value)
{
_value = value;
}
public long Value => _value;
}
[Config(typeof (Config))]
public class HashSets
{
private const int ListSize = 1000;
private readonly List<long?> _nullables;
private readonly List<long> _longs;
private readonly List<NullableLongWrapper> _nullableWrappers;
private readonly List<LongWrapper> _wrappers;
public HashSets()
{
_nullables = Enumerable.Range(1, ListSize).Select(i => (long?) i).ToList();
_longs = Enumerable.Range(1, ListSize).Select(i => (long) i).ToList();
_nullableWrappers = Enumerable.Range(1, ListSize).Select(i => new NullableLongWrapper(i)).ToList();
_wrappers = Enumerable.Range(1, ListSize).Select(i => new LongWrapper(i)).ToList();
}
[Benchmark]
public void Longs() => new HashSet<long>(_longs);
[Benchmark]
public void NullableLongs() => new HashSet<long?>(_nullables);
[Benchmark(Baseline = true)]
public void Wrappers() => new HashSet<LongWrapper>(_wrappers);
[Benchmark]
public void NullableWrappers() => new HashSet<NullableLongWrapper>(_nullableWrappers);
}
результат:
Method | Median | Scaled
----------------- |---------------- |---------
Longs | 22.8682 us | 0.42
NullableLongs | 39.0337 us | 0.62
Wrappers | 62.8877 us | 1.00
NullableWrappers | 231,993.7278 us | 3,540.34
использование структуры с Nullable<long> по сравнению со структурой с long - это 3540 раз медленнее!
В моем случае это сделало разницу между 800 мс и
вот информация об окружающей среде из BenchmarkDotNet:
OS=Microsoft Windows NT 6.1.7601 Service Pack 1
Процессор = Intel(R) Core (TM) i7-5600U CPU 2.60 GHz, ProcessorCount=4
Частота=2536269 тиков, разрешение = 394.2799 НС, таймер=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-битный релиз [RyuJIT]
ГХ=параллельные станции
JitModules=clrjit-v4.6.1076.0
в чем причина такой низкой производительности?
2 ответа:
это происходит потому, что каждый из элементов
_nullableWrappersимеет тот же хэш-код, возвращенныйGetHashCode(), что приводит к тому, что хэширование вырождается в O(N) доступ, а не O(1).вы можете проверить это, распечатав все хэш-коды.
если вы измените свою структуру следующим образом:
public struct NullableLongWrapper { private readonly long? _value; public NullableLongWrapper(long? value) { _value = value; } public override int GetHashCode() { return _value.GetHashCode(); } public long? Value => _value; }он работает гораздо быстрее.
теперь очевидный вопрос: почему хэш-код каждого
NullableLongWrapperто же самое.ответ обсуждается в этой теме. Однако это не совсем отвечает на вопрос, поскольку ответ Ханса вращается вокруг структуры, имеющей два поля, из которых можно выбрать при вычислении хэш-кода , но в этом коде есть только одно поле на выбор - и это тип значения (a
struct).однако мораль этой истории такова:никогда не полагайтесь на значение по умолчанию
GetHashCode()значение типы!
дополнительное соглашение
я подумал, что, возможно, то, что происходит, связано с ответом Ханса в потоке, который я связал - возможно, он принимал значение первого поля (bool) в
Nullable<T>структура), и мои эксперименты показывают, что это может быть связано - но это сложно:рассмотрим этот код и его вывод:
using System; public class Program { static void Main() { var a = new Test {A = 0, B = 0}; var b = new Test {A = 1, B = 0}; var c = new Test {A = 0, B = 1}; var d = new Test {A = 0, B = 2}; var e = new Test {A = 0, B = 3}; Console.WriteLine(a.GetHashCode()); Console.WriteLine(b.GetHashCode()); Console.WriteLine(c.GetHashCode()); Console.WriteLine(d.GetHashCode()); Console.WriteLine(e.GetHashCode()); } } public struct Test { public int A; public int B; } Output: 346948956 346948957 346948957 346948958 346948959обратите внимание, как второй и третий хэш-кодов (по 1/0 и 0/1) являются то же самое, но все остальные разные. Я нахожу это странным, потому что четкое изменение a изменяет хэш-код, как и изменение B, но с учетом двух значений X и Y, один и тот же хэш-код генерируется для A=X, B=Y и A=Y, B=X.
(это звучит как некоторые вещи XOR происходит за кулисами, но это предположение.)
кстати, это поведение, когда оба поля могут быть показаны, чтобы внести свой вклад в хэш-код доказывает, что комментарий в справочном источнике для
ValueType.GetHashType()is неточно или неправильно:действие: наш алгоритм для возврата хэш-кода немного сложен. Мы ищем первое нестатическое поле и получаем его хэш-код. Если тип не имеет нестатических полей, мы возвращаем хэш-код типа. Мы не можем взять хэш-код статического элемента, потому что если этот элемент имеет тот же тип, что и исходный тип, мы окажемся в бесконечном цикле.
если этот комментарий был правдой, то четыре из пяти хэш-кодов в приведенном выше примере будет то же самое, так как
Aимеет то же значение, 0, для всех тех, кто. (Это предполагаетAэто первое поле, но вы получите те же результаты, если вы поменяете значения вокруг: оба поля явно вносят свой вклад в хэш-код.)затем я попытался изменить первое поле, чтобы быть bool:
using System; public class Program { static void Main() { var a = new Test {A = false, B = 0}; var b = new Test {A = true, B = 0}; var c = new Test {A = false, B = 1}; var d = new Test {A = false, B = 2}; var e = new Test {A = false, B = 3}; Console.WriteLine(a.GetHashCode()); Console.WriteLine(b.GetHashCode()); Console.WriteLine(c.GetHashCode()); Console.WriteLine(d.GetHashCode()); Console.WriteLine(e.GetHashCode()); } } public struct Test { public bool A; public int B; } Output 346948956 346948956 346948956 346948956 346948956Вау! Таким образом, создание первого поля bool делает все хэш-коды одинаковыми, независимо от значений любого из поля!
это все еще выглядит как какая-то ошибка для меня.
ошибка была исправлена в .NET 4, но только для Nullable. Пользовательские типы по-прежнему дают плохое поведение. источник