La nouvelle version de .NET a amélioré les performances des méthodes Min, Max, Average et Sum pour les tableaux et les listes. À votre avis, de combien leur vitesse d’exécution a-t-elle augmenté ? Deux fois ou cinq fois ? Non, ils sont allés encore plus vite. Voyons comment cela a été réalisé.
Comment LINQ s’est-il amélioré ?
LINQ (Language-Integrated Query) est un langage de requête simple et pratique. Il vous permet d’exprimer des opérations complexes de manière simple. Presque tous les développeurs .NET utilisent LINQ. Cependant, cette simplicité d’utilisation se fait au prix de la vitesse d’exécution et de l’allocation de mémoire supplémentaire. Dans la plupart des situations, cela n’a pas d’effet significatif. Cependant, dans les cas où les performances sont critiques, ces limitations peuvent être assez désagréables.
Ainsi, la récente mise à jour a amélioré les performances des méthodes suivantes :
- Enumérable.Max
- Enumérable.Min
- Enumérable.Moyenne
- Enumerable.Sum
Voyons comment leurs performances ont augmenté en utilisant le benchmark suivant :
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Collections.Generic;
using System.Linq;
[MemoryDiagnoser(displayGenColumns: false)]
public partial class Program
{
static void Main(string[] args) =>
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
[Params (10, 10000)]
public int Size { get; set; }
private IEnumerable<int> items;
[GlobalSetup]
public void Setup()
{
items = Enumerable.Range(1, Size).ToArray();
}
[Benchmark]
public int Min() => items.Min();
[Benchmark]
public int Max() => items.Max();
[Benchmark]
public double Average() => items.Average();
[Benchmark]
public int Sum() => items.Sum();
}
Résultats de référence :
Méthode |
Durée |
Taille |
Moyenne |
Rapport |
Alloué |
---|---|---|---|---|---|
Min |
.NET 6.0 |
dix |
75,491 ns |
1,00 |
32 B |
Min |
.NET 7.0 |
dix |
7,749 ns |
0,10 |
– |
Max |
.NET 6.0 |
dix |
71,128 ns |
1,00 |
32 B |
Max |
.NET 7.0 |
dix |
6,493 ns |
0,09 |
– |
Moyenne |
.NET 6.0 |
dix |
68,963 ns |
1,00 |
32 B |
Moyenne |
.NET 7.0 |
dix |
7,315 ns |
0,11 |
– |
Somme |
.NET 6.0 |
dix |
69,509 ns |
1,00 |
32 B |
Somme |
.NET 7.0 |
dix |
9,058 ns |
0,13 |
– |
Min |
.NET 6.0 |
10000 |
61 567,392 ns |
1,00 |
32 B |
Min |
.NET 7.0 |
10000 |
2 967,947 ns |
0,05 |
– |
Max |
.NET 6.0 |
10000 |
56 106,592 ns |
1,00 |
32 B |
Max |
.NET 7.0 |
10000 |
2 948,302 ns |
0,05 |
– |
Moyenne |
.NET 6.0 |
10000 |
52 803,907 ns |
1,00 |
32 B |
Moyenne |
.NET 7.0 |
10000 |
2 967,810 ns |
0,06 |
– |
Somme |
.NET 6.0 |
10000 |
52 732,121 ns |
1,00 |
32 B |
Somme |
.NET 7.0 |
10000 |
5 897,220 ns |
0,11 |
– |
Les résultats montrent que le temps d’exécution pour trouver l’élément minimum d’un tableau a généralement diminué de dix fois pour les petits tableaux et de 20 fois pour les tableaux contenant 10 000 éléments. De même, pour les autres méthodes (sauf pour trouver la somme, la différence entre les tailles des collections n’a pas beaucoup affecté les résultats).
Il convient également de noter que dans .NET 7, aucune mémoire supplémentaire n’est allouée lorsque les méthodes sont appelées.
Voyons comment ces méthodes fonctionnent avec Liste
Méthode |
Durée |
Taille |
Moyenne |
Rapport |
Alloué |
---|---|---|---|---|---|
Min |
.NET 6.0 |
dix |
122,554 ns |
1,00 |
40 B |
Min |
.NET 7.0 |
dix |
8,995 ns |
0,07 |
– |
Max |
.NET 6.0 |
dix |
115,135 ns |
1,00 |
40 B |
Max |
.NET 7.0 |
dix |
9,171 ns |
0,08 |
– |
Moyenne |
.NET 6.0 |
dix |
110,825 ns |
1,00 |
40 B |
Moyenne |
.NET 7.0 |
dix |
8,163 ns |
0,07 |
– |
Somme |
.NET 6.0 |
dix |
113,812 ns |
1,00 |
40 B |
Somme |
.NET 7.0 |
dix |
13,197 ns |
0,12 |
– |
Min |
.NET 6.0 |
10000 |
91 529,841 ns |
1,00 |
40 B |
Min |
.NET 7.0 |
10000 |
2 941,226 ns |
0,03 |
– |
Max |
.NET 6.0 |
10000 |
84 565,787 ns |
1,00 |
40 B |
Max |
.NET 7.0 |
10000 |
2 957,451 ns |
0,03 |
– |
Moyenne |
.NET 6.0 |
10000 |
81 205,103 ns |
1,00 |
40 B |
Moyenne |
.NET 7.0 |
10000 |
2 959,882 ns |
0,04 |
– |
Somme |
.NET 6.0 |
10000 |
81 857,576 ns |
1,00 |
40 B |
Somme |
.NET 7.0 |
10000 |
5 783,370 ns |
0,07 |
– |
Dans .NET 6, toutes les opérations sur les tableaux sont beaucoup plus rapides que sur les listes. Il en va de même pour les petites collections dans .NET 7. Cependant, à mesure que le nombre d’éléments augmente, les performances des listes sont égales aux tableaux.
Selon les résultats des tests, les performances des listes ont été multipliées par 31.
Mais comment cela pourrait-il être réalisé ?
Examinons de plus près la mise en œuvre de la Min méthode.
C’est ainsi que le Min méthode est implémentée dans .NET 6 :
public static int Min(this IEnumerable<int> source)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
int value;
using (IEnumerator<int> e = source.GetEnumerator())
{
if (!e.MoveNext())
{
ThrowHelper.ThrowNoElementsException();
}
value = e.Current;
while (e.MoveNext())
{
int x = e.Current;
if (x < value)
{
value = x;
}
}
}
return value;
}
La méthode est assez simple. D’abord, on obtient le IEnumerable
La nouvelle version du Min la méthode est différente :
public static int Min(this IEnumerable<int> source) => MinInteger(source);
Les MinEntier La méthode est appliquée à une collection d’entiers. Examinons-le plus en détail.
private static T MinInteger<T>(this IEnumerable<T> source)
where T : struct, IBinaryInteger<T>
{
T value;
if (source.TryGetSpan(out ReadOnlySpan<T> span))
{
if (Vector.IsHardwareAccelerated &&
span.Length >= Vector<T>.Count * 2)
{
.... // Optimized implementation
return ....;
}
}
.... //Implementation as in .NET 6
}
Premièrement, nous essayons d’obtenir l’objet de la ReadOnlySpan
Mais comment ça ReadOnlySpan, réellement? Les Portée et ReadOnlySpan Les types fournissent une représentation sûre d’une zone de mémoire gérée en continu et non gérée. La structuration de la Portée le type est défini comme un structure de référence. Cela signifie qu’il ne peut être placé que sur la pile, ce qui permet d’éviter d’allouer de la mémoire supplémentaire et améliore les performances des données.
Les Portée type a également subi quelques modifications dans la nouvelle version de C#. Depuis que C# 11 a introduit la possibilité de créer des champs de référence dans un structure de référencela représentation interne de Portée
Mais revenons au Min méthode. Quand vous obtenez ReadOnlySpanla méthode tente d’effectuer une recherche vectorielle à l’aide de la Vecteur
if (Vector.IsHardwareAccelerated && span.Length >= Vector<T>.Count * 2)
La première partie de la condition vérifie si la propriété Vector.IsHardwareAccelerated renvoie true. Voyons l’implémentation de cette propriété.
public static bool IsHardwareAccelerated
{
[Intrinsic]
get => false;
}
Les [Intrinsic] L’attribut est appliqué au getter. L’attribut indique que la valeur retournée par IsHardwareAccelerated peut être remplacé JIT. La propriété revient vrai si l’accélération matérielle peut être appliquée aux opérations sur les vecteurs grâce à la prise en charge JIT intégrée ; autrement, faux est retourné. Pour activer l’accélération matérielle, vous devez exécuter la génération pour la plate-forme x64 avec la configuration Release ou générer le projet pour AnyCPU avec le paramètre « Préférer 32 bits » désactivé.
Pour remplir la deuxième partie de la condition, la taille du Portée doit être au moins deux fois la taille du vecteur.
Comment la taille de ce vecteur est-elle calculée ?
Les Vecteur