Performance dans .NET Core

Bonjour à tous ! Cet article est une compilation des meilleures pratiques que mes collègues et moi-même appliquons depuis longtemps dans divers projets.
Informations sur la machine sur laquelle les calculs ont été effectués :BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 cœurs logiques et 4 cœurs physiques
.NET Core SDK=3.0.100
[Host]: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), RyuJIT 64 bits
Core: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), RyuJIT 64 bits
[Host]: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), RyuJIT 64 bits
Core: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), RyuJIT 64 bits
Job=Runtime Core=Core
ToList vs ToArray et Cycles
J'avais prévu de préparer cette information avec la sortie de .NET Core 3.0, mais j'ai été devancé ; je ne veux pas voler la gloire des autres ni copier des informations déjà existantes, donc je vais simplement indiquer .
J'aimerais seulement vous présenter mes mesures et résultats, j'ai ajouté des boucles inverses pour les amateurs de style d'écriture "C++".
Code :
public class Bench
{
private List _list;
private int[] _array;
[Params(100000, 10000000)] public int N;
[GlobalSetup]
public void Setup()
{
const int MIN = 1;
const int MAX = 10;
Random random = new Random();
_list = Enumerable.Repeat(0, N).Select(i => random.Next(MIN, MAX)).ToList();
_array = _list.ToArray();
}
[Benchmark]
public int ForList()
{
int total = 0;
for (int i = 0; i 0; i--)
{
total += _list[i];
}
return total;
}
[Benchmark]
public int ForeachList()
{
int total = 0;
foreach (int i in _list)
{
total += i;
}
return total;
}
[Benchmark]
public int ForeachArray()
{
int total = 0;
foreach (int i in _array)
{
total += i;
}
return total;
}
[Benchmark]
public int ForArray()
{
int total = 0;
for (int i = 0; i 0; i--)
{
total += _array[i];
}
return total;
}
}
La vitesse d'exécution dans .NET Core 2.2 et 3.0 est presque identique. Voici ce que j'ai pu obtenir dans .NET Core 3.0 :


Nous pouvons conclure que le traitement cyclique d'une collection de type Array est plus rapide grâce à ses optimisations internes et à une allocation explicite de la taille de la collection. Il est également important de rappeler que la collection de type List présente ses propres avantages, et vous devez utiliser la collection appropriée en fonction des calculs nécessaires. Même si vous écrivez la logique de travail avec des boucles, n'oubliez pas qu'il s'agit d'une boucle classique, et qu'elle peut également faire l'objet d'optimisations. Un article est paru il y a quelque temps sur Habr : . Il est encore d'actualité et recommandé à la lecture.
Lancer
Il y a un an, je travaillais dans une entreprise sur un projet héritage, où il était normal de gérer la validation des champs via une structure try-catch-throw. Je comprenais déjà à l'époque que c'était une logique métier malsaine, donc j'essayais de m'abstenir d'utiliser cette structure autant que possible. Mais examinons pourquoi il est mauvais d'aborder les erreurs de cette manière. J'ai écrit un petit code pour comparer deux approches et j'ai effectué des 'benchmarks' pour chaque option.
Code :
public bool ContainsHash()
{
bool result = false;
foreach (var file in _files)
{
var extension = Path.GetExtension(file);
if (_hash.Contains(extension))
result = true;
}
return result;
}
public bool ContainsHashTryCatch()
{
bool result = false;
try
{
foreach (var file in _files)
{
var extension = Path.GetExtension(file);
if (_hash.Contains(extension))
result = true;
}
if(!result)
throw new Exception("false");
}
catch (Exception e)
{
result = false;
}
return result;
}Les résultats dans .NET Core 3.0 et Core 2.2 montrent des résultats similaires (.NET Core 3.0) :


Le try-catch complique la compréhension du code et augmente le temps d'exécution de votre programme. Mais si vous avez besoin de cette structure, évitez d'insérer des lignes de code qui ne sont pas censées traiter des erreurs — cela facilitera la compréhension du code. En réalité, le système n'est pas tant affecté par la gestion des exceptions que par le lancement de ces erreurs via la structure throw new Exception.
Le lancement d'exceptions est plus lent qu'une classe qui rassemblerait l'erreur dans le bon format. Si vous traitez un formulaire ou des données et que vous savez clairement quelle erreur doit être gérée, pourquoi ne pas les traiter ?
Il ne faut pas écrire la construction throw new Exception() si cette situation n'est pas exceptionnelle. La gestion et le lancement d'exception coûtent très cher !!!
ToLower, ToLowerInvariant, ToUpper, ToUpperInvariant
Au cours de mes 5 années d'expérience sur la plateforme .NET, j'ai rencontré de nombreux projets qui utilisaient la comparaison de chaînes. J'ai également vu la situation suivante : il y avait une solution Enterprise avec de nombreux projets, chacun effectuant la comparaison de chaînes de manière différente. Mais que faut-il utiliser et comment unifier cela ? Dans le livre CLR via C# de Richter, j'ai trouvé des informations indiquant que la méthode ToUpperInvariant() fonctionne plus rapidement que ToLowerInvariant().
Extrait du livre :

Bien sûr, je ne l'ai pas cru et j'ai décidé de faire quelques tests, alors encore sur .NET Framework, et le résultat m'a choqué — plus de 15% de gain de performance. Le lendemain matin, à mon arrivée au travail, j'ai montré ces mesures à ma direction et leur ai fourni l'accès aux sources. Après cela, 2 des 14 projets ont été modifiés pour de nouvelles mesures, et compte tenu du fait que ces deux projets existaient pour traiter d'énormes tableaux Excel, le résultat était très significatif pour le produit.
Je vous présente également les mesures pour différentes versions de .NET Core, afin que chacun d'entre vous puisse faire un choix vers la solution la plus optimale. Je tiens juste à ajouter que dans l'entreprise où je travaille, nous utilisons ToUpper() pour la comparaison de chaînes.
Code :
public const string defaultString = "VXTDuob5YhummuDq1PPXOHE4PbrRjYfBjcHdFs8UcKSAHOCGievbUItWhU3ovCmRALgdZUG1CB0sQ4iMj8Z1ZfkML2owvfkOKxBCoFUAN4VLd4I8ietmlsS5PtdQEn6zEgy1uCVZXiXuubd0xM5ONVZBqDu6nOVq1GQloEjeRN8jXrj0MVUexB9aIECs7caKGddpuut3";
[Benchmark]
public bool ToLower()
{
return defaultString.ToLower() == defaultString.ToLower();
}
[Benchmark]
public bool ToLowerInvariant()
{
return defaultString.ToLowerInvariant() == defaultString.ToLowerInvariant();
}
[Benchmark]
public bool ToUpper()
{
return defaultString.ToUpper() == defaultString.ToUpper();
}
[Benchmark]
public bool ToUpperInvariant()
{
return defaultString.ToUpperInvariant() == defaultString.ToUpperInvariant();
}


.NET Core 3.0 montre un gain d'environ x2 pour chacun de ces méthodes et équilibre les implémentations entre elles.


Compilation Tier
Dans mon précédent article, j'ai brièvement décrit cette fonctionnalité, et j'aimerais corriger et compléter mes propos. La compilation à plusieurs niveaux accélère le temps de démarrage de votre solution, mais vous sacrifiez le fait que certaines parties de votre code seront compilées en une version plus optimisée en arrière-plan, ce qui peut entraîner quelques frais généraux. Avec l'arrivée de .NET Core 3.0, le temps de compilation des projets avec la compilation par niveaux a été réduit et les bogues liés à cette technologie ont été corrigés. Auparavant, cette technologie entraînait des erreurs lors des premières requêtes dans ASP.NET Core et provoquait un gel lors de la première compilation en mode compilation à plusieurs niveaux. À ce jour, dans .NET Core 3.0, elle est activée par défaut, mais vous pouvez la désactiver si vous le souhaitez. Si vous occupez un poste de team lead, senior, middle ou si vous êtes responsable de département, vous devez comprendre que le développement rapide d'un projet augmente la valeur de l'équipe et que cette technologie vous permettra d'économiser du temps tant pour les développeurs que pour le temps de travail du projet.
Élever le niveau de .NET
Mettez à jour votre .NET Framework / .NET Core. Souvent, chaque nouvelle version offre un gain de performance supplémentaire et ajoute de nouvelles fonctionnalités.
Mais quels sont exactement les avantages ? Examinons certains d'entre eux :
- .NET Core 3.0 a introduit des images R2R, qui permettront de réduire le temps de démarrage des applications .NET Core.
- À partir de la version 2.2, la compilation à plusieurs niveaux a été introduite, ce qui permet aux programmeurs de passer moins de temps à démarrer un projet.
- Support des nouveaux standards .NET Standard.
- Support de la nouvelle version du langage de programmation.
- Optimisation, chaque nouvelle version améliore l'optimisation des bibliothèques de base Collection/Struct/Stream/String/Regex et bien d'autres. Si vous passez de .NET Framework à .NET Core, vous bénéficierez d'un gain de performance important dès la sortie de la boîte. À titre d'exemple, je joins un lien sur une partie des optimisations qui ont été ajoutées dans .NET Core 3.0 :

Conclusion
Lors de l'écriture de code, il est important de prêter attention à différents aspects de votre projet et d'utiliser les fonctionnalités de votre langage de programmation et de votre plateforme pour atteindre le meilleur résultat. Je serais heureux si vous pouviez partager vos connaissances sur l'optimisation dans .NET.
Source : habr.com
