Novedades del entorno de ejecución de .NET 11

En este artículo se describen las nuevas características del entorno de ejecución de .NET para .NET 11. Se actualizó por última vez para la versión preliminar 4.

Requisitos mínimos de hardware actualizados

Los requisitos mínimos de hardware para .NET 11 se han actualizado para requerir conjuntos de instrucciones más modernos en arquitecturas x86/x64 y Arm64. Además, los destinos de compilación ReadyToRun (R2R) se han actualizado para aprovechar las ventajas de las funcionalidades de hardware más recientes.

Requisitos de Arm64

Para Apple, no hay ningún cambio en el hardware mínimo ni en el objetivo ReadyToRun. Los chips M1 de Apple son aproximadamente equivalentes a armv8.5-a y proporcionan soporte para al menos los AdvSimd conjuntos de instrucciones (NEON), CRC, DOTPROD, LSE, RCPC, RCPC2y RDMA .

Para Linux, no hay ningún cambio en el hardware mínimo. .NET sigue admitiendo dispositivos como Raspberry Pi que solo pueden proporcionar compatibilidad con el conjunto de instrucciones AdvSimd. El objetivo ReadyToRun se ha actualizado para incluir el conjunto de LSE instrucciones, lo que podría resultar en una sobrecarga adicional de jitting si lanzas una aplicación.

Para Windows, la línea base se actualiza para requerir el conjunto de instrucciones LSE. Esto es requerido por Windows 11 y por todas las CPU Arm64 compatibles oficialmente con Windows 10. Además, está alineado con los requisitos de Arm SBSA (Server Base System Architecture). El destino ReadyToRun se ha actualizado para que sea armv8.2-a + RCPC, que proporciona compatibilidad con al menos AdvSimd, , CRCLSE, RCPC, y RDMA, y cubre la mayoría del hardware admitido oficialmente.

Sistema operativo JIT/AOT mínimo anterior Nuevo JIT/AOT mínimo Destino de R2R anterior Nuevo destino de R2R
Manzana Apple M1 (Sin cambios) Apple M1 (Sin cambios)
Linux armv8.0-a (Sin cambios) armv8.0-a armv8.0-a + LSE
Windows armv8.0-a armv8.0-a + LSE armv8.0-a armv8.2-a + RCPC

Requisitos de x86/x64

Para los tres sistemas operativos (Apple, Linux y Windows), la línea base se actualiza de x86-64-v1 a x86-64-v2. Esto cambia el hardware de solo garantizar CMOV, CX8, SSE y SSE2 a también garantizar CX16, POPCNT, SSE3, SSSE3, SSE4.1 y SSE4.2. Esta garantía es requerida por Windows 11 y por todas las CPU x86/x64 compatibles oficialmente con Windows 10. Incluye todos los chips aún admitidos oficialmente por Intel y AMD, con los últimos chips más antiguos que han salido de soporte alrededor de 2013.

El destino ReadyToRun se ha actualizado a x86-64-v3 para Windows y Linux, que además incluye los AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT y MOVBE conjuntos de instrucciones. El destino ReadyToRun para Apple permanece sin cambios.

Sistema operativo JIT/AOT mínimo anterior Nuevo JIT/AOT mínimo Destino de R2R anterior Nuevo destino de R2R
Manzana x86-64-v1 x86-64-v2 x86-64-v2 (Sin cambios)
Linux x86-64-v1 x86-64-v2 x86-64-v2 x86-64-v3
Windows x86-64-v1 x86-64-v2 x86-64-v2 x86-64-v3

Impacto

A partir de .NET 11, .NET no se puede ejecutar en hardware anterior y podría imprimir un mensaje similar al siguiente:

La CPU actual carece de uno o más de los conjuntos de instrucciones de línea base.

En el caso de los ensamblados compatibles con ReadyToRun, puede haber una sobrecarga de inicio adicional en algún hardware compatible que no cumpla la compatibilidad esperada con un dispositivo típico.

Motivo del cambio

.NET admite una amplia gama de hardware, a menudo por encima y más allá de los requisitos mínimos de hardware establecidos por el sistema operativo subyacente. Esta compatibilidad agrega una complejidad significativa al código base, especialmente para hardware mucho más antiguo que es poco probable que todavía esté en uso. Además, define un "mínimo común denominador" al que los objetivos de AOT deben ajustarse por defecto, lo que, en algunos escenarios, puede dar lugar a una reducción del rendimiento.

La actualización a la línea base mínima se realizó para reducir la complejidad de mantenimiento del código base y para alinearse mejor con los requisitos de hardware documentados (y a menudo aplicados) del sistema operativo subyacente.

Para obtener más información, consulte Requisitos mínimos de hardware actualizados.

Sincronización en tiempo de ejecución

.NET 11 presenta la asincronía nativa en tiempo de ejecución (Runtime Async V2), un paso importante para reemplazar las máquinas de estado asincrónicas generadas por el compilador con suspensión administrada en tiempo de ejecución y reanudación. En lugar de que el compilador genere clases de máquina de estados, el entorno de ejecución realiza un seguimiento de la ejecución asincrónica, produciendo seguimientos de pila más limpios, mejor capacidad de depuración y menor sobrecarga.

Runtime Async es una característica en versión preliminar. Para participar, agregue la siguiente propiedad al archivo del proyecto:

<PropertyGroup>
  <Features>runtime-async=on</Features>
</PropertyGroup>

Un net11.0 proyecto ya no requiere <EnablePreviewFeatures>true</EnablePreviewFeatures> usar Async en tiempo de ejecución.

Las bibliotecas en tiempo de ejecución de .NET se compilan con runtime-async=on. Las bibliotecas en tiempo de ejecución ya no contienen máquinas de estado generadas por el compilador y dependen completamente de la asincrónica proporcionada por el entorno de ejecución. Esto permite migrar una aplicación completa (solo con dependencias de biblioteca) al nuevo modelo y proporciona una amplia validación funcional y de rendimiento de la característica. El equipo del producto da la bienvenida a cualquier informe (positivo o negativo) sobre los cambios de rendimiento y tamaño de biblioteca que observe.

.NET 11 también incluye dos mejoras adicionales:

  • Redefiniciones covariantes TaskTask<T>: Cuando una clase derivada devuelve Task<T> para un método base que devuelve Task, el entorno de ejecución ahora genera un thunk con valor de retorno void que salva la diferencia de convención de llamada, por lo que el despacho virtual funciona para ambas variantes. La misma corrección se aplica a NativeAOT.
  • Inserción en crossgen2: Se han quitado las restricciones que impedían que los métodos asincrónicos en tiempo de ejecución se insertaran durante la compilación ReadyToRun (R2R). Todas las pruebas asincrónicas se pasan tanto con crossgen2 como con R2R compuesto, e inclusión de llamadas asincrónicas sin espera (la ruta de acceso rápida sincrónica) se confirma de un extremo a otro.

Nota:

Las DOTNET_RuntimeAsync variables de entorno y UNSUPPORTED_RuntimeAsync que controlaban previamente el comportamiento asincrónico en tiempo de ejecución se han quitado. Para deshabilitar runtime-async para un proyecto, establezca <UseRuntimeAsync>false</UseRuntimeAsync> en el archivo del proyecto en lugar de depender de la variable de entorno.

Pistas de pila vivas más limpias

La mejora más visible está en los rastreosde pila en vivo: qué perfiladores, depuradores y new StackTrace() ven durante la ejecución. Con asíncronas generadas por compiladores, cada método asíncrono produce múltiples tramas a partir de la infraestructura de la máquina de estados. Con Runtime Async, los métodos reales aparecen directamente en la pila de llamadas.

// To enable runtime async, add the following to your .csproj:
//   <Features>runtime-async=on</Features>

await OuterAsync();

static async Task OuterAsync()
{
    await Task.CompletedTask;
    await MiddleAsync();
}

static async Task MiddleAsync()
{
    await Task.CompletedTask;
    await InnerAsync();
}

static async Task InnerAsync()
{
    await Task.CompletedTask;
    Console.WriteLine(new StackTrace(fNeedFileInfo: true));
}

Sin runtime-async—13 fotogramas, la infraestructura de la máquina de estados es visible:

   at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
   at Program.<<Main>$>g__InnerAsync|0_2()
   at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
   at Program.<<Main>$>g__MiddleAsync|0_1()
   at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
   at Program.<<Main>$>g__OuterAsync|0_0()
   at Program.<Main>$(String[] args) in Program.cs:line 3
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](...)
   at Program.<Main>$(String[] args)
   at Program.<Main>(String[] args)

Con runtime-async—5 marcos, la cadena de llamadas real:

   at Program.<<Main>$>g__InnerAsync|0_2() in Program.cs:line 24
   at Program.<<Main>$>g__MiddleAsync|0_1() in Program.cs:line 14
   at Program.<<Main>$>g__OuterAsync|0_0() in Program.cs:line 8
   at Program.<Main>$(String[] args) in Program.cs:line 3
   at Program.<Main>(String[] args)

Nota:

Las trazas de la pila de excepciones (de catch (Exception ex)) ya se ven igual con o sin Async en tiempo de ejecución, porque la limpieza existente ExceptionDispatchInfo en el código generado por compiladores gestiona ese caso. La mejora está en lo que observas durante la ejecución en vivo.

Esta mejora beneficia a cualquier cosa que inspeccione la pila de ejecución en vivo, incluyendo herramientas de perfilado, registros de diagnóstico y la ventana de la pila de llamadas al depurador.

Compatibilidad con NativeAOT y ReadyToRun

Runtime Async admite la compilación NativeAOT y ReadyToRun. Esto extiende la funcionalidad más allá del código compilado JIT a escenarios compilados anticipadamente. El runtime también reutiliza los objetos de continuación de forma más agresiva y evita guardar locales sin cambios, reduciendo la presión de asignación en código asíncrono.

Mejoras en la depuración

Los puntos de interrupción ahora se vinculan correctamente dentro de métodos asíncronos en tiempo de ejecución, y el depurador puede atravesar await los límites sin tener que entrar en una infraestructura generada por el compilador.

Mejoras de JIT

  • Eliminación de la comprobación de límites: El compilador Just-In-Time (JIT) ahora elimina las comprobaciones de límites del patrón común en el que se compara un índice más una constante con una longitud, como i + cns < len. También elimina comprobaciones de límites más redundantes para el acceso indexado desde el extremo (por ejemplo, values[^1]). Estas mejoras reducen las comprobaciones redundantes en bucles ajustados y mejoran el rendimiento de las operaciones de matriz y segmento.
  • Eliminación de contextos comprobados redundantes: El JIT ahora puede demostrar y quitar contextos aritméticos comprobados redundantes, por ejemplo, cuando ya se sabe que un valor está en el intervalo. Esta optimización elimina las comprobaciones de desbordamiento innecesarias en el código generado.
  • Plegamiento de expresiones por interruptor: Las expresiones multi-objetivo switch ahora se pliegan en comprobaciones sin ramificación más simples cuando los objetivos son un conjunto pequeño de constantes, por ejemplo x is 0 or 1 or 2 or 3 or 4.
  • Lanzamiento más rápido de uint-a-float/doble fundición: Casting uint a float o double es más rápido en hardware x86 pre-AVX-512.
  • Devirtualización en imágenes ReadyToRun: Las imágenes ReadyToRun (R2R) ahora pueden desvirtualizar llamadas a métodos virtuales genéricos no compartidos, lo que mejora el rendimiento del código compilado de antemano para escenarios genéricos.
  • Intrínsecos SVE2: Nuevos intrínsecos de SVE2 (Scalable Vector Extension 2) Arm están disponibles: ShiftRightLogicalNarrowingSaturate(Even|Odd). Esto amplía el conjunto de operaciones vectorizadas disponibles en el hardware de Arm que admite SVE2.

Para mejorar el rendimiento y la calidad del código, .NET 11 agrega varias optimizaciones JIT más:

Secuencia de plegado constanteEqual

El JIT ahora puede plegar una llamada a string.Equals o ReadOnlySpan<T>.SequenceEqual cuyos operandos sean ambos constantes de tiempo de compilación, sustituyendo la comparación byte a byte por el resultado constante true o false. Esto importa más después de la inserción, cuando un autor de la llamada pasa otro literal a un asistente que se compara con una cadena conocida. Cuando IsAdmin se integra en una función que llama y pasa "Guest", el JIT ve que "Guest" == "Admin" y lo reduce a false:

static bool IsAdmin(string role) => role == "Admin";

La optimización se aplica a literales de cadena, const string campos y literales UTF-8 (por ejemplo, "PNG"u8).

Eliminación de las comprobaciones de límites después de una protección de intervalos vacíos

El JIT ahora recoge la length != 0 aserción de una comprobación de intervalo vacío y la usa para demostrar que la comprobación de límites en el primer elemento se realiza correctamente:

if (!span.IsEmpty && span[0] == value)
{
    // The bounds check on span[0] is now eliminated.
}

Eliminación de bifurcaciones y pruebas redundantes

Cuando un predicado externo ya está implícito en una rama interna, el JIT ahora quita la comprobación externa redundante. De forma similar, cuando el código asigna un valor en un condicional y, a continuación, lo prueba inmediatamente, se elimina la segunda prueba:

if (x > 0)
{
    if (x > 1) S();  // The outer x > 0 check is folded away.
}

int y = condition ? 1 : 2;
if (y == 1) A(); else B();  // The y == 1 test is eliminated;
                             // each branch of the ternary goes directly to A() or B().

Estas optimizaciones son más visibles después de la inserción, donde los guardias de diferentes métodos llegan al mismo cuerpo compilado.

Intrínsecos de hardware y generación de código

.NET 11 incluye varias nuevas mejoras intrínsecas de hardware y generación de código:

  • Aceleración F16C para Halffloat conversiones en x64: cuando la CPU admite F16C (la mayoría del hardware compatible con AVX2), las conversiones entre Half yfloat/doubleahora usan las instrucciones dedicadas vcvtph2ps/vcvtps2ph en lugar de las llamadas auxiliares.
  • Mejor modelo de costes para SIMD x86/x64: Los costes de ejecución en coma flotante y de tamaño del JIT reflejaban anteriormente los supuestos de la era x87. Los costes actualizados, que reflejan el hardware SSE/AVX moderno, permiten al JIT tomar mejores decisiones sobre la elevación de código y la eliminación de subexpresiones comunes (CSE) en torno al código SIMD.
  • Más rápido DotProduct en AVX: La fase de lowering de las operaciones de tipo Vector128.Dot ahora genera una secuencia mul + permute + add en lugar de vdpps/vdppd cuando AVX está disponible, que es sistemáticamente más rápida.
  • Más rápidas IndexOfAnyAsciiSearcher en Arm64: las versiones de Arm64 de Vector*.Count, IndexOf y LastIndexOf ya no pasan por ExtractMostSignificantBits, lo que supone una mejora del 5 al 50 % en las cargas de trabajo que utilizan estas API en su bucle interno.
  • Arm64 ToScalar para enteros de 64 bits:ToScalar en Vector*<long> y Vector*<ulong> ahora usa fmov en lugar de umov, que es más corto y más rápido en la mayoría de los núcleos.
  • Ampliación de la API de SVE CreateWhile:CreateWhile incorpora variantes con signo, double y single junto con las ya existentes sin signo, completando el conjunto de generación de predicados para los bucles de SVE.
  • Nueva detección de conjuntos de instrucciones SVE2 y Arm64: El tiempo de ejecución ahora notifica SVE_AES, SVE_SHA3, SVE_SM4, SHA3y SM4 como conjuntos de instrucciones independientes, lo que permite Sve2*.IsSupported consultas para esas características en hardware compatible.
  • Corrección de detección de AVX10: La caché de CPUID-bit para AVX10 se sobrescribía mediante una consulta posterior, lo que podría provocar que AVX10 se notificó erróneamente en hardware compatible. Este problema ya está solucionado.

Mejoras de ReadyToRun

Comparer<T>.Default y EqualityComparer<T>.Default ahora están especializados en imágenes ReadyToRun (R2R). Anteriormente, los comparadores predeterminados usaban reflexión que R2R no podía ver con antelación, lo que hacía que las llamadas tuvieran que recurrir al JIT. R2R ahora genera un asistente especializado en la imagen, en línea con el enfoque de NativeAOT, y las pruebas comparativas muestran una mejora de hasta 20× en las operaciones con colecciones que se basan en el comparador predeterminado.

Mejoras en la máquina virtual

  • Despacho de interfaces en caché en plataformas no JIT: En plataformas que carecían de soporte JIT, como iOS, el despacho de interfaces recaía en un costoso camino genérico de corrección. El despacho en caché produce hasta 200 mejoras en código con mucha interfaz en estos objetivos.
  • Guid.NewGuid() en Linux:Guid.NewGuid() en Linux ahora utiliza el getrandom() syscall con caché por lotes en lugar de leer desde /dev/urandom, lo que produce una mejora de aproximadamente un 12% en el rendimiento para la generación de GUID.

Mejoras de WebAssembly

La compatibilidad con Explorador y WebAssembly tiene varias mejoras:

  • Carga de carga de WebCIL: El tiempo de ejecución ahora puede cargar cargas de WebCIL directamente, lo que mejora la compatibilidad con escenarios de implementación basados en explorador.
  • Símbolos de depuración mejorados: La calidad de los símbolos y del seguimiento de pila para la depuración en WebAssembly ha mejorado, lo que facilita el diagnóstico de problemas en las aplicaciones .NET alojadas en el navegador.
  • float[], Span<float>, y ArraySegment<float> marshaling:float[], Span<float>, y ArraySegment<float> ahora se organizan más directamente a través de los límites de JavaScript, reduciendo la sobrecarga para código con mucho interop.

.NET incluye mejoras en CoreCLR-on-WebAssembly:

  • WebCIL V1 es el valor predeterminado para las compilaciones WASM de CoreCLR: El encabezado webCIL compartido obtiene un TableBase campo (28 → 32 bytes). Los lectores Mono y CoreCLR aceptan V0 y V1. WasmObjectWriter de Crossgen2 produce V1 directamente, y las compilaciones del SDK de WASM basadas en CoreCLR establecen de forma predeterminada WasmWebcilVersion en V1.
  • El re-enlace nativo funciona con las aplicaciones WASM de CoreCLR: Una canalización completa basada en Emscripten sustituye los anteriores destinos ficticios. Volver a enlazar dotnet.native.wasm del paquete en tiempo de ejecución e incluir código nativo personalizado a través de NativeFileReference ahora funciona.
  • Minificación de JavaScript en compilaciones de versión de lanzamiento: Las compilaciones de versión de lanzamiento de Browser CoreCLR incluyen JavaScript minimizado.
  • La publicación de NativeAOT para WASM ya no omite los ensamblados satélite de los paquetes: Los ensamblados satélite de los paquetes NuGet ahora se pasan a ILC y se excluyen del resultado de la publicación, lo que corrige la localización de las aplicaciones publicadas con AOT que dependen de paquetes como System.CommandLine.

Compatibilidad de plataforma con más de 1024 CPU

El entorno de ejecución de .NET ahora puede inicializarse en máquinas con más de 1024 procesadores lógicos. Anteriormente, sched_getaffinity se invocaba con el valor por defecto cpu_set_t (limitado a 1024), lo que hacía que fallara la inicialización en servidores con un elevado número de núcleos. El entorno de ejecución ahora asigna dinámicamente el conjunto de procesadores. El GC mantiene su límite de 1024 montones, pero se elimina el límite del número de CPU.

Consulte también