← Todos los artículos

.NET 11: cómo medir las mejoras reales de JIT, JSON, URI y Native AOT

El recorrido oficial de rendimiento de .NET 11 reúne cientos de cambios. Explicamos sus microbenchmarks y cómo comprobar si una mejora llega a tu backend.

17 de septiembre de 2026 · · 13 min de lectura
Ilustración abstracta de un pipeline de ejecución con bloques de código, engranajes, llaves de código, un enlace, un chip y un indicador de medición sobre un fondo azul oscuro

El equipo de .NET publicó el 15 de septiembre de 2026 el recorrido técnico Performance Improvements in .NET 11, firmado por Stephen Toub. El artículo reúne cientos de cambios pequeños y grandes en el runtime y en las bibliotecas: comprobaciones que desaparecen, menos asignaciones, rutas de sistema operativo más estrechas, barreras de memoria más precisas, SIMD y llamadas que dejan de ser necesarias.

La idea importante no es que todas las aplicaciones se vuelvan un porcentaje concreto más rápidas. Es que una mejora del JIT o de una biblioteca puede aparecer automáticamente cuando el código de una aplicación coincide con el patrón optimizado. Para saber si eso importa en un backend hay que conectar tres niveles de evidencia: el cambio técnico, el microbenchmark que lo aísla y una prueba de carga del servicio que lo utiliza.

Estado exacto de .NET 11

La consulta del canal oficial 11.0 devuelve:

Elemento Valor observado Qué significa
Última versión del canal 11.0.0-rc.1 Es una release candidate, no la versión final
Runtime 11.0.0-rc.1.26425.128 Runtime usado por el ejemplo oficial
SDK 11.0.100-rc.1.26425.128 SDK asociado a RC1
Fecha de release 8 de septiembre de 2026 Fecha de publicación de RC1
Fase go-live Microsoft ofrece una licencia de soporte go-live para esta RC
Tipo de release sts Canal de soporte estándar de la familia .NET

El JSON oficial de metadatos del canal 11.0 es la referencia para los identificadores exactos. El anuncio de RC1 explica la licencia go-live y enlaza las notas de la versión. Go-live permite probar y usar la release bajo las condiciones de Microsoft; no equivale a decir que .NET 11 ya sea la versión estable final.

Esta distinción importa para los resultados: el artículo de rendimiento compara .NET 10 con una RC, y una RC posterior o la versión final puede modificar tanto el código como las cifras. Antes de repetir el experimento, registra dotnet --info, el sistema operativo, la arquitectura, la CPU, la configuración del GC y el identificador exacto de cada runtime.

Qué está midiendo Microsoft

El recorrido oficial crea un proyecto de consola en un directorio nuevo y lo hace multi-target para net10.0 y net11.0. El proyecto habilita AllowUnsafeBlocks y ServerGarbageCollection, fija los paquetes System.* al runtime que corresponde a cada destino y usa BenchmarkDotNet 0.16.0-preview.1.

El comando habitual es:

dotnet run -c Release -f net10.0 --filter "*" --runtimes net10.0 net11.0

El csproj de la publicación contiene, de forma resumida, esta configuración:

<PropertyGroup>
  <TargetFrameworks>net11.0;net10.0</TargetFrameworks>
  <LangVersion>preview</LangVersion>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  <ServerGarbageCollection>true</ServerGarbageCollection>
  <SystemPackageVersion Condition="'$(TargetFramework)' == 'net10.0'">10.0.12</SystemPackageVersion>
  <SystemPackageVersion Condition="'$(TargetFramework)' == 'net11.0'">11.0.0-rc.1.26425.128</SystemPackageVersion>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="BenchmarkDotNet" Version="0.16.0-preview.1" />
</ItemGroup>

La preview de BenchmarkDotNet es parte del montaje del experimento, no una recomendación general para cualquier proyecto. Si se reproduce el benchmark, conviene conservar esa versión para acercarse al entorno de Microsoft y, en otro experimento, repetir con una versión estable compatible para comprobar que la conclusión no depende del arnés.

Un microbenchmark intenta que una operación domine la medición. Eso es útil para responder “¿esta ruta aislada hace menos trabajo?”, pero deja fuera red, TLS, DNS, serializadores adicionales, middleware, bases de datos, colas, contención y el patrón de concurrencia de un servicio. Además, sus resultados dependen de la CPU, el sistema operativo, la arquitectura, el estado térmico, la afinidad, el GC y la carga de la máquina. La ratio 0.26 significa que el tiempo observado en ese caso fue aproximadamente el 26 % del baseline; no significa que una API completa vaya a ser 3,9 veces más rápida.

JIT: menos coste de abstracción cuando el patrón es demostrable

El JIT transforma el IL en instrucciones nativas. Una optimización en esta capa puede beneficiar código de la aplicación y de las bibliotecas sin que el equipo tenga que rediseñar la API. .NET 11 continúa el trabajo de deabstracción: cuando puede demostrar que una llamada virtual, una conversión de interfaz o una asignación no tiene efectos observables en ese contexto, puede especializar la ruta, reutilizar hechos de tipos y abrir oportunidades de inlining.

La consecuencia práctica es potencial, no universal. Dos métodos que parecen equivalentes en C# pueden producir código diferente por el tipo concreto conocido, los límites de la llamada, el tamaño del método y la arquitectura. El JIT también cambia su estrategia por CPU: una mejora que usa AVX-512, AVX10.2 o SVE no se traslada automáticamente a un host sin esas capacidades.

Para investigar una ruta propia:

  1. Aísla la operación en un benchmark que no incluya I/O y evita que el resultado pueda eliminarse.
  2. Añade MemoryDiagnoser si las asignaciones son parte de la pregunta.
  3. Usa DisassemblyDiagnoser solo cuando necesites explicar por qué cambió el resultado; el ensamblador no es un sustituto de la prueba de carga.
  4. Ejecuta la misma fuente contra los dos runtimes y registra la arquitectura real.
  5. Comprueba tanto una entrada favorable como entradas pequeñas, vacías y adversariales.

El post muestra numerosos ejemplos de generación de código, incluidos casos en los que se eliminan copias, se agrupan escrituras de estructuras o se utilizan instrucciones vectoriales. Son indicios de cómo se construye la ganancia acumulada: no un contrato de rendimiento para cualquier método que use una abstracción parecida.

JSON: escanear solo lo que hace falta

Utf8JsonWriter tiene que localizar comillas, caracteres de control y otros valores que no puede copiar directamente al JSON. En .NET 11, la ruta del codificador predeterminado usa conjuntos SearchValues precomputados para buscar directamente los caracteres que necesitan escape. La ayuda que escribe la secuencia escapada recibe además el rango que se sabe escribible, de modo que el JIT puede retirar comprobaciones repetidas.

En la lectura, los documentos indentados pueden contener largas secuencias de espacios y saltos de línea. Utf8JsonReader pasa a usar IndexOfAnyExcept con un conjunto de los cuatro bytes de espacio en blanco JSON para saltar tramos completos.

Microsoft publica estos dos ejemplos:

Caso .NET 10 .NET 11 RC1 Ratio de .NET 11
Serializar una cadena formada por 2.048 comillas 30,83 µs 7,875 µs 0,26
Leer el payload indentado del ejemplo 7,418 µs 5,945 µs 0,80

El primer caso fuerza una entrada completamente escapada; el segundo favorece la ruta que salta espacios. Ninguno representa por sí solo el JSON de una API. Para una medición útil, prepara al menos tres familias:

// Compacto: se parece al transporte habitual de una API.
var compacto = JsonSerializer.SerializeToUtf8Bytes(modelo);

// Indentado: sirve para probar secuencias largas de espacio y salto de línea.
var indentado = JsonSerializer.SerializeToUtf8Bytes(
    modelo,
    new JsonSerializerOptions { WriteIndented = true });

// Texto con escapes: fuerza comillas repetidas que requieren escape.
var conEscapes = new string('"', 2048);

Mide la operación que quieras mejorar —escritura, lectura o ambas— con tamaños de payload y tipos de contrato representativos. Registra media, error, asignaciones y percentiles del benchmark. En la aplicación, verifica si el cambio modifica CPU por solicitud, bytes asignados, presión de gen0 y p95/p99; un serializador más rápido puede quedar oculto si la base de datos o la red domina el tiempo.

URI: menos temporales en entradas que requieren normalización

El análisis oficial usa como ejemplo una dirección con path, query, fragmento y un carácter Unicode. Al construir una Uri, el runtime tiene que encontrar delimitadores y validar si cada componente ya está en forma canónica, necesita escapar caracteres o requiere una normalización diferente.

En .NET 11, la detección de delimitadores usa rutas basadas en IndexOfAny y SearchValues para examinar tramos más largos. El cambio más visible para entradas no ASCII separa la reconstrucción de la validación: path, query y fragment se normalizan en un builder, se crea una cadena final y después se validan los límites. Así se evitan varias cadenas temporales en ciertos casos Unicode.

En el benchmark de normalización publicado por Microsoft:

Caso .NET 10 .NET 11 RC1 Ratio Asignaciones
Parseo de URI con Unicode en el fragment 547,6 ns 386,9 ns 0,71 936 B → 432 B

El mismo post muestra casos ASCII y con percent-encoding en los que el tiempo y las asignaciones cambian de otra forma. Si el servicio procesa URLs, separa el coste de new Uri(...) del de una petición HTTP: no mezcles parseo, DNS, conexión, TLS y lectura de respuesta en un único número.

Una matriz mínima debe incluir hosts largos, rutas cortas, query extensa, caracteres Unicode, % ya codificado y entradas inválidas. Comprueba también qué parte se ejecuta una sola vez (por ejemplo, configuración) y qué parte ocurre por solicitud.

Diagnóstico: un callback observable no necesita una matriz temporal

Las métricas de System.Diagnostics.Metrics pueden ser observables: una MeterListener pide el valor actual mediante un callback. Ese callback puede devolver un valor único, una Measurement<T> o una secuencia de mediciones etiquetadas.

En .NET 10, las dos formas de valor único se normalizaban internamente al modelo enumerable. Cada recolección creaba una matriz de una sola posición y la enumeraba. .NET 11 reconoce esas formas y notifica directamente el valor, manteniendo el camino enumerable para los callbacks que realmente devuelven varias mediciones.

El benchmark oficial, con listener activo y una gauge que devuelve un int, registra:

Método .NET 10 .NET 11 RC1 Ratio Asignaciones
Registrar el instrumento observable 17,16 ns 4,511 ns 0,26 72 B → 0 B

La mejora es relevante para una ruta de observabilidad que se consulta con frecuencia, pero no es “OpenTelemetry gratis”. El exporter, la agregación, las etiquetas, el transporte, la frecuencia de colección y el backend de métricas tienen sus propios costes. Mide por separado:

  • instrumento sin listener;
  • listener activo con callback de un valor;
  • callback que devuelve varias mediciones;
  • proveedor/exporter real;
  • servicio bajo carga con la telemetría que se habilita en producción.

El post también muestra reducciones en rutas de logging JSON. Ese resultado debe leerse con la misma cautela: el logger, los filtros, el listener y el destino alteran la medición.

ProcessName y Native AOT: una mejora condicionada al sistema operativo y al artefacto

En Linux y macOS, consultar solo ProcessName en .NET 10 activaba la recopilación del objeto completo de información del proceso. .NET 11 añade una consulta más estrecha para ese nombre. En el ejemplo de Microsoft, la media pasa de 332,13 µs a 11,90 µs (ratio 0,04).

Es un caso dependiente de sistema operativo: el comando del benchmark se ejecuta en Linux o macOS. No extrapoles ese número a Windows ni a una aplicación que necesita obtener otros datos del proceso.

La misma separación entre caminos locales y remotos ayuda a Native AOT. Las APIs locales de Process dejan de conservar una referencia innecesaria a la implementación de procesos remotos. Si una aplicación solo usa APIs locales, el trimmer puede demostrar que parte de esa maquinaria y sus dependencias son inalcanzables.

El ejemplo oficial publica una aplicación muy pequeña para win-x64, con PublishAot e InvariantGlobalization, y observa estos ejecutables:

Runtime Ejecutable
.NET 10.0 1.599.488 bytes
.NET 11.0 1.326.080 bytes

Ese tamaño pertenece a ese código, RID, sistema operativo y opciones de publicación. Una API con reflexión, serialización, globalización, diagnósticos y dependencias nativas tendrá otra huella. Para evaluar Native AOT en un servicio, publica el artefacto real y revisa warnings de trimming como errores en CI; mide tamaño comprimido, arranque en frío, memoria residente, compatibilidad y comportamiento de actualización.

De la cifra aislada a una decisión de backend

La transición correcta es una cadena de mediciones, no un salto directo desde una tabla de BenchmarkDotNet a una promesa de producción:

microbenchmark reproducible
        ↓
perfil de la ruta en el servicio
        ↓
prueba de carga representativa
        ↓
canario con métricas y rollback

1. Fija los artefactos

Guarda dotnet --info, global.json si existe, imágenes base o digest, RID, arquitectura, opciones del GC y versiones de paquetes. Para acercarte a la publicación oficial, .NET 10 usa 10.0.12 y .NET 11 RC1 usa 11.0.0-rc.1.26425.128; una comparación posterior debe declarar cualquier diferencia.

2. Repite con datos propios

No uses un único payload. Conserva entradas pequeñas, medianas, grandes, vacías, Unicode, inválidas y con escapes. En URI, no mezcles parseo con red. En métricas, activa el mismo listener y exporter que el servicio. En Native AOT, usa el RID de despliegue y la configuración final.

3. Mide lo que decide una operación

En el servicio de staging registra throughput, p50, p95 y p99, CPU, memoria residente, asignaciones, colecciones de GC, errores, tiempo de arranque y tamaño de imagen. Compara suficientes muestras y repite en condiciones similares. Una media mejor con p99 peor puede ser una regresión para el usuario.

4. Comprueba compatibilidad

Una mejora del runtime no elimina la necesidad de revisar breaking changes, paquetes, analizadores, serialización, reflexión, diagnósticos y dependencias nativas. En Native AOT, una ruta que funciona bajo JIT puede requerir anotaciones o generación de código para que el trimmer preserve lo necesario.

5. Despliega de forma reversible

Promueve primero un canario o un porcentaje pequeño del tráfico. Define por adelantado umbrales de error, p95/p99, CPU y memoria que detengan la promoción. Conserva el artefacto anterior y una vía de rollback que no dependa de reconstruir durante la incidencia.

La discrepancia C# 14/C# 15

El rendimiento de este artículo no depende de la versión del lenguaje, pero la documentación oficial consultada presenta una inconsistencia que conviene dejar registrada. El JSON de metadatos de .NET 11 lista csharp-version: "14.0" para el SDK de RC1. En cambio, What's new in .NET 11, actualizado el 9 de septiembre, y la tabla de versionado de C# indican que C# 15 es la versión predeterminada para proyectos que apuntan a .NET 11.

No hay base suficiente para elegir una cifra ignorando la otra. La publicación oficial de benchmarks usa <LangVersion>preview</LangVersion> y no necesita características de C# 15 para explicar sus resultados. Por eso, la guía no presenta C# 14 o C# 15 como requisito de rendimiento. Si un proyecto necesita una característica de lenguaje, fija el SDK exacto, comprueba la versión efectiva del compilador en CI y sigue las notas de RC correspondientes; no deduzcas el comportamiento de un proyecto solo a partir de ese campo del JSON.

Conclusión

.NET 11 puede sumar muchas ganancias pequeñas: el JIT elimina trabajo cuando reconoce un patrón, System.Text.Json escanea y escapa con menos comprobaciones, Uri reduce temporales en determinadas entradas, las métricas observables evitan una matriz intermedia y Native AOT puede retirar rutas remotas que una aplicación local nunca utiliza.

Los ejemplos oficiales son valiosos porque muestran cómo encontrar el mecanismo detrás de una mejora. También marcan el límite de la evidencia: son microbenchmarks, en un hardware y una configuración concretos, comparando .NET 10 con una release candidate. La decisión de actualización debe salir del perfil del backend y de una prueba reproducible con carga real. Si el canal 11.0 cambia antes de adoptar la versión, vuelve a consultar el metadato, las notas de release y el artículo de rendimiento.

Fuentes oficiales

Comentarios

Cargando…