c# .net performance stack stackalloc

C#&.NET: stackalloc



performance (2)

Tengo algunas preguntas sobre la funcionalidad del operador stackalloc .

  1. ¿Cómo se asigna realmente? Pensé que hace algo como:

    void* stackalloc(int sizeInBytes) { void* p = StackPointer (esp); StackPointer += sizeInBytes; if(StackPointer exceeds stack size) throw new StackOverflowException(...); return p; }

    Pero he hecho algunas pruebas, y no estoy seguro de que funcione así. No podemos saber exactamente qué hace y cómo lo hace, pero quiero saber lo básico.

  2. Pensé que la asignación de pila (Bueno, en realidad estoy segura ) es más rápida que la asignación de pila. Entonces, ¿por qué este ejemplo:

    class Program { static void Main(string[] args) { Stopwatch sw1 = new Stopwatch(); sw1.Start(); StackAllocation(); Console.WriteLine(sw1.ElapsedTicks); Stopwatch sw2 = new Stopwatch(); sw2.Start(); HeapAllocation(); Console.WriteLine(sw2.ElapsedTicks); } static unsafe void StackAllocation() { for (int i = 0; i < 100; i++) { int* p = stackalloc int[100]; } } static void HeapAllocation() { for (int i = 0; i < 100; i++) { int[] a = new int[100]; } } }

da los resultados promedio de 280 ~ ticks para la asignación de pila , y generalmente 1-0 ticks para la asignación de montón? (En mi computadora personal, Intel Core i7).

En la computadora que estoy usando ahora (Intel Core 2 Duo), los resultados tienen más sentido que los anteriores (Probablemente porque el código de Optimize no se verificó en VS): 460 ~ ticks para la asignación de pila , y alrededor de 380 ticks para la asignación de montón .

Pero esto todavía no tiene sentido. ¿Por que es esto entonces? Supongo que el CLR se da cuenta de que no usamos la matriz, ¿entonces tal vez ni siquiera la asigna?


  1. No puedo dar una respuesta exacta, pero stackalloc se implementa utilizando el localloc código de operación IL. Miré el código de máquina generado por una compilación de lanzamiento para stackalloc y fue más complicado de lo que esperaba. No sé si localloc verificará el tamaño de la pila como lo indica por usted o si la CPU detecta el desbordamiento de la pila cuando la pila de hardware realmente se desborda.

    Los comentarios a esta respuesta indican que el enlace proporcionado a localloc asigna espacio desde "el montón local". El problema es que no hay una buena referencia en línea para MSIL, excepto el estándar real disponible en formato PDF. El enlace anterior es de la clase System.Reflection.Emit.OpCodes que no se trata de MSIL sino de una biblioteca para generar MSIL.

    Sin embargo, en el documento de normas ECMA 335 - Infraestructura de lenguaje común hay una descripción más precisa:

    Parte de cada estado del método es un conjunto de memoria local. La memoria se puede asignar explícitamente desde el conjunto de memoria local usando la instrucción localloc . Toda la memoria en el conjunto de memoria local se reclama al salir del método, y esa es la única forma en que se reclama la memoria del conjunto de memoria local (no se ha proporcionado ninguna instrucción para liberar memoria local que se asignó durante esta invocación del método). La agrupación de memoria local se utiliza para asignar objetos cuyo tipo o tamaño no se conoce en el momento de la compilación y que el programador no desea asignar en el montón administrado.

    Básicamente, el "conjunto de memoria local" es lo que se conoce como "la pila" y el lenguaje C # usa el operador stackalloc para asignar desde este conjunto.

  2. En una versión de lanzamiento, el optimizador es lo suficientemente inteligente como para eliminar completamente la llamada a HeapAllocation lo que HeapAllocation resultado un tiempo de ejecución mucho menor. Parece que no es lo suficientemente inteligente como para realizar la misma optimización cuando se usa stackalloc . Si desactivas la optimización o si de alguna manera usas el búfer asignado, verás que stackalloc es un poco más rápido.


Un caso donde stackalloc es más rápido:

private static volatile int _dummy; // just to avoid any optimisations // that have us measuring the wrong // thing. Especially since the difference // is more noticable in a release build // (also more noticable on a multi-core // machine than single- or dual-core). static void Main(string[] args) { System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch(); Thread[] threads = new Thread[20]; sw1.Start(); for(int t = 0; t != 20; ++t) { threads[t] = new Thread(DoSA); threads[t].Start(); } for(int t = 0; t != 20; ++t) threads[t].Join(); Console.WriteLine(sw1.ElapsedTicks); System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch(); threads = new Thread[20]; sw2.Start(); for(int t = 0; t != 20; ++t) { threads[t] = new Thread(DoHA); threads[t].Start(); } for(int t = 0; t != 20; ++t) threads[t].Join(); Console.WriteLine(sw2.ElapsedTicks); Console.Read(); } private static void DoSA() { Random rnd = new Random(1); for(int i = 0; i != 100000; ++i) StackAllocation(rnd); } static unsafe void StackAllocation(Random rnd) { int size = rnd.Next(1024, 131072); int* p = stackalloc int[size]; _dummy = *(p + rnd.Next(0, size)); } private static void DoHA() { Random rnd = new Random(1); for(int i = 0; i != 100000; ++i) HeapAllocation(rnd); } static void HeapAllocation(Random rnd) { int size = rnd.Next(1024, 131072); int[] a = new int[size]; _dummy = a[rnd.Next(0, size)]; }

Diferencias importantes entre este código y el de la pregunta:

  1. Tenemos varios hilos en ejecución. Con la asignación de pila, están asignando en su propia pila. Con la asignación de montón, se están asignando desde un montón compartido con otros subprocesos.

  2. Tamaños más grandes asignados.

  3. Diferentes tamaños asignados cada vez (aunque sembré el generador aleatorio para hacer las pruebas más deterministas). Esto hace que sea más probable que ocurra la fragmentación del montón, lo que hace que la asignación del montón sea menos eficiente que con asignaciones idénticas cada vez.

Además de esto, también vale la pena señalar que stackalloc usaría a menudo como una alternativa al uso de stackalloc para fijar una matriz en el montón. Fijar arrays es malo para el rendimiento del montón (no solo para ese código, sino también para otros subprocesos que usan el mismo montón), por lo que el impacto en el rendimiento sería aún mayor si la memoria reclamada estuviera en uso durante un período de tiempo razonable.

Si bien mi código demuestra un caso en el que stackalloc proporciona un beneficio de rendimiento, en la pregunta probablemente esté más cerca de la mayoría de los casos en que alguien podría "optimizar" con entusiasmo al usarlo. Es de esperar que las dos piezas de código juntas muestren que stackalloc completo puede dar un impulso, también puede afectar mucho el rendimiento también.

En general, ni siquiera debería considerar stackalloc menos que vaya a necesitar usar la memoria stackalloc para interactuar con el código no administrado de todos modos, y debería considerarse una alternativa a una solución fixed lugar de una alternativa a la asignación general de pilas. El uso en este caso aún requiere precaución, previsión antes de comenzar y creación de perfiles después de que termine.

El uso en otros casos podría dar un beneficio, pero debería estar muy por debajo de la lista de mejoras de rendimiento que probaría.

Editar:

Para contestar la parte 1 de la pregunta. Stackalloc es conceptualmente mucho como lo describe. Obtiene una parte de la memoria de la pila y luego devuelve un puntero a esa parte. No verifica que la memoria se ajuste como tal, sino que si intenta obtener memoria al final de la pila, que está protegida por .NET en la creación de subprocesos, esto hará que el sistema operativo devuelva una excepción al tiempo de ejecución. , que luego se convierte en una excepción administrada .NET. Lo mismo sucede si simplemente asigna un solo byte en un método con recursión infinita, a menos que la llamada se haya optimizado para evitar esa asignación de pila (a veces es posible), entonces un solo byte eventualmente se sumará lo suficiente para desencadenar la excepción de desbordamiento de pila.