Actualmente estoy trabajando en un programa muy crítico para el rendimiento y una ruta que decidí explorar que puede ayudar a reducir el consumo de recursos fue aumentar el tamaño de la pila de mis hilos de trabajo para poder mover la mayoría de los datos a float[]
los que accederé la pila (usando stackalloc
).
He leído que el tamaño de pila predeterminado para un subproceso es de 1 MB, por lo que para mover todos mis float[]
mensajes tendría que expandir la pila aproximadamente 50 veces (a 50 MB ~).
Entiendo que esto generalmente se considera "inseguro" y no se recomienda, pero después de comparar mi código actual con este método, ¡descubrí un aumento del 530% en la velocidad de procesamiento! Por lo tanto, no puedo pasar por alto esta opción sin más investigación, lo que me lleva a mi pregunta; ¿Cuáles son los peligros asociados con aumentar la pila a un tamaño tan grande (qué podría salir mal) y qué precauciones debo tomar para minimizar tales peligros?
Mi código de prueba
public static unsafe void TestMethod1()
{
float* samples = stackalloc float[12500000];
for (var ii = 0; ii < 12500000; ii++)
{
samples[ii] = 32768;
}
}
public static void TestMethod2()
{
var samples = new float[12500000];
for (var i = 0; i < 12500000; i++)
{
samples[i] = 32768;
}
}
fuente
Marshal.AllocHGlobal
(no se olvideFreeHGlobal
también) para asignar los datos fuera de la memoria administrada? Luego lanza el puntero a afloat*
, y deberías ser ordenado.Respuestas:
Al comparar el código de prueba con Sam, ¡determiné que ambos tenemos razón!
Sin embargo, sobre cosas diferentes:
Dice así:
stack
<global
<heap
. (tiempo de asignación)Técnicamente, la asignación de la pila no es realmente una asignación, el tiempo de ejecución solo se asegura de que una parte de la pila (¿marco?) esté reservada para la matriz.
Sin embargo, recomiendo tener cuidado con esto.
Recomiendo lo siguiente:
( Nota : 1. solo se aplica a los tipos de valor; los tipos de referencia se asignarán en el montón y el beneficio se reducirá a 0)
Para responder a la pregunta en sí: no he encontrado ningún problema con ninguna prueba de gran tamaño.
Creo que los únicos problemas posibles son un desbordamiento de pila, si no tiene cuidado con sus llamadas a funciones y se queda sin memoria al crear su (s) hilo (s) si el sistema se está agotando.
La siguiente sección es mi respuesta inicial. Es incorrecto y las pruebas no son correctas. Se guarda solo como referencia.
¡Mi prueba indica que la memoria asignada a la pila y la memoria global son al menos un 15% más lentas que (toma el 120% del tiempo de) la memoria asignada en el montón para su uso en matrices!
Este es mi código de prueba , y esta es una salida de muestra:
Probé en Windows 8.1 Pro (con la Actualización 1), usando un i7 4700 MQ, en .NET 4.5.1 Probé
con x86 y x64 y los resultados son idénticos.
Editar : Aumenté el tamaño de la pila de todos los hilos 201 MB, el tamaño de la muestra a 50 millones y disminuí las iteraciones a 5.
Los resultados son los mismos que los anteriores :
Sin embargo, parece que la pila se está volviendo más lenta .
fuente
Ese es, con mucho, el mayor peligro que diría. Hay algo muy mal con su punto de referencia, el código que se comporta de manera impredecible generalmente tiene un error desagradable oculto en alguna parte.
Es muy, muy difícil consumir mucho espacio de pila en un programa .NET, excepto por una recursión excesiva. El tamaño del marco de la pila de métodos administrados se establece en piedra. Simplemente la suma de los argumentos del método y las variables locales en un método. Menos los que se pueden almacenar en un registro de CPU, puede ignorar eso ya que hay muy pocos de ellos.
Aumentar el tamaño de la pila no logra nada, solo reservará un montón de espacio de direcciones que nunca se utilizará. No hay ningún mecanismo que pueda explicar un aumento de rendimiento por no usar memoria, por supuesto.
Esto es diferente a un programa nativo, particularmente uno escrito en C, también puede reservar espacio para matrices en el marco de la pila. El vector de ataque de malware básico detrás de los desbordamientos del búfer de pila. También es posible en C #, tendría que usar la
stackalloc
palabra clave. Si está haciendo eso, entonces el peligro obvio es tener que escribir código inseguro que esté sujeto a tales ataques, así como la corrupción aleatoria del marco de la pila. Muy difícil de diagnosticar errores. Hay una contramedida contra esto en jitters posteriores, creo que a partir de .NET 4.0, donde el jitter genera código para poner una "cookie" en el marco de la pila y comprueba si todavía está intacto cuando el método regresa. Accidente instantáneo en el escritorio sin ninguna forma de interceptar o informar el error si eso sucede. Eso es ... peligroso para el estado mental del usuario.El hilo principal de su programa, el iniciado por el sistema operativo, tendrá una pila de 1 MB de forma predeterminada, 4 MB cuando compile su programa dirigido a x64. Un aumento que requiere ejecutar Editbin.exe con la opción / STACK en un evento posterior a la compilación. Por lo general, puede solicitar hasta 500 MB antes de que su programa tenga problemas para comenzar cuando se ejecuta en modo de 32 bits. Los subprocesos también pueden, por supuesto, mucho más fácil, la zona de peligro suele rondar los 90 MB para un programa de 32 bits. Se activa cuando su programa se ha estado ejecutando durante mucho tiempo y el espacio de direcciones se fragmentó a partir de asignaciones anteriores. El uso total del espacio de direcciones ya debe ser alto, en un concierto, para obtener este modo de falla.
Verifique tres veces su código, hay algo muy mal. No puede obtener una aceleración x5 con una pila más grande a menos que escriba explícitamente su código para aprovecharlo. Que siempre requiere un código inseguro. El uso de punteros en C # siempre tiene un don para crear código más rápido, no está sujeto a las verificaciones de los límites de la matriz.
fuente
float[]
afloat*
. La gran pila fue simplemente cómo se logró eso. Una aceleración x5 en algunos escenarios es completamente razonable para ese cambio.Tendría una reserva allí que simplemente no sabría cómo predecirla: permisos, GC (que necesita escanear la pila), etc., todo podría verse afectado. Estaría muy tentado a usar memoria no administrada en su lugar:
fuente
stackalloc
no está sujeta a recolección de basura.stackalloc
, necesita saltar, y es de esperar que lo haga sin esfuerzo, pero el punto que estoy tratando de hacer es que introduce complicaciones / preocupaciones innecesarias OMI,stackalloc
es excelente como un búfer de memoria virtual, pero para un espacio de trabajo dedicado, se espera que asigne un fragmento de memoria en algún lugar, en lugar de abusar / confundir la pila,Una cosa que puede salir mal es que es posible que no obtenga el permiso para hacerlo. A menos que se ejecute en modo de plena confianza, el Framework simplemente ignorará la solicitud de un tamaño de pila más grande (consulte MSDN en
Thread Constructor (ParameterizedThreadStart, Int32)
)En lugar de aumentar el tamaño de la pila del sistema a números tan grandes, sugeriría reescribir su código para que use Iteration y una implementación de pila manual en el montón.
fuente
Las matrices de alto rendimiento podrían ser accesibles de la misma manera que un C # uno normal, pero eso podría ser el comienzo de un problema: considere el siguiente código:
Espera una excepción fuera de límite y esto tiene mucho sentido porque está intentando acceder al elemento 200 pero el valor máximo permitido es 99. Si va a la ruta stackalloc, no habrá ningún objeto envuelto alrededor de su matriz para verificar y Lo siguiente no mostrará ninguna excepción:
Arriba está asignando suficiente memoria para contener 100 flotantes y está configurando la ubicación de memoria sizeof (float) que comienza en la ubicación iniciada de esta memoria + 200 * sizeof (float) para mantener su valor flotante 10. Como era de esperar, esta memoria está fuera del asignada memoria para los flotadores y nadie sabría lo que podría almacenarse en esa dirección. Si tiene suerte, es posible que haya utilizado alguna memoria no utilizada actualmente, pero al mismo tiempo es probable que pueda sobrescribir alguna ubicación que se utilizó para almacenar otras variables. Para resumir: comportamiento de tiempo de ejecución impredecible.
fuente
stackalloc
, en cuyo caso estamos hablando,float*
etc., que no tiene los mismos controles. Se llamaunsafe
por una muy buena razón. Personalmente, estoy perfectamente feliz de usarunsafe
cuando hay una buena razón, pero Sócrates hace algunos puntos razonables.Los lenguajes de microbenchmarking con JIT y GC como Java o C # pueden ser un poco complicados, por lo que generalmente es una buena idea usar un marco existente: Java ofrece mhf o Caliper, que son excelentes, lamentablemente a mi entender C # no ofrece cualquier cosa que se aproxime a esos. Jon Skeet escribió esto aquí, que asumiré ciegamente que se ocupa de las cosas más importantes (Jon sabe lo que está haciendo en esa área; también sí, no te preocupes, en realidad lo comprobé). Ajusté un poco el tiempo porque 30 segundos por prueba después del calentamiento era demasiado para mi paciencia (5 segundos deberían hacerlo).
Entonces, primero los resultados, .NET 4.5.1 en Windows 7 x64: los números denotan las iteraciones que podría ejecutar en 5 segundos, por lo que cuanto más alto, mejor.
x64 JIT:
x86 JIT (sí, eso todavía es un poco triste):
Esto proporciona una aceleración mucho más razonable de a lo sumo 14% (y la mayor parte de la sobrecarga se debe a que el GC tiene que ejecutarse, considérelo como el peor de los casos de manera realista). Sin embargo, los resultados x86 son interesantes, no del todo claro lo que está sucediendo allí.
y aquí está el código:
fuente
12500000
como tamaño, en realidad obtengo una excepción stackoverflow. Pero principalmente se trataba de rechazar la premisa subyacente de que usar código asignado por pila es más rápido en varios órdenes de magnitud. De lo contrario, estamos haciendo la menor cantidad de trabajo posible aquí, y la diferencia ya es solo del 10 al 15%, en la práctica será aún más baja ... esto, en mi opinión, definitivamente cambia toda la discusión.Dado que la diferencia de rendimiento es demasiado grande, el problema apenas está relacionado con la asignación. Es probable que sea causado por el acceso a la matriz.
Desmonté el cuerpo del bucle de las funciones:
TestMethod1:
TestMethod2:
Podemos verificar el uso de la instrucción y, lo que es más importante, la excepción que arrojan en la especificación ECMA :
Excepciones que arroja:
Y
Excepción que arroja:
Como puede ver,
stelem
hace más trabajo en la verificación de rango de matriz y la verificación de tipo. Como el cuerpo del bucle hace poca cosa (solo asigna un valor), la sobrecarga de la verificación domina el tiempo de cálculo. Por eso, el rendimiento difiere en un 530%.Y esto también responde a sus preguntas: el peligro es la ausencia de verificación de rango y tipo de matriz. Esto no es seguro (como se menciona en la declaración de función; D).
fuente
EDITAR: (un pequeño cambio en el código y en la medición produce un gran cambio en el resultado)
Primero ejecuté el código optimizado en el depurador (F5) pero eso estaba mal. Debe ejecutarse sin el depurador (Ctrl + F5). En segundo lugar, el código puede estar completamente optimizado, por lo que debemos complicarlo para que el optimizador no interfiera con nuestra medición. Hice que todos los métodos devolvieran un último elemento en la matriz, y la matriz se llena de manera diferente. También hay un cero adicional en los OP
TestMethod2
que siempre lo hace diez veces más lento.Intenté algunos otros métodos, además de los dos que proporcionaste. El método 3 tiene el mismo código que el método 2, pero se declara la función
unsafe
. El método 4 está utilizando el acceso del puntero a la matriz creada regularmente. El método 5 está utilizando el acceso del puntero a la memoria no administrada, como lo describe Marc Gravell. Los cinco métodos se ejecutan en tiempos muy similares. M5 es el más rápido (y M1 es el segundo más cercano). La diferencia entre el más rápido y el más lento es de alrededor del 5%, que no es algo que me importe.fuente
TestMethod4
vsTestMethod1
es una comparación mucho mejor parastackalloc
.