memoria - punteros c++
¿Cómo ansioso comprometer la memoria asignada en C++? (1)
Solución actual, pseudo código simplificado:
// During startup
{
SetProcessWorkingSetSize(GetCurrentProcess(), 2*1024*1024*1024, -1);
}
// In the DX11 render loop thread
{
DX11context->Map(..., &resource)
VirtualLock(resource.pData, resource.size);
notify();
wait();
DX11context->Unmap(...);
}
// In the processing threads
{
wait();
std::memcpy(buffer, source, size);
signal();
}
VirtualLock()
obliga al kernel a respaldar inmediatamente el rango de direcciones especificado con RAM. La llamada a la función VirtualUnlock()
complementaria es opcional, sucede implícitamente (y sin costo adicional) cuando el rango de direcciones no se asigna desde el proceso. (Si se llama explícitamente, cuesta aproximadamente 1/3 del costo de bloqueo).
Para que VirtualLock()
funcione en absoluto, SetProcessWorkingSetSize()
debe llamar a SetProcessWorkingSetSize()
, ya que la suma de todas las regiones de memoria bloqueadas por VirtualLock()
no puede exceder el tamaño de conjunto de trabajo mínimo configurado para el proceso. Establecer el tamaño del conjunto de trabajo "mínimo" en algo más alto que la huella de memoria básica de su proceso no tiene efectos secundarios, a menos que su sistema realmente esté potencialmente intercambiando, su proceso no consumirá más RAM que el tamaño real del conjunto de trabajo.
Solo el uso de VirtualLock()
, aunque en subprocesos individuales y el uso de contextos DX11 diferidos para llamadas Map
/ Unmap
, disminuyó instantáneamente la penalización de rendimiento del 40-50% a un 15% ligeramente más aceptable.
Descartar el uso de un contexto diferido, y desencadenar exclusivamente tanto todas las fallas blandas, como la correspondiente desasignación al desasignar en un único subproceso, dio el impulso de rendimiento necesario. El costo total de ese bloqueo de giro ahora se reduce a <1% del uso total de la CPU.
¿Resumen?
Cuando espere fallas suaves en Windows, intente lo que pueda para mantenerlas todas en el mismo hilo. Realizar una memcpy
paralela en sí no es problemático, en algunas situaciones incluso es necesario para utilizar completamente el ancho de banda de la memoria. Sin embargo, eso es solo si la memoria ya está comprometida con la memoria RAM. VirtualLock()
es la forma más eficiente de garantizar eso.
(A menos que trabaje con una API como DirectX que mapee la memoria en su proceso, es poco probable que encuentre memoria no comprometida con frecuencia. Si solo está trabajando con C ++ estándar new
o malloc
su memoria se agrupará y reciclará dentro de su proceso de todos modos, tan suave las fallas son raras.)
Solo asegúrese de evitar cualquier forma de fallas de página simultáneas cuando trabaje con Windows.
La situación general
Una aplicación que es extremadamente intensiva en uso de ancho de banda, uso de CPU y GPU necesita transferir aproximadamente 10-15 GB por segundo de una GPU a otra. Está utilizando la API DX11 para acceder a la GPU, por lo que solo se puede cargar a la GPU con almacenamientos intermedios que requieren el mapeo para cada carga individual. La carga se produce en fragmentos de 25 MB a la vez, y 16 hilos escriben búferes en búferes asignados al mismo tiempo. No hay mucho que se pueda hacer sobre esto. El nivel de concurrencia real de las escrituras debe ser menor, si no fuera por el siguiente error.
Es una estación de trabajo fornida con 3 GPU Pascal, un procesador Haswell de alta gama y RAM de cuatro canales. No se puede mejorar mucho en el hardware. Está ejecutando una edición de escritorio de Windows 10.
El problema real
Una vez que paso ~ 50% de carga de la CPU, algo en MmPageFault()
(dentro del núcleo de Windows, llamado al acceder a la memoria que ha sido mapeada en su espacio de direcciones, pero no fue comprometido por el sistema operativo) se rompe horriblemente, y el 50% restante La carga de la CPU se está desperdiciando en un bloqueo de giro dentro de MmPageFault()
. La CPU se utiliza al 100% y el rendimiento de la aplicación se degrada por completo.
Debo suponer que esto se debe a la inmensa cantidad de memoria que debe asignarse al proceso cada segundo y que también está completamente desasignada del proceso cada vez que no se asigna el búfer DX11. En consecuencia, en realidad se trata de miles de llamadas a MmPageFault()
por segundo, que ocurren de forma secuencial, ya que memcpy()
está escribiendo secuencialmente en el búfer. Para cada página no comprometida encontrada.
Una la carga de la CPU va más allá del 50%, el optimista bloqueo de giro en el kernel de Windows que protege la administración de la página degrada por completo el rendimiento.
Consideraciones
El buffer es asignado por el controlador DX11. No se puede modificar nada acerca de la estrategia de asignación. El uso de una API de memoria diferente y especialmente la reutilización no es posible.
Las llamadas a la API DX11 (asignar / quitar el mapeo de los búferes) ocurren todas a partir de un solo hilo. Las operaciones de copia reales pueden tener múltiples subprocesos en más hilos que procesadores virtuales en el sistema.
La reducción de los requisitos de ancho de banda de memoria no es posible. Es una aplicación en tiempo real. De hecho, el límite estricto es actualmente el ancho de banda PCIe 3.0 16x de la GPU principal. Si pudiera, ya tendría que seguir adelante.
Evitar las copias de subprocesos múltiples no es posible, ya que hay colas independientes productor-consumidor que no se pueden fusionar trivialmente.
La degradación del rendimiento de bloqueo de giro parece ser tan rara (porque el caso de uso lo está empujando tan lejos) que en Google, no encontrará un solo resultado para el nombre de la función de bloqueo de giro.
La actualización a una API que le da más control sobre las asignaciones (Vulkan) está en progreso, pero no es adecuada como solución a corto plazo. El cambio a un kernel de sistema operativo mejor actualmente no es una opción por el mismo motivo.
La reducción de la carga de la CPU tampoco funciona; hay mucho trabajo que debe realizarse aparte de la copia de búfer (generalmente trivial y económica).
La pregunta
¿Qué se puede hacer?
Necesito reducir significativamente el número de páginas individuales. Sé la dirección y el tamaño del búfer que se ha mapeado en mi proceso, y también sé que la memoria no se ha confirmado todavía.
¿Cómo puedo asegurarme de que la memoria se compromete con la menor cantidad posible de transacciones?
Indicadores exóticos para DX11 que evitarían la desasignación de los almacenamientos intermedios después de quitar el mapeo, las API de Windows para forzar el compromiso en una sola transacción, prácticamente todo es bienvenido.
El estado actual
// In the processing threads
{
DX11DeferredContext->Map(..., &buffer)
std::memcpy(buffer, source, size);
DX11DeferredContext->Unmap(...);
}