-.NET 9.0 réduit fortement le temps d’exécution par rapport à .NET 8 dans plusieurs scénarios LINQ courants, et supprime même les allocations dans certains benchmarks
- L’une des améliorations clés consiste à obtenir un
ReadOnlySpan<T>viaTryGetSpan()lors de l’itération sur un tableau ou uneList<T>, afin de réduire le coût de parcours TryGetSpan()identifieTSource[]etList<TSource>par comparaison de type, mais la façon dont il récupère un span sur le tableau interne deList<T>est une optimisation de type Unsafe qui peut être invalidée si la capacité change- Le LINQ de .NET 9 reconnaît des chaînes d’appels fréquentes pour créer des iterators spécialisés, puis applique des optimisations supplémentaires dans des méthodes terminales comme
Count(),First(),Last(),ElementAt()etSum() - Une simple migration et recompilation suffit pour profiter d’une partie des gains de performance de LINQ, avec aussi des optimisations comme l’usage du SIMD et la détection précoce des séquences vides
Pourquoi le parcours des tableaux et des listes est plus rapide
- Le premier benchmark stocke
Enumerable.Range(1, 10_000).ToArray()dans unIEnumerable<int>, puis exécuteCount,All,Any,First,Single,Lastafin de comparer .NET 8 et .NET 9 - Il utilise BenchmarkDotNet, et le projet doit cibler
net8.0;net9.0puis être compilé en mode Release - Dans .NET 9, le temps d’exécution de plusieurs méthodes chute fortement et les allocations disparaissent aussi
LinqCount: 16,198.490 ns à 3,043.563 ns, de 32 B alloués à aucune allocationLinqAny: 17,096.735 ns à 2,483.927 ns, de 32 B alloués à aucune allocationLinqFirst: 15,289.747 ns à 2,243.341 ns, de 32 B alloués à aucune allocationLinqSingle: 21,684.114 ns à 4,884.329 ns, de 32 B alloués à aucune allocationLinqAll: 10.588 ns à 2.562 ns, de 32 B alloués à aucune allocationLinqLast: 15.967 ns à 6.918 ns
Ce que change TryGetSpan()
- La principale cause du gain de performances est l’utilisation de
TryGetSpan() - Si l’enumerable parcouru est un tableau ou une liste,
TryGetSpan()renvoie unReadOnlySpan<T>permettant une itération plus rapide - Le branchement central vérifie
source.GetType() == typeof(TSource[])ousource.GetType() == typeof(List<TSource>), puis récupère le span- Les tableaux sont traités avec
Unsafe.As<TSource[]>(source) - Les listes utilisent
CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source))pour obtenir un span à partir du tableau interne
- Les tableaux sont traités avec
- Dans le code,
source.GetType()est appelé deux fois et le traitement ne passe pas par un cast suivi d’un test de null, mais c’est un choix fait par les experts performance de .NET en tenant compte des optimisations du compilateur C# et du JIT - Dans une pile .NET hautement optimisée, les micro-optimisations peuvent donner des résultats différents de ce que laisse penser l’apparence du code
Les contraintes de CollectionsMarshal.AsSpan()
List<TSource>référence en interne un tableau, et lorsqu’il faut augmenter ou réduire la capacité de la liste, un nouveau tableau est créé puis référencéCollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source))obtient unSpan<TSource>à partir de ce tableau interne- Si la capacité de la liste change d’une manière ou d’une autre, le tableau obtenu ainsi peut devenir invalide
- À cause de cette contrainte, certaines opérations
Enumerablequi incluent une itération différée, comme avecyield, peuvent difficilement s’appuyer sur cette optimisation - Le nom même de
System.Runtime.CompilerServices.Unsafereflète ce risque
Portée des appels à TryGetSpan()
- NDepend a analysé
System.Linq.dllpour identifier les appelants directs et indirects deTryGetSpan() - Le chemin de l’assembly analysé est
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0-rc.1.24431.7\System.Linq.dll - Une requête de code a été générée depuis
TryGetSpan()pour identifier les appelants, puis 56 méthodes correspondantes ont été exportées dans un graphe de dépendances - De nombreuses méthodes
Enumerablestandard tentent un parcours via span lorsque la collection est un tableau ou une liste - Mais comme la technique qui retient le tableau interne d’une liste n’est pas sûre, les opérations nécessitant une exécution différée gardent certaines limitations
Optimisations fondées sur des iterators spécialisés
- Le deuxième benchmark reprend les cas du PR Consolidate LINQ’s internal IIListProvider/IPartition into base Iterator class
- Les tests couvrent notamment
Distinct().First(),Append().Select().Last(),Reverse().Count(),DefaultIfEmpty().Select().ElementAt(),Skip().Take().ElementAt(),Union().First(),Select().Where().Select().Sum() - Dans .NET 9, certaines chaînes d’appels deviennent extrêmement rapides
DistinctFirst: 65.318 ns à 11.192 ns, de 328 B alloués à aucune allocationAppendSelectLast: 4,122.007 ns à 2.661 ns, de 144 B alloués à aucune allocationDefaultIfEmptySelectElementAt: 4,090.818 ns à 5.724 ns, de 144 B alloués à aucune allocationRangeUnionFirst: 66.309 ns à 6.193 ns, de 344 B alloués à aucune allocationListSkipTakeElementAt: 6.268 ns à 2.916 nsRangeReverseCount: 11.024 ns à 6.134 ns
- À l’inverse,
SelectWhereSelectSumest devenu plus lent, passant de 3,959.622 ns dans .NET 8 à 4,460.008 ns dans .NET 9, avec toujours 112 B alloués
Reconnaissance des chaînes LINQ fréquentes
- L’équipe performance de .NET a conçu le code pour reconnaître des chaînes d’appels LINQ courantes
- Lorsqu’une chaîne donnée est détectée, un iterator spécialisé est créé pour traiter le flux de travail plus efficacement
- Si la chaîne se termine par une méthode comme
Count(),First(),Last(),ElementAt()ouSum(), des optimisations supplémentaires deviennent possibles - Par exemple,
OrderBy(criteria).First()peut être optimisé pour s’exécuter commeMin(criteria)
Structure de Iterator<T> et des classes dérivées
- En interne, LINQ contient une classe de base abstraite
Iterator<T>et 40 classes dérivées - Ces classes sont toutes imbriquées dans la classe
Enumerable Iterator<T>est une classe abstraite, mais ses méthodes sont virtuelles, donc les classes dérivées ne redéfinissent que celles dont elles ont besoin- Cette structure sert de base pour embarquer des comportements spécialisés selon les chaînes d’appels
Exemple de ListWhereSelectIterator<TSource, TResult>
ListWhereSelectIterator<TSource, TResult>traite la chaîneWhere(...).Select(...)sur une liste avec un seul iterator- Cet iterator est créé par la
redéfinition de Select()dansListWhereIterator<TSource, TResult> ListWhereIterator<TSource>est créé lorsqueEnumerable.Where()vérifie si la source est uneList<TSource>ListWhereSelectIterator<TSource, TResult>ne redéfinit pas de méthodes commeTryGetFirst()ouTryGetLast()- Le cœur du gain de performance vient du fait que la chaîne très fréquente
Where(...).Select(...)sur une liste est fusionnée en un seul iterator au lieu de deux- Dans
MoveNext(), les deux delegates_predicateet_selectorsont appelés ensemble
- Dans
Exemple de IListSkipTakeIterator<TSource>
IListSkipTakeIterator<TSource>est un iterator spécialisé créé quand les conditions s’y prêtentMoveNext()utilise_state - 1comme index 0-based dans la liste- Un champ d’index séparé serait plus lisible, mais une valeur biaisée est stockée dans
_stateafin de réduire la taille des champs de l’iterator - L’optimisation de cet iterator consiste à éviter de parcourir inutilement les éléments situés hors de la plage
_minIndexInclusiveà_maxIndexInclusive
Des optimisations obtenues par la seule migration
- Dans .NET 9, plusieurs scénarios LINQ courants deviennent plus rapides
- Pour profiter des améliorations du nouveau runtime, il suffit de migrer puis de recompiler
- LINQ est aussi optimisé d’autres façons
- Pour des cas comme la somme de séquences d’entiers, le SIMD est utilisé là où c’est possible
- Les séquences vides sont détectées plus tôt, ce qui réduit le coût d’énumération
- DeepDotnet videos peut servir de ressource d’apprentissage .NET avec Scott Hanselman et Stephen Toub
1 commentaires
Avis sur Hacker News
À mon avis, la partie la plus utile de LINQ n’est ni la structure d’extension fondée sur les arbres syntaxiques de
IQueryable, ni la syntaxe intégrée au langage, mais les méthodes d’extension deIEnumerableAutrefois, on appelait cela, de façon un peu déroutante, « LINQ to Objects », et cela permet d’écrire du C# de manière concise, dans un style fonctionnel
L’article d’origine traite surtout de l’optimisation de ces méthodes d’extension
Ce n’est qu’après avoir appris Haskell que cette approche m’a vraiment parlé, et elle partage aussi certains des avantages et pièges de Haskell, comme l’évaluation paresseuse
Utilisée sans discernement, elle peut produire du code obscur et lent ; je ne la recommanderais donc pas si personne dans l’équipe ne connaît les idiomes fonctionnels de base et l’évaluation paresseuse
IEnumerableetIQueryableC’est plus facile à raisonner et, dans des environnements comme Entity Framework, ce n’est pas toujours le choix le plus rapide, mais c’est généralement une option tout à fait correcte
J’aime aussi utiliser Dapper plutôt qu’EF
Cela dit, les projets C# ont tendance à empiler un nombre absurde de couches d’abstraction, et le développement « enterprise » est généralement pénible à regarder
La nomenclature est parfois un peu non standard, mais tout ce dont on a besoin est là
Eric Lippert a écrit une excellente série d’articles expliquant les monades en lien avec LINQ : https://ericlippert.com/2013/04/02/monads-part-twelve/
Je n’aime pas avoir un autre langage « intégré » à l’intérieur du langage hôte, et le résultat final doit de toute façon revenir à du C#
Même lorsqu’on n’utilise pas d’ORM comme Entity Framework ou Dapper, je préfère placer la logique d’accès aux données, SQL compris, dans un projet abstrait séparé
Ainsi, elle ne se répand pas dans toute l’application et on peut la remplacer si un autre SGBDR devient nécessaire
En 20 ans, cela ne m’est arrivé qu’une seule fois, mais bon
Quand des développeurs juniors utilisent LINQ, leur mettre un profiler et un débogueur entre les mains aide à comprendre ce qui se passe en interne
Il est parfois aussi utile de leur faire écrire d’abord la solution avec une boucle
foret de la logique C# ordinaire, puis de la comparer à l’implémentation LINQ, afin de voir les avantages et les inconvénients des deux approchesLa syntaxe de requête n’est pas codée en dur exclusivement pour
IEnumerable: c’est simplement son comportement par défaut, et on peut l’utiliser presque partoutElle fonctionne un peu comme la surcharge d’opérateurs
[1] : https://github.com/acple/ParsecSharp/blob/da8d0cb9ec39e28dd9...
Je ne comprends pas pourquoi l’équipe dotnet n’investit pas davantage de ressources et de temps dans l’outillage
Il faudrait des doctests et de la génération de documentation, la possibilité d’écrire de meilleurs tests unitaires, plus rapides, à côté du code réel, un meilleur accès au code source, un environnement où appuyer sur F12 ne nécessite pas de décompiler une DLL, et un hub centralisé pour les packages et la documentation comme
pkg.go.devoudocs.rsLa plupart des packages NuGet n’ont soit aucune documentation, soit seulement un README GitHub, soit un court wiki
D’autres écosystèmes comme Rust, Go, Java ou Python ont des années-lumière d’avance sur ce point
Mais, de façon terrifiante, j’ai aussi l’impression que ce n’est pas si loin de la vérité
Exemple : https://learn.microsoft.com/en-us/dotnet/api/system.string.s...
Pour le code source des packages NuGet, c’est aussi facile si Source Link est activé, mais la fonctionnalité est encore relativement récente et tous les packages ne l’ont pas encore adoptée
J’imagine que la plupart du code C# est encore écrit en entreprise en source fermé
Si Microsoft continue dans la direction plus ouverte qu’on observe ces dernières années, cela devrait s’améliorer avec le temps
Certaines de ces fonctionnalités sont fournies par des outils comme Resharper, et je me demande s’il n’existe pas un accord explicite ou implicite pour éviter de marcher sur les plates-bandes des uns et des autres
Honnêtement, la plupart de la documentation que j’ai vue dans des projets C# était de mauvaise qualité, si bien que je finissais par lire le code source
Mon expérience est que, même avec beaucoup d’outils d’autocomplétion, cela aide surtout à écrire, pas vraiment à lire
https://github.com/EWSoftware/SHFB
Je ne sais pas trop si je la recommanderais
J’ai essayé puis je suis revenu en arrière ; les tests semblaient prendre plus longtemps, peut-être parce que la mise en cache des artefacts de build se dégradait
Plutôt que « amélioration des performances de LINQ », il faudrait dire « amélioration des performances de leur propre implémentation de
List»Microsoft semble passer son temps à améliorer les parties dont ils ont besoin, plutôt qu’à apporter des améliorations générales
LINQ, en particulier la syntaxe de requête plutôt que les extensions de méthodes, a besoin d’investissements
Il faut surtout réduire les allocations de lambdas et, si possible, simplifier les lambdas au moment de la compilation
Il faudrait des lambdas locales de type valeur, ou une stratégie pour que l’allocation de lambdas n’ait plus le même surcoût qu’aujourd’hui
Les variables LINQ devraient aussi désormais prendre en charge le joker (
_), mais cela a été complètement ignoré lors de son introduction dans les lambdasDe plus, en dernier élément d’une expression LINQ, on devrait pouvoir utiliser des types relevés comme
IEnumerableouOptionau lieu deselect ...Dans certains cas d’usage,
selectcrée un surcoût inutile et limite aussi des choses comme les expressions LINQ en récursion terminaleLes bibliothèques comme la mienne, qui misent tout sur LINQ mais n’utilisent pas
IEnumerable,IQueryableni les extensions LINQ, continuent d’être ignoréesParce que Microsoft se concentre uniquement sur l’amélioration des performances de ses propres projets
Un bon exemple est l’inférence de lambdas améliorée
Elle a été accélérée parce qu’elle était nécessaire aux Minimal APIs d’ASP.NET Core
Une grande partie des fonctionnalités du langage et du framework semble guidée davantage par les besoins internes de Microsoft que par ceux de la communauté
Le pire, c’est que l’ensemble des méthodes magiques, comme
Select,SelectMany,Wherepour les extensions LINQ, mais aussiGetAwaiter, continue de s’étendreAu lieu d’ajouter les traits de kinds d’ordre supérieur qui sont réellement nécessaires pour dissiper cette magie, Microsoft ajoute des fonctionnalités pour ses propres besoins, principalement ceux du compilateur
Résultat, tout reste faiblement typé, et le compilateur ne peut détecter les choses que de façon approximative
LINQ est l’un des grands différenciateurs du langage, mais il a été quasiment laissé à l’abandon depuis C# 3
Il est vraiment dommage qu’on voie encore LINQ comme utile seulement pour parcourir des listes, surtout leur propre implémentation de listes
Les améliorations de performances en elles-mêmes sont appréciables et aideront beaucoup d’utilisateurs, mais le focus reste toujours étroit et limite le potentiel
[1] https://github.com/louthy/language-ext/
dotnet/runtimeBeaucoup des améliorations de performances LINQ abordées dans l’article sont arrivées de cette manière
Il y a beaucoup d’instructions
using, ce qui n’est pas un gros problème pour quelqu’un qui comprend comment découper finement les projets selon les besoins et séparer les préoccupationsMais la plupart des développeurs ne structurent pas leurs projets ainsi, et ce genre de petit détail peut devenir un obstacle pour le développeur moyen
Les développeurs juniors ont déjà souvent du mal avec la syntaxe et les méthodes LINQ standard, en particulier aussi avec les performances
C’est bien que le README mentionne ce point
D’habitude, les bibliothèques cherchent surtout à se « vendre », et j’apprécie que celle-ci explique réellement où elle est forte et quel est son objectif
Le fait qu’elle soit présentée comme non idiomatique peut aussi poser problème à quelqu’un qui apprend C#/.NET
Microsoft veut probablement que les outils et le langage suivent certaines pratiques, et une nomenclature naturellement alignée sur la programmation fonctionnelle peut être vue comme un obstacle assez important lorsque Microsoft envisage des améliorations
J’ai mis une étoile au dépôt, et ce que vous avez créé m’intéresse beaucoup
Dans quelques grosses applications que j’ai faites récemment, j’ai utilisé un type
Resultqui ressemble grosso modo àOptionMais en regardant à nouveau la bibliothèque, je me rends compte que je pensais assez bien connaître la programmation fonctionnelle, alors qu’en réalité ce n’est pas le cas
Je suis plutôt bon en C# et j’ai fait des choses complexes, mais malgré beaucoup de lectures, la programmation fonctionnelle reste difficile à maîtriser, et F# For Fun And Profit est ce qui m’a paru le plus compréhensible
Au final, cela ne veut pas dire que vous faites quelque chose de mal
Microsoft visera probablement les développeurs moyens ou débutants, qui constituent la majorité de son écosystème
J’espère que cette bibliothèque pourra bénéficier d’améliorations internes
On voit que vous y avez consacré énormément de temps, et le simple nombre d’étoiles GitHub suffit à indiquer qu’il y a des gens qui l’utilisent vraiment et en tirent profit
Désolé si cela a pu sonner comme du mépris, mais le travail réalisé est intéressant, et la documentation semble suffisante pour apprendre progressivement des concepts que je ne connais pas
Plus C# empruntera à F#, mieux ce sera
J’attends que les unions discriminées arrivent enfin dans C#, pour pouvoir faire correctement de la modélisation de domaine
À chaque fois, la question qui me vient est : « pourquoi ne pas simplement utiliser F# ? »
C# essaie de rattraper son retard depuis des années
Si F# porte beaucoup d’innovations dans l’écosystème .NET et a plusieurs années d’avance côté fonctionnalités, je me demande pourquoi cet effort n’est pas récompensé par l’usage
Si vous voulez orienter le développement d’un langage dans une certaine direction, il faut l’encourager par de vrais choix
Cela a fonctionné ainsi dans l’écosystème Java, et Java s’améliore désormais lui aussi
Quand le marché grandit, une boucle de rétroaction positive peut se créer, avec davantage d’efforts d’ingénierie
Après des années à lire les forums, j’ai fortement l’impression que le camp C# veut « simplement » rester dans son camp et attendre
Cela ressemble un peu à du tribalisme, comme si l’équipe était « C# »
Je vois rarement ce genre de culture dans d’autres écosystèmes de langages, et j’ai l’impression que si F# avait appartenu à un autre écosystème que .NET, il aurait peut-être prospéré depuis longtemps
Cela rendrait la maintenance du code d’ingénierie ou scientifique beaucoup plus simple
Exemple d’utilisation concrète : https://chrlschn.dev/blog/2024/07/csharp-discriminated-union...
[0] https://github.com/mcintyre321/OneOf
[1] https://github.com/domn1995/dunet
Ce qui me manque le plus quand je travaille dans d’autres langages ou écosystèmes, c’est LINQ.
Avoir ce genre de fonctionnalité dans la bibliothèque standard, c’est vraiment appréciable, et c’est conçu avec élégance compte tenu des contraintes imposées.
Il y a une section à ce sujet dans l’article annuel, long comme un livre, qui couvre toutes les améliorations de performance de .NET 9.
https://devblogs.microsoft.com/dotnet/performance-improvemen...
Curieusement, HN n’a pas autorisé une nouvelle soumission, donc l’article n’est jamais arrivé en page d’accueil et a été enterré.
Une fois qu’on s’est habitué à LINQ, et qu’on travaille généralement dans des domaines où LINQ brille, on n’a plus envie de revenir à autre chose.
LINQ vous retiendra, et vous finirez par en vouloir aux environnements qui n’en disposent pas.
Je me demande s’il existe un livre ou un tutoriel complet de qualité pour apprendre le développement web de bout en bout avec dotnet.
Ce que j’ai trouvé était pour la plupart trop basique, trop ancien ou de faible qualité.
Personnellement, je pense qu’il suivra la même trajectoire que Silverlight.
Les anciennes technologies sont toujours présentes dans .NET 9, fonctionnent encore et sont maintenues.
Faire du développement web en .NET aujourd’hui consiste généralement à créer des API HTTP/JSON/REST et à les connecter au framework frontend de son choix.
Dans mon cas, j’utilise React ou NextJS.
Les bons mots-clés de recherche sont ASP.NET WebApi, ou plus moderne, ASP.NET Minimal API.
Le rendu côté serveur en .NET MVC avec Razor reste également possible.
Comme c’est le langage de balisage d’ASP.NET MVC, il faut chercher “ASP.NET MVC Razor”.
Pour le meilleur ou pour le pire, ASP.NET semble être de facto la bonne réponse pour créer des applications web en .NET.
Le manque d’alternatives est un peu suspect, certes.
J’ai écouté un podcast avec Andrew Lock, l’auteur de “ASP.NET Core in Action”, et il avait l’air de bien maîtriser le sujet.
Je n’ai pas encore lu le livre, mais c’est peut-être celui que vous cherchez.
1: https://dotnetcore.show/season-6/navigating-the-aspnet-core-...
2: https://www.manning.com/books/asp-net-core-in-action-third-e...
Côté serveur, on peut faire tourner Giraffe au-dessus d’ASP.NET ; c’est une couche de programmation fonctionnelle avec des performances proches de celles de C#.
Côté frontend, on peut écrire du React dans un vrai langage de programmation fonctionnelle.
Évidemment, on peut aussi partager du code F# entre le frontend et le backend.
Côté livres, il y a “C# 12 and .NET 8 - Modern Cross-Platform Development Fundamentals - Eighth Edition: Start building websites and services with ASP.NET Core 8, Blazor, and EF Core 8” de Mark J Price, ainsi que “Web API Development with ASP.NET Core 8: Learn techniques, patterns, and tools for building high-performance, robust, and scalable web APIs” de Xiaodi Yan.
Pour les tutoriels, il y a les séries YouTube de IAmTimCorey et Shawn Wildermuth.
Pour une combinaison backend .NET et frontend JS, cherchez des ressources sur Minimal API.
MVC est très bien aussi, mais il porte beaucoup de bagage de rétrocompatibilité, ce qui a conduit à l’apparition de Minimal API.
Il doit y avoir mieux que cet amas de spaghettis d’annotations.
Chaque fois que je regarde du code .NET moderne, j’ai mal aux yeux.
Le code de tests unitaires et de benchmarks finit souvent par ressembler plus ou moins à des spaghettis.
Cela dit, je ne laisserais pas passer une PR qui ferait ça dans de la vraie logique métier.
Si vous détestez vraiment ça, vous pouvez aussi utiliser des choses comme AspNetCore sans toucher au moindre attribut.
Personnellement, j’utilise très peu les attributs.
Le fait que davantage d’optimisations soient possibles quand une chaîne se termine par des méthodes comme
Count(),First(),Last(),ElementAt()ouSum(), et que par exempleOrderBy(criteria).First()puisse être optimisé pour s’exécuter commeMin(criteria), peut être utile.Mais il vaut mieux écrire du meilleur code dès le départ.
Pour des chaînes générées dynamiquement, c’est intéressant, mais dans du code écrit à la main, ce genre d’opération ressemble à une forme de renforcement positif un peu tordu.
La bibliothèque reconnaît un motif inefficace et le corrige à votre place.
J’espère au minimum qu’il existe un retour suggérant d’améliorer le code sous-jacent.