Warum sind HashSets von Strukturen mit nullbaren Werten unglaublich langsam?

Ich habe den Leistungsabfall untersucht und ihn aufgespürt, um HashSets zu verlangsamen.
Ich habe Strukturen mit nullwertfähigen Werten, die als Primärschlüssel verwendet werden. Beispielsweise

public struct NullableLongWrapper
{
    private readonly long? _value;

    public NullableLongWrapper(long? value)
    {
        _value = value;
    }
}

Ich habe festgestellt, dass das Erstellen einesHashSet<NullableLongWrapper> ist außergewöhnlich langsam.

Hier ist ein Beispiel mit 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);
}

Ergebnis

           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

Verwenden einer Struktur mit einemNullable<long> im Vergleich zu einer Struktur mit einemlong ist 3540 mal langsamer!
In meinem Fall machte es den Unterschied zwischen 800ms und <1ms.

Hier sind die Umgebungsinformationen von BenchmarkDotNet:

OS = Microsoft Windows NT 6.1.7601 Service Pack 1
Prozessor = Intel (R) Core (TM) i7-5600U-CPU 2,60 GHz, ProcessorCount = 4
Frequenz = 2536269 Ticks, Auflösung = 394,2799 ns, Timer = TSC
CLR = MS.NET 4.0.30319.42000, Arch = 64-Bit-RELEASE [RyuJIT]
GC = Concurrent Workstation
JitModules = clrjit-v4.6.1076.0

Was ist der Grund, warum die Leistung so schlecht ist?

Antworten auf die Frage(4)

Ihre Antwort auf die Frage