.NET Evolution: Ist das Upgrade auf die neuste Version die Mühe wert?

Sascha Kiefer

Einleitung

Seit der Veröffentlichung von .NET 6 im November 2021 haben wir eine rasante Entwicklung in der .NET-Reihe erlebt. Innerhalb von drei Jahren kamen .NET 7, .NET 8 und nun, im November 2024, steht .NET 9 kurz vor der Einführung. Diese schnelle Abfolge von Updates bringt die Frage mit sich: Ist es wirklich sinnvoll, auf die neueste Version zu aktualisieren? Welchen Einfluss haben diese Upgrades auf die Performance unserer Anwendungen? In diesem Artikel nehmen wir die Leistungssteigerungen in .NET 6, .NET 8 und .NET 9 unter die Lupe, um zu erörtern, ob ein Update für eure Projekte oder Kunden tatsächlich lohnenswert ist.

Die zentrale Frage für uns ist immer:

Kann ich eine spürbare Beschleunigung meiner Anwendung erwarten, wenn ich auf die neueste .NET-Version umsteige, ohne dass eine Anpassung meines Codes notwendig ist?

In diesem Beitrag vergleichen wir die Performance-Verbesserungen der Versionen .NET 6, .NET 8 und .NET 9, basierend auf den offiziellen Benchmarks und Tests von Microsoft. Wir wollen herausfinden, ob die neuesten Updates der .NET-Reihe einen echten Mehrwert bieten können.

Umgebung

Um die Performance-Verbesserungen in .NET 6, .NET 8 und .NET 9 zu vergleichen, haben wir die offiziellen Benchmarks von Microsoft verwendet und nutzen die .NET Benchmarking Library für unsere eigenen Tests.

Alle Benchmarks wurden auf einem Windows 11-System mit einem 13th Gen Intel Core i9-13900H durchgeführt. Die .NET-Versionen, die wir für die Tests verwendet haben, sind:

  • .NET 6.0 : .NET 6.0.33 (6.0.3324.36610), X64 RyuJIT AVX2
  • .NET 8.0 : .NET 8.0.8 (8.0.824.36612), X64 RyuJIT AVX2
  • .NET 9.0 : .NET 9.0.0 (9.0.24.43107), X64 RyuJIT AVX2

Was wurde verglichen?

Es gab viele Performance-Verbesserungen in .NET 6, .NET 8 und .NET 9, viele davon fallen jedoch in Nischen, die nicht für alle Anwendungen relevant sind. LINQ, JSON und Reflections sind jedoch wichtige Bestandteile moderner Anwendungen und daher haben wir uns entschieden, diese zu vergleichen.

LINQ

Wir fangen mit LINQ an, da es in der Vergangenheit oft kritisierte Performance hatte. Wir haben die folgenden LINQ-Operationen getestet ...

public class LinqBenchmarks
{
    private static readonly IEnumerable<int> _range = Enumerable.Range(0, 1000);
    private readonly IEnumerable<int> _list = _range.ToList();
    private readonly IEnumerable<int> _arrayDistinct = _range.ToArray().Distinct();
    private readonly IEnumerable<int> _appendSelect = _range.ToArray().Append(42).Select(i => i * 2);
    private readonly IEnumerable<int> _rangeReverse = _range.Reverse();
    private readonly IEnumerable<int> _listDefaultIfEmptySelect = _range.ToList().DefaultIfEmpty().Select(i => i * 2);
    private readonly IEnumerable<int> _listSkipTake = _range.ToList().Skip(500).Take(100);
    private readonly IEnumerable<int> _rangeUnion = _range.Union(Enumerable.Range(500, 1000));

    [Benchmark] public bool Any() => _list.Any(i => i == 1000);
    [Benchmark] public bool All() => _list.All(i => i >= 0);
    [Benchmark] public int Count() => _list.Count(i => i == 0);
    [Benchmark] public int First() => _list.First(i => i == 999);
    [Benchmark] public int Single() => _list.Single(i => i == 0);
    [Benchmark] public int DistinctFirst() => _arrayDistinct.First();
    [Benchmark] public int AppendSelectLast() => _appendSelect.Last();
    [Benchmark] public int RangeReverseCount() => _rangeReverse.Count();
    [Benchmark] public int DefaultIfEmptySelectElementAt() => _listDefaultIfEmptySelect.ElementAt(999);
    [Benchmark] public int ListSkipTakeElementAt() => _listSkipTake.ElementAt(99);
    [Benchmark] public int RangeUnionFirst() => _rangeUnion.First();
}

... und haben die spannendsten Ergebnisse zusammengefasst:

Dotnet Performance Linq Any() FunktionDotnet Performance Linq DefaultIfEmptySelectElementAt() FunktionDotnet Performance Linq DistinctFirst() FunktionDotnet Performance Linq RangeUnionFirst() Funktion

Die Ergebnisse zeigen, dass die Performance von LINQ-Operationen in .NET 9 im Vergleich zu .NET 6 und .NET 8 deutlich verbessert wurde. Wenn das Upgrade vonn .NET 6 auf .NET 8 schon enorm war, so ist der Sprung von .NET 8 auf .NET 9 noch beeindruckender.

JSON

JSON ist ein weiterer wichtiger Bestandteil moderner Anwendungen. Wir haben die Performance von JSON-Serialisierung und Deserialisierung in .NET 6, .NET 8 und .NET 9 getestet.

public class JsonBenchmarks
{
    private static readonly JsonSerializerOptions s_options = new()
    {
        Converters = { new JsonStringEnumConverter() },
        DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
    };

    [Params(BindingFlags.Default, BindingFlags.NonPublic | BindingFlags.Instance)]
    public BindingFlags _value;

    private byte[]? _jsonValue;
    private Utf8JsonWriter _writer = new(Stream.Null);

    [GlobalSetup]
    public void Setup() => _jsonValue = JsonSerializer.SerializeToUtf8Bytes(_value, s_options);

    [Benchmark]
    public void Serialize()
    {
        _writer.Reset();
        JsonSerializer.Serialize(_writer, _value, s_options);
    }

    [Benchmark]
    public BindingFlags Deserialize() =>
        JsonSerializer.Deserialize<BindingFlags>(_jsonValue, s_options);
}

Die Ergebnisse der JSON-Benchmarks sehen wie folgt aus:

Dotnet Performance Json Deserialize() FunktionDotnet Performance Json Serialize() Funktion

Die Performance von JSON-Serialisierung und Deserialisierung haben sich in .NET 9 im Vergleich zu .NET 6 und .NET 8 verbessert.

Reflections

Auch wenn man Reflections in modernen Anwendungen vermeiden sollte, gibt es immer noch Fälle, in denen sie notwendig sind.

public class ReflectionBenchmarks
{
    private static object s_staticReferenceField = new object();
    private object _instanceReferenceField = new object();
    private static int s_staticValueField = 1;
    private int _instanceValueField = 2;
    private object _obj = new();

    private static readonly Type _type = typeof(ReflectionBenchmarks);
    private readonly FieldInfo _staticReferenceFieldInfo = _type
        .GetField(nameof(s_staticReferenceField), BindingFlags.NonPublic | BindingFlags.Static)!;
    private readonly FieldInfo _instanceReferenceFieldInfo = _type
        .GetField(nameof(_instanceReferenceField), BindingFlags.NonPublic | BindingFlags.Instance)!;
    private readonly FieldInfo _staticValueFieldInfo = _type
        .GetField(nameof(s_staticValueField), BindingFlags.NonPublic | BindingFlags.Static)!;
    private readonly FieldInfo _instanceValueFieldInfo = _type
        .GetField(nameof(_instanceValueField), BindingFlags.NonPublic | BindingFlags.Instance)!;

    [Benchmark] public object? GetStaticReferenceField() => _staticReferenceFieldInfo.GetValue(null);
    [Benchmark] public void SetStaticReferenceField() => _staticReferenceFieldInfo.SetValue(null, _obj);

    [Benchmark] public object? GetInstanceReferenceField() => _instanceReferenceFieldInfo.GetValue(this);
    [Benchmark] public void SetInstanceReferenceField() => _instanceReferenceFieldInfo.SetValue(this, _obj);

    [Benchmark] public int GetStaticValueField() => (int)_staticValueFieldInfo.GetValue(null)!;
    [Benchmark] public void SetStaticValueField() => _staticValueFieldInfo.SetValue(null, 3);

    [Benchmark] public int GetInstanceValueField() => (int)_instanceValueFieldInfo.GetValue(this)!;
    [Benchmark] public void SetInstanceValueField() => _instanceValueFieldInfo.SetValue(this, 4);
}

Hier ist es nun auffällig, dass .NET8 gegenüber .NET6 teilweise langsamere Ergebnisse liefert. .NET9 hingegen zeigt in allen Bereichen eine deutliche Verbesserung.

Dotnet Performance Reflection GetStaticReferenceField() FunktionDotnet Performance Reflection SetStaticReferenceField() FunktionDotnet Performance Reflection GetInstanceReferenceField() FunktionDotnet Performance Reflection SetInstanceReferenceField() Funktion

Fazit

Die Performance-Verbesserungen in .NET 9 sind beeindruckend. Die Benchmarks zeigen, dass .NET 9 in vielen Bereichen eine deutliche Steigerung der Performance bietet. Wenn ihr also auf der Suche nach einer Möglichkeit seid, die Leistung eurer Anwendungen zu verbessern, kann ein Upgrade auf .NET 9 eine gute Option sein ohne dass ihr euren Code anpassen müsst. Es ist jedoch wichtig zu beachten, dass die Ergebnisse je nach Anwendung variieren können. Eigenes Testing und Benchmarking ist daher unerlässlich.

Quellcode

Der vollständige Quellcode der Benchmarks ist auf GitHub verfügbar.

Weiterführende Links

Keinen Artikel mehr verpassen

Kein Spam. Nur relevante News über und von uns. Jederzeit abbestellbar.