-.NET 9.0 reduce significativamente el tiempo de ejecución frente a .NET 8 en varios escenarios comunes de LINQ y, en algunos benchmarks, incluso elimina las asignaciones
- Una de las mejoras clave es obtener un
ReadOnlySpan<T>conTryGetSpan()al recorrer arreglos oList<T>, para reducir el costo de iteración TryGetSpan()identificaTSource[]yList<TSource>mediante comparación de tipos, pero la forma de tomar un span del arreglo interno deList<T>es una optimización de la familia Unsafe que puede invalidarse si cambia la capacidad- El LINQ de .NET 9 reconoce cadenas de llamadas comunes y crea iterators especiales, además de aplicar optimizaciones adicionales en métodos terminales como
Count(),First(),Last(),ElementAt()ySum() - Con solo migrar y recompilar ya se pueden obtener algunas mejoras de rendimiento de LINQ, y también se incluyen optimizaciones como uso de SIMD y detección temprana de secuencias vacías
Por qué ahora es más rápido recorrer arreglos y listas
- El primer benchmark guarda
Enumerable.Range(1, 10_000).ToArray()comoIEnumerable<int>y luego ejecutaCount,All,Any,First,SingleyLastpara comparar .NET 8 con .NET 9 - Se usa BenchmarkDotNet, y el proyecto debe apuntar a
net8.0;net9.0y compilarse en modo Release - En .NET 9 el tiempo de ejecución de varios métodos baja mucho y también desaparecen las asignaciones
LinqCount: de 16,198.490 ns a 3,043.563 ns, de 32 B asignados a ninguna asignaciónLinqAny: de 17,096.735 ns a 2,483.927 ns, de 32 B asignados a ninguna asignaciónLinqFirst: de 15,289.747 ns a 2,243.341 ns, de 32 B asignados a ninguna asignaciónLinqSingle: de 21,684.114 ns a 4,884.329 ns, de 32 B asignados a ninguna asignaciónLinqAll: de 10.588 ns a 2.562 ns, de 32 B asignados a ninguna asignaciónLinqLast: de 15.967 ns a 6.918 ns
La diferencia que genera TryGetSpan()
- La causa principal de la mejora de rendimiento es el uso de
TryGetSpan() - Si el enumerable que se recorre es un arreglo o una lista,
TryGetSpan()devuelve unReadOnlySpan<T>que permite iterar más rápido - El código de bifurcación clave comprueba
source.GetType() == typeof(TSource[])osource.GetType() == typeof(List<TSource>)y luego obtiene el span- Los arreglos se manejan con
Unsafe.As<TSource[]>(source) - Las listas usan
CollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source))para obtener un span desde el arreglo interno
- Los arreglos se manejan con
- En el código
source.GetType()se llama dos veces y no se resuelve con casting más verificación de null, pero es una decisión tomada por expertos en rendimiento de .NET considerando las optimizaciones del compilador de C# y del JIT - En el stack de .NET altamente optimizado, las microoptimizaciones pueden dar resultados distintos a lo que aparentan
Restricciones de CollectionsMarshal.AsSpan()
List<TSource>internamente referencia un arreglo, y cuando la capacidad de la lista debe crecer o reducirse crea un arreglo nuevo y pasa a referenciarloCollectionsMarshal.AsSpan(Unsafe.As<List<TSource>>(source))obtiene unSpan<TSource>desde ese arreglo interno- Si la capacidad de la lista cambia de cualquier manera, el arreglo obtenido por este método puede invalidarse
- Por esta limitación, algunas operaciones de
Enumerableque incluyen iteración diferida, comoyield, no pueden depender fácilmente de esta optimización - El propio nombre
System.Runtime.CompilerServices.Unsafeya deja ver ese riesgo
Alcance de las llamadas a TryGetSpan()
- Con NDepend se escaneó
System.Linq.dllpara verificar los llamadores directos e indirectos deTryGetSpan() - La ruta del ensamblado analizado es
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.0-rc.1.24431.7\System.Linq.dll - Desde
TryGetSpan()se generó una consulta de código para identificar llamadores y se exportaron 56 métodos coincidentes a un grafo de dependencias - Muchos métodos estándar de
Enumerableintentan hacer recorrido por span cuando la colección es un arreglo o una lista - Aun así, como tomar el arreglo interno de una lista no es seguro, siguen existiendo límites en operaciones que requieren ejecución diferida
Optimización basada en iterators especiales
- El segundo benchmark usa un caso del PR Consolidate LINQ’s internal IIListProvider/IPartition into base Iterator class
- Entre los casos probados están
Distinct().First(),Append().Select().Last(),Reverse().Count(),DefaultIfEmpty().Select().ElementAt(),Skip().Take().ElementAt(),Union().First()ySelect().Where().Select().Sum() - En .NET 9 algunas cadenas de llamadas se vuelven extremadamente rápidas
DistinctFirst: de 65.318 ns a 11.192 ns, de 328 B asignados a ninguna asignaciónAppendSelectLast: de 4,122.007 ns a 2.661 ns, de 144 B asignados a ninguna asignaciónDefaultIfEmptySelectElementAt: de 4,090.818 ns a 5.724 ns, de 144 B asignados a ninguna asignaciónRangeUnionFirst: de 66.309 ns a 6.193 ns, de 344 B asignados a ninguna asignaciónListSkipTakeElementAt: de 6.268 ns a 2.916 nsRangeReverseCount: de 11.024 ns a 6.134 ns
- En cambio,
SelectWhereSelectSumse volvió más lento: pasó de 3,959.622 ns en .NET 8 a 4,460.008 ns en .NET 9, y además mantuvo la asignación de 112 B
Reconocimiento de cadenas comunes de LINQ
- El equipo de rendimiento de .NET diseñó el código para reconocer cadenas comunes de llamadas LINQ
- Cuando detecta una cadena específica, crea un iterator especial que procesa el flujo de trabajo de manera más eficiente
- Si la cadena termina en métodos como
Count(),First(),Last(),ElementAt()oSum(), se pueden aplicar optimizaciones adicionales - Por ejemplo,
OrderBy(criteria).First()puede optimizarse para ejecutarse comoMin(criteria)
Estructura de Iterator<T> y sus clases derivadas
- Dentro de LINQ existe la clase base abstracta
Iterator<T>y 40 clases derivadas - Todas estas clases están anidadas dentro de la clase
Enumerable Iterator<T>es una clase abstracta, pero sus métodos son virtuales, así que las clases derivadas solo sobrescriben los métodos que necesitan- Esta estructura sirve como base para encapsular comportamientos especiales según cada cadena de llamadas
Caso de ListWhereSelectIterator<TSource, TResult>
ListWhereSelectIterator<TSource, TResult>procesa la cadenaWhere(...).Select(...)sobre listas en un solo iterator- Este iterator se crea desde el
overridedeSelect()deListWhereIterator<TSource, TResult> ListWhereIterator<TSource>se crea cuandoEnumerable.Where()verifica que la fuente seaList<TSource>ListWhereSelectIterator<TSource, TResult>no sobrescribe métodos comoTryGetFirst()oTryGetLast()- La clave de la mejora de rendimiento es que la cadena tan común
Where(...).Select(...)sobre listas se fusiona en un solo iterator, en lugar de usar dos- Dentro de
MoveNext()se invocan juntos los dos delegates_predicatey_selector
- Dentro de
Caso de IListSkipTakeIterator<TSource>
IListSkipTakeIterator<TSource>es un iterator especial que se crea cuando aplicaMoveNext()usa_state - 1como índice base 0 de la lista- Tener un campo de índice separado sería más fácil de leer, pero para reducir el tamaño de los campos del iterator se almacena con bias dentro de
_state - La optimización de este iterator consiste en no recorrer innecesariamente elementos fuera del rango
_minIndexInclusivey_maxIndexInclusive
Optimizaciones adicionales que se obtienen solo con migrar
- En .NET 9 varios escenarios comunes de LINQ se vuelven más rápidos
- Para aprovechar las mejoras de la nueva versión de .NET, lo único necesario es migrar y recompilar
- LINQ también se optimiza de otras formas
- En casos como la suma de secuencias de enteros, usa SIMD cuando es posible
- Las secuencias vacías se detectan antes, reduciendo el costo de enumeración
- DeepDotnet videos puede verse como material de aprendizaje de .NET con Scott Hanselman y Stephen Toub
1 comentarios
Opiniones de Hacker News
Creo que la parte más útil de LINQ no es la estructura extensible basada en árboles de sintaxis de
IQueryable, ni la sintaxis integrada en el lenguaje, sino los métodos de extensión deIEnumerableAntes se le llamaba, de forma algo confusa, “LINQ to Objects”, y permite escribir C# de manera concisa con un estilo funcional
El artículo original trata principalmente sobre la optimización de estos métodos de extensión
Recién después de aprender Haskell entendí bien este enfoque, y también comparte algunas de las ventajas y trampas de Haskell, como la evaluación diferida
Si se usa sin criterio, puede terminar en código difícil de entender y lento, así que no lo recomendaría si en el equipo no hay alguien que conozca los modismos funcionales básicos y la evaluación diferida
IEnumerableeIQueryableEs más fácil razonar sobre ellas y, en lugares como Entity Framework, no siempre son la opción más rápida, pero en general son una alternativa bastante buena
También me gusta usar Dapper más que EF
Dicho eso, los proyectos en C# tienden a acumular una cantidad absurda de capas de abstracción, y el desarrollo “enterprise” suele ser bastante doloroso de ver
Hay algunos nombres un poco poco estándar, pero tiene todo lo necesario
Eric Lippert escribió una excelente serie de artículos explicando las mónadas en relación con LINQ: https://ericlippert.com/2013/04/02/monads-part-twelve/
No me gusta que haya otro lenguaje “integrado” dentro del lenguaje anfitrión, y porque el resultado final de todos modos tiene que volver a C#
Incluso cuando no se usan ORM como Entity Framework o Dapper, conviene dejar la lógica de acceso a datos, incluido SQL, en un proyecto separado y abstraído
Así no se dispersa por toda la aplicación y se puede reemplazar si alguna vez se necesita otro RDBMS
Aunque en 20 años eso solo me pasó una vez
Cuando un desarrollador junior usa LINQ, ponerle un profiler y un depurador ayuda a entender qué está ocurriendo por dentro
A veces también es útil pedirle que lo escriba primero con un bucle
fory lógica C# común, y luego compararlo con una implementación en LINQ, para ver las ventajas y desventajas de ambos enfoquesLa sintaxis de consulta no está hardcodeada exclusivamente para
IEnumerable; ese es solo su comportamiento predeterminado, y puede usarse casi en cualquier ladoFunciona de forma algo parecida a la sobrecarga de operadores
[1]: https://github.com/acple/ParsecSharp/blob/da8d0cb9ec39e28dd9...
No entiendo por qué el equipo de dotnet no invierte más recursos y tiempo en las herramientas
Hace falta doctest y generación de documentación, una forma de escribir pruebas unitarias mejores y más rápidas junto al código real, accesibilidad al código fuente, un entorno en el que al presionar F12 no haya que descompilar un DLL, y un hub central de paquetes y documentación como
pkg.go.devodocs.rsLa mayoría de los paquetes de NuGet no tienen documentación alguna, o solo tienen un README de GitHub, o una wiki breve
Otros entornos como Rust, Go, Java y Python están a años luz en este aspecto
Pero, de forma horrible, también siento que eso está bastante cerca de la verdad
Ejemplo: https://learn.microsoft.com/en-us/dotnet/api/system.string.s...
Para el código fuente de paquetes NuGet también es fácil si se activa Source Link, pero todavía es una función relativamente nueva y no todos los paquetes la han adoptado
Aún parece que la mayoría del código C# se escribe en empresas como código cerrado
Si Microsoft sigue avanzando en la dirección abierta de estos últimos años, creo que mejorará con el tiempo
Algunas de estas funciones las ofrecen herramientas como Resharper, y me pregunto si no habrá algún acuerdo explícito o implícito para no pisarse los terrenos
Sinceramente, la mayoría de la documentación que he visto en proyectos C# era de baja calidad, así que terminaba mirando el código fuente
Mi experiencia es que, aunque haya muchas herramientas de autocompletado, no ayudan mucho a leer, solo a escribir
https://github.com/EWSoftware/SHFB
No estoy seguro de recomendarla
La probé y luego la revertí; parecía que los tests tardaban más, quizá porque empeoraba el caché de artefactos de compilación
Más que “mejoras de rendimiento de LINQ”, sería más correcto decir “mejoras de rendimiento de su propia implementación de
List”Parece que Microsoft dedica tiempo a mejorar lo que ellos necesitan, más que a mejoras generales
LINQ, en especial la sintaxis de consulta y no las extensiones de métodos, necesita inversión
Principalmente hace falta reducir las asignaciones de lambdas y, si es posible, simplificar las lambdas en tiempo de compilación
Se necesita una estrategia para que las lambdas locales de tipos por valor, o las asignaciones de lambdas, no sean una sobrecarga como lo son ahora
Las variables de LINQ también deberían soportar comodines (
_) a estas alturas, pero cuando se introdujeron en las lambdas se ignoró por completoAdemás, como último elemento de una expresión LINQ debería poder usarse un tipo elevado como
IEnumerableuOption, en vez deselect ...En ciertos casos de uso,
selectintroduce una sobrecarga innecesaria y también limita cosas como expresiones LINQ con recursión de colaLas bibliotecas que, como la mía, apuestan todo por LINQ pero no usan
IEnumerable,IQueryableni extensiones LINQ siguen siendo ignoradasPorque Microsoft se concentra solo en mejorar el rendimiento de sus propios proyectos
Un buen ejemplo es la inferencia de lambdas mejorada
Se adelantó porque era necesaria para las Minimal API de ASP.NET Core
Da la impresión de que buena parte de las funciones del lenguaje y del framework se mueven más por necesidades internas de Microsoft que por las de la comunidad
Lo peor es que sigue creciendo el conjunto de métodos mágicos, no solo las extensiones LINQ
Select,SelectManyyWhere, sino también cosas comoGetAwaiterEn lugar de incorporar las características de kind de orden superior que realmente hacen falta para eliminar esta magia, Microsoft agrega funciones para ellos mismos, sobre todo para el compilador
Por eso todo queda con tipos débiles, y el compilador apenas puede detectarlo de forma aproximada
LINQ es uno de los diferenciadores clave entre lenguajes, pero desde C# 3 ha quedado casi abandonado
Es realmente una lástima que todavía se vea a LINQ como algo útil solo para recorrer listas, en especial para recorrer su propia implementación de listas
Agradezco las mejoras de rendimiento en sí, y ayudarán a muchos usuarios, pero el foco siempre es tan estrecho que limita su potencial
[1] https://github.com/louthy/language-ext/
dotnet/runtimeo enviar un PRMuchas de las mejoras de rendimiento de LINQ tratadas en el artículo entraron de esa manera
Hay muchas sentencias
using, aunque eso no es un gran problema para quien entiende cómo dividir un proyecto en unidades necesarias y separar responsabilidadesPero la mayoría de los desarrolladores no estructuran así sus proyectos, y detalles pequeños como este pueden convertirse en una barrera para el desarrollador promedio
Los desarrolladores junior muchas veces ya tienen dificultades con la sintaxis y los métodos estándar de LINQ, especialmente también con el rendimiento
Está bien que el README mencione esto
Normalmente las bibliotecas están ocupadas en “venderse”, así que me gusta que realmente indique dónde es fuerte y cuál es su propósito
La mención de que no es idiomática también puede ser un problema para quienes están aprendiendo C#/.NET
Microsoft probablemente quiera que las herramientas y el lenguaje sigan ciertas prácticas, y los nombres alineados de forma natural con la programación funcional podrían verse como un obstáculo bastante grande cuando Microsoft evalúe mejoras
Le puse estrella al repositorio y me interesa mucho lo que construiste
En algunas aplicaciones grandes que hice recientemente usé un tipo
Resultque parece más o menos similar aOptionPero al volver a mirar la biblioteca, sentí que, aunque pensaba que sabía bastante de programación funcional, en realidad no era así
Soy bastante bueno en C# y he hecho cosas complejas, pero la programación funcional sigue siendo difícil de dominar aunque lea mucho; F# For Fun And Profit fue lo que mejor pude entender
En definitiva, no significa que estés haciendo algo mal
Microsoft apuntará al desarrollador promedio o principiante que constituye la mayor parte de su ecosistema
Ojalá esta biblioteca pueda obtener mejoras internas
Se nota que le dedicaste muchísimo tiempo, y con solo ver la cantidad de estrellas en GitHub ya hay señales suficientes de que hay gente que realmente la usa y recibe ayuda de ella
Si sonó como un comentario despectivo, lo siento, pero el trabajo que hiciste es interesante y parece tener documentación suficiente como para ir aprendiendo poco a poco conceptos que no conozco
Cuanto más tome C# de F#, mejor
Estoy esperando que las uniones discriminadas por fin lleguen a C# para poder hacer modelado de dominio como corresponde
Cada vez que pasa, me surge la pregunta: “¿por qué no simplemente usar F#?”
C# lleva años tratando de ponerse al día
Si F# ha impulsado muchas innovaciones del ecosistema .NET y además estuvo años adelantado en funciones, me pregunto por qué no recompensan ese esfuerzo usándolo
Si hay una dirección de desarrollo del lenguaje que quieren, deberían incentivarla con elecciones reales
En el ecosistema Java eso funcionó de esa manera, y ahora Java también está mejorando
Si el mercado crece, puede generarse un ciclo de retroalimentación positiva en el que también aumente el esfuerzo de ingeniería
Al leer foros durante años, da una fuerte impresión de que el bando de C# quiere “simplemente” quedarse en su lado y esperar
Se ve un poco tribalista, como si el equipo fuera “C#”
No he visto mucho esta cultura en otros ecosistemas de lenguajes, y me da la impresión de que si F# hubiera estado en otro ecosistema que no fuera .NET, quizá habría prosperado hace mucho
Harían mucho más fácil mantener código de ingeniería o científico
Ejemplo de uso real: https://chrlschn.dev/blog/2024/07/csharp-discriminated-union...
[0] https://github.com/mcintyre321/OneOf
[1] https://github.com/domn1995/dunet
Lo que más extraño cuando trabajo en otros lenguajes o ecosistemas es LINQ.
Es realmente genial que una biblioteca estándar tenga una funcionalidad así, y está diseñado de forma elegante dentro de las restricciones dadas.
Hay una sección relacionada en el artículo anual, del tamaño de un libro, que cubre todas las mejoras de rendimiento de .NET 9.
https://devblogs.microsoft.com/dotnet/performance-improvemen...
Curiosamente, HN no permitió volver a enviarlo, así que el artículo no llegó a la portada y quedó enterrado.
Una vez que te acostumbras a LINQ y normalmente trabajas en dominios donde LINQ brilla, ya no quieres volver a hacerlo de otra manera.
LINQ los atrapará y terminarán resentidos con los entornos que no lo tienen.
Me pregunto si habrá algún libro o tutorial integral bueno para aprender desarrollo web de punta a punta con dotnet.
La mayoría de lo que encontré era demasiado básico, viejo o de baja calidad.
Personalmente, creo que seguirá el mismo camino que Silverlight.
Las tecnologías antiguas siguen estando en .NET 9, funcionan y se mantienen.
Hoy en día, hacer desarrollo web con .NET por lo general significa crear APIs HTTP/JSON/REST y conectarlas con el framework de frontend que prefieras.
En mi caso uso React o NextJS.
Buenos términos de búsqueda son ASP.NET WebApi o, de forma más moderna, ASP.NET Minimal API.
También se puede seguir haciendo renderizado del lado del servidor con .NET MVC usando Razor.
Como es el lenguaje de marcado de ASP.NET MVC, puedes buscar “ASP.NET MVC Razor”.
Para bien o para mal, en .NET la forma de crear aplicaciones web parece ser, en la práctica, ASP.NET.
Aunque la falta de alternativas resulta un poco sospechosa.
Escuché un podcast con Andrew Lock, autor de “ASP.NET Core in Action”, y parecía alguien que domina el tema.
Todavía no leí el libro, pero podría ser el que estás buscando.
1: https://dotnetcore.show/season-6/navigating-the-aspnet-core-...
2: https://www.manning.com/books/asp-net-core-in-action-third-e...
En el servidor puedes correr Giraffe sobre ASP.NET, que es una capa de programación funcional con un rendimiento similar al de C#.
En el frontend puedes escribir React con un lenguaje de programación funcional de verdad.
Por supuesto, también puedes compartir código F# entre el frontend y el backend.
Como libros están “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, y “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.
Para tutoriales, están las series de IAmTimCorey y Shawn Wildermuth en YouTube.
Si buscas combinar un backend .NET con un frontend JS, busca materiales que usen Minimal API.
MVC también está bien, pero carga con mucho lastre de compatibilidad hacia atrás, y por eso surgió Minimal API.
Tiene que haber una forma mejor que este montón de fideos de anotaciones.
Cada vez que veo código .NET moderno me duelen los ojos.
El código de pruebas unitarias y benchmarking suele verse algo espagueti.
Aun así, no aprobaría un PR que se viera así en lógica de negocio real.
Si de verdad los odias, también puedes usar cosas como AspNetCore sin tocar ni un solo atributo.
Yo casi no uso atributos.
La parte sobre que se pueden hacer más optimizaciones cuando la cadena termina con métodos como
Count(),First(),Last(),ElementAt(),Sum(), y que, por ejemplo,OrderBy(criteria).First()puede optimizarse para ejecutarse comoMin(criteria), podría ser útil.Pero lo correcto sería escribir mejor código desde el principio.
Sería interesante para cadenas generadas dinámicamente, pero si haces estas operaciones en código escrito a mano, se siente como una especie de refuerzo positivo torcido.
La biblioteca estaría reconociendo patrones ineficientes y corrigiéndolos.
Como mínimo, espero que haya feedback que sugiera mejorar el código subyacente.