Codificación para varios núcleos en Xbox 360 y Windows
Durante años, el rendimiento de los procesadores ha aumentado constantemente, y los juegos y otros programas han cosechado los beneficios de este creciente poder sin tener que hacer nada especial.
Las reglas han cambiado. El rendimiento de los núcleos de un solo procesador ahora aumenta muy lentamente, si está en absoluto. Sin embargo, la potencia informática disponible en un equipo o consola típico sigue creciendo. La diferencia es que la mayor parte de esta ganancia de rendimiento ahora proviene de tener varios núcleos de procesador en una sola máquina, a menudo en un solo chip. La CPU de Xbox 360 tiene tres núcleos de procesador en un chip, y aproximadamente el 70 % de los procesadores pc vendidos en 2006 eran de varios núcleos.
Los aumentos en la potencia de procesamiento disponible son tan dramáticos como en el pasado, pero ahora los desarrolladores tienen que escribir código multiproceso para poder usar esta potencia. La programación multiproceso aporta nuevos desafíos de diseño y programación. En este tema se proporcionan algunos consejos sobre cómo empezar a trabajar con la programación multiproceso.
La importancia del buen diseño
El buen diseño de programas multiproceso es fundamental, pero puede ser muy difícil. Si mueves los principales sistemas de juego a diferentes subprocesos, es probable que encuentres que cada subproceso pasa la mayor parte de su tiempo esperando en los otros subprocesos. Este tipo de diseño conduce a una mayor complejidad y un esfuerzo de depuración significativo, sin prácticamente ninguna ganancia de rendimiento.
Cada vez que los subprocesos tienen que sincronizar o compartir datos, existe la posibilidad de daños en los datos, sobrecarga de sincronización, interbloqueos y complejidad. Por lo tanto, el diseño multiproceso debe documentar claramente cada punto de sincronización y comunicación, y debe minimizar tantos puntos como sea posible. Cuando los subprocesos necesiten comunicarse, aumentará el esfuerzo de codificación, lo que puede reducir la productividad si afecta demasiado al código fuente.
El objetivo de diseño más sencillo para multithreading es dividir el código en partes independientes grandes. Si, a continuación, restringe estas piezas para comunicarse solo unas cuantas veces por fotograma, verá una velocidad significativa de multithreading, sin complejidad indebida.
Tareas típicas en subprocesos
Algunos tipos de tareas han demostrado ser fáciles de colocar en subprocesos independientes. La siguiente lista no está pensada para ser exhaustiva, pero debe dar algunas ideas.
Representación
Representación, que puede incluir recorrer el gráfico de escenas o, posiblemente, solo llamar a funciones D3D, a menudo representa un 50 % o más de tiempo de CPU. Por lo tanto, mover la representación a otro subproceso puede tener ventajas significativas. El subproceso de actualización puede rellenar algún tipo de búfer de descripción de representación, que el subproceso de representación puede procesar.
El subproceso de actualización del juego siempre es un fotograma delante del subproceso de representación, lo que significa que toma dos fotogramas antes de que las acciones del usuario aparezcan en la pantalla. Aunque este aumento de la latencia puede ser un problema, el aumento de la velocidad de fotogramas de la división de la carga de trabajo generalmente mantiene aceptable la latencia total.
En la mayoría de los casos, toda la representación se realiza en un único subproceso, pero es un subproceso diferente de la actualización del juego.
La marca D3DCREATE_MULTITHREADED se usa a veces para permitir la representación en un subproceso y la creación de recursos en otros subprocesos; esta marca se omite en Xbox 360 y debes evitar usarla en Windows. En Windows, especificar esta marca obliga a D3D a dedicar una cantidad significativa de tiempo en la sincronización, lo que ralentiza el subproceso de representación.
Descompresión de archivos
Los tiempos de carga siempre son demasiado largos y el streaming de datos en memoria sin afectar a la velocidad de fotogramas puede ser difícil. Si todos los datos se comprimen de forma agresiva en el disco, es menos probable que la velocidad de transferencia de datos desde el disco duro o el disco óptico sean un factor de limitación. En un procesador de un solo subproceso, normalmente no hay suficiente tiempo de procesador disponible para la compresión para ayudar a los tiempos de carga. Sin embargo, en un sistema multiprocesador, la descompresión de archivos usa ciclos de CPU que, de lo contrario, se desperdiciarían; mejora los tiempos de carga y el streaming; y ahorra espacio en el disco.
No use la descompresión de archivos como reemplazo del procesamiento que se debe realizar durante la producción. Por ejemplo, si dedicas un subproceso adicional para analizar datos XML durante la carga de nivel, no estás usando multithreading para mejorar la experiencia del jugador.
Al usar un subproceso de descompresión de archivos, debe seguir usando E/S asincrónica de archivos y lecturas grandes para maximizar la eficacia de lectura de datos.
Gráficos fluff
Hay muchas agradables gráficas que mejoran el aspecto del juego, pero no son estrictamente necesarias. Estos incluyen cosas como animaciones en la nube generadas mediante procedimientos, simulaciones de tela y pelo, ondas de procedimiento, vegetación de procedimientos, más partículas o físicas que no son de juego.
Dado que estos efectos no afectan al juego, no causan problemas de sincronización complicados, pueden sincronizarse con los demás subprocesos una vez por fotograma o menos a menudo. Además, en juegos para Windows estos efectos pueden agregar valor para los jugadores con CPU de varios núcleos, mientras que se omiten en modo silencioso en equipos de núcleo único, lo que proporciona una manera sencilla de escalar en una amplia gama de funcionalidades.
Física
La física a menudo no se puede colocar en un subproceso independiente para ejecutarse en paralelo con la actualización del juego porque la actualización del juego normalmente requiere los resultados de los cálculos físicos inmediatamente. La alternativa para la física multithreading es ejecutarla en varios procesadores. Aunque esto se puede hacer, es una tarea compleja que requiere acceso frecuente a estructuras de datos compartidas. Si puede mantener la carga de trabajo física lo suficientemente baja como para ajustarse al subproceso principal, el trabajo será más sencillo.
Hay disponibles bibliotecas que admiten la ejecución de física en varios subprocesos. Sin embargo, esto puede provocar un problema: cuando el juego ejecuta físicamente, usa muchos subprocesos, pero el resto del tiempo que usa pocos. La ejecución de física en varios subprocesos requerirá abordar esto para que la carga de trabajo se distribuya uniformemente sobre el marco. Si escribe un motor de física multiproceso, debe prestar mucha atención a todas las estructuras de datos, los puntos de sincronización y el equilibrio de carga.
Ejemplos de diseños multiproceso
Los juegos para Windows deben ejecutarse en equipos con diferentes números de núcleos de CPU. La mayoría de las máquinas de juegos todavía tienen un solo núcleo, aunque el número de máquinas de dos núcleos está creciendo rápidamente. Un juego típico para Windows podría dividir su carga de trabajo en un subproceso para actualizar y representar, con subprocesos de trabajo opcionales para agregar funcionalidad adicional. Además, es probable que se usen algunos subprocesos en segundo plano para realizar E/S de archivos y redes. En la figura 1 se muestran los subprocesos, junto con los puntos de transferencia de datos principales.
Figura 1. Diseño de subprocesos en un juego para Windows
Un juego típico de Xbox 360 puede usar subprocesos de software de uso intensivo de CPU adicionales, por lo que podría dividir su carga de trabajo en un subproceso de actualización, un subproceso de representación y tres subprocesos de trabajo, como se muestra en la figura 2.
Ilustración 2. Diseño de subprocesos en un juego para Xbox 360
Con la excepción de la E/S de archivos y las redes, todas estas tareas tienen la posibilidad de ser lo suficientemente intensivos para la CPU como para beneficiarse de estar en su propio subproceso de hardware. Estas tareas también tienen la posibilidad de ser lo suficientemente independientes como para que se puedan ejecutar para un fotograma completo sin comunicarse.
El subproceso de actualización del juego administra la entrada del controlador, la inteligencia artificial y la física, y prepara instrucciones para los otros cuatro subprocesos. Estas instrucciones se colocan en búferes que pertenecen al subproceso de actualización del juego, por lo que no se requiere ninguna sincronización a medida que se generan las instrucciones.
Al final del fotograma, el subproceso de actualización del juego entrega los búferes de instrucciones a los otros cuatro subprocesos y, a continuación, comienza a trabajar en el siguiente fotograma, rellenando otro conjunto de búferes de instrucciones.
Dado que los subprocesos de actualización y representación funcionan entre sí, sus búferes de comunicación simplemente se almacenan en doble búfer: en un momento dado, el subproceso de actualización rellena un búfer mientras el subproceso de representación lee del otro.
Los demás subprocesos de trabajo no están necesariamente vinculados a la velocidad de fotogramas. La descompresión de un fragmento de datos puede tardar mucho menos que un fotograma o puede tardar muchos fotogramas. Incluso el paño y la simulación del cabello pueden no tener que ejecutarse exactamente a la velocidad de fotogramas, ya que las actualizaciones menos frecuentes pueden ser bastante aceptables. Por lo tanto, estos tres subprocesos necesitan estructuras de datos diferentes para comunicarse con el subproceso de actualización y el subproceso de representación. Cada uno necesita una cola de entrada que pueda contener solicitudes de trabajo y el subproceso de representación necesita una cola de datos que pueda contener los resultados generados por los subprocesos. Al final de cada fotograma, el subproceso de actualización agregará un bloque de solicitudes de trabajo a las colas de subprocesos de trabajo. Agregar a la lista una sola vez por fotograma garantiza que el subproceso de actualización minimice la sobrecarga de sincronización. Cada uno de los subprocesos de trabajo extrae las asignaciones de la cola de trabajo lo antes posible, mediante un bucle similar al siguiente:
for(;;)
{
while( WorkQueueNotEmpty() )
{
RemoveWorkItemFromWorkQueue();
ProcessWorkItem();
PutResultInDataQueue();
}
WaitForSingleObject( hWorkSemaphore );
}
Dado que los datos van de los subprocesos de actualización a los subprocesos de trabajo y, a continuación, al subproceso de representación, puede haber un retraso de tres o más fotogramas antes de que algunas acciones la realicen en la pantalla. Sin embargo, si asigna tareas tolerantes a la latencia a los subprocesos de trabajo, esto no debería ser un problema.
Un diseño alternativo sería tener varios subprocesos de trabajo dibujados a partir de la misma cola de trabajo. Esto proporcionaría equilibrio de carga automático y haría que fuera más probable que todos los subprocesos de trabajo permanezcan ocupados.
El subproceso de actualización del juego debe tener cuidado de no dar demasiado trabajo a los subprocesos de trabajo o, de lo contrario, las colas de trabajo pueden crecer continuamente. La forma en que el subproceso de actualización administra esto depende de qué tipo de tareas están haciendo los subprocesos de trabajo.
Multithreading simultáneo y número de subprocesos
Todos los subprocesos no se crean iguales. Dos subprocesos de hardware pueden estar en chips independientes, en el mismo chip o incluso en el mismo núcleo. La configuración más importante para los programadores de juegos que debe tener en cuenta es dos subprocesos de hardware en un núcleo: multiproceso simultáneo (SMT) o tecnología Hyper-Threading (tecnología HT).
Los subprocesos de tecnología SMT o HT comparten los recursos del núcleo de CPU. Dado que comparten las unidades de ejecución, la velocidad máxima de ejecutar dos subprocesos en lugar de uno suele ser de 10 a 20 por ciento, en lugar del 100 por ciento posible de dos subprocesos de hardware independientes.
Más significativamente, los subprocesos de tecnología SMT o HT comparten la instrucción L1 y las memorias caché de datos. Si sus patrones de acceso a memoria no son compatibles, pueden acabar luchando por la memoria caché y provocar muchos errores de caché. En el peor de los casos, el rendimiento total del núcleo de CPU puede disminuir realmente cuando se ejecuta un segundo subproceso. En Xbox 360, este es un problema bastante sencillo. La configuración de Xbox 360 se conoce (tres núcleos de CPU cada uno con dos subprocesos de hardware) y los desarrolladores asignan sus subprocesos de software a subprocesos de CPU específicos y pueden medir para ver si su diseño de subprocesos les proporciona un rendimiento adicional.
En Windows, la situación es más complicada. El número de subprocesos y su configuración variarán de equipo a equipo y la determinación de la configuración es complicada. La función GetLogicalProcessorInformation proporciona información sobre la relación entre diferentes subprocesos de hardware y esta función está disponible en Windows Vista, Windows 7 y Windows XP SP3. Por lo tanto, por ahora tiene que usar la instrucción CPUID y los algoritmos proporcionados por Intel y AMD para decidir cuántos subprocesos "reales" tiene disponibles. Consulte las referencias para obtener más información.
El ejemplo CoreDetection del SDK de DirectX contiene código de ejemplo que usa la función GetLogicalProcessorInformation o la instrucción CPUID para devolver la topología de núcleo de CPU. La instrucción CPUID se usa si GetLogicalProcessorInformation no se admite en la plataforma actual. CoreDetection se puede encontrar en las siguientes ubicaciones:
-
Fuente:
-
Raíz del SDK de DirectX\Samples\C++\Misc\CoreDetection
-
Ejecutable:
-
Raíz del SDK de DirectX\Samples\C++\Misc\Bin\CoreDetection.exe
La suposición más segura es no tener más de un subproceso intensivo de CPU por núcleo de CPU. Tener más subprocesos con un uso intensivo de CPU que los núcleos de CPU ofrece pocas o ninguna ventaja, y aporta la sobrecarga adicional y la complejidad de los subprocesos adicionales.
Creación de subprocesos
La creación de subprocesos es una operación bastante sencilla, pero hay muchos errores potenciales. El código siguiente muestra la manera adecuada de crear un subproceso, esperar a que finalice y, a continuación, limpiarlo.
const int stackSize = 65536;
HANDLE hThread = (HANDLE)_beginthreadex( 0, stackSize,
ThreadFunction, 0, 0, 0 );
// Do work on main thread here.
// Wait for child thread to complete
WaitForSingleObject( hThread, INFINITE );
CloseHandle( hThread );
...
unsigned __stdcall ThreadFunction( void* data )
{
#if _XBOX_VER >= 200
// On Xbox 360 you must explicitly assign
// software threads to hardware threads.
XSetThreadProcessor( GetCurrentThread(), 2 );
#endif
// Do child thread work here.
return 0;
}
Al crear un subproceso, tiene la opción de especificar el tamaño de pila del subproceso secundario o especificar cero, en cuyo caso el subproceso secundario heredará el tamaño de pila del subproceso primario. En Xbox 360, donde las pilas se confirman completamente cuando se inicia el subproceso, especificar cero puede desperdiciar memoria significativa, ya que muchos subprocesos secundarios no necesitarán tanta pila como el elemento primario. En Xbox 360 también es importante que el tamaño de la pila sea un múltiplo de 64 KB.
Si usa la función CreateThread para crear subprocesos, el entorno de ejecución de C/C++ (CRT) no se inicializará correctamente en Windows. Se recomienda usar la función _beginthreadex CRT en su lugar.
El valor devuelto de CreateThread o _beginthreadex es un identificador de subproceso. Este subproceso se puede usar para esperar a que finalice el subproceso secundario, que es mucho más sencillo y mucho más eficaz que girar en un bucle comprobando el estado del subproceso. Para esperar a que finalice el subproceso, simplemente llame a WaitForSingleObject con el identificador de subproceso.
Los recursos del subproceso no se liberarán hasta que finalice el subproceso y se haya cerrado el identificador del subproceso. Por lo tanto, es importante cerrar el identificador de subproceso con CloseHandle cuando haya terminado con él. Si va a esperar a que el subproceso finalice con WaitForSingleObject, asegúrese de no cerrar el identificador hasta que se haya completado la espera.
En Xbox 360, debes asignar explícitamente subprocesos de software a un subproceso de hardware determinado mediante XSetThreadProcessor. De lo contrario, todos los subprocesos secundarios permanecerán en el mismo subproceso de hardware que el primario. En Windows, puede usar SetThreadAffinityMask para sugerir fuertemente al sistema operativo en el que se deben ejecutar los subprocesos de hardware en los que se debe ejecutar el subproceso. Por lo general, esta técnica debe evitarse en Windows, ya que no se sabe qué otros procesos podrían estar ejecutándose en el sistema. Normalmente, es mejor permitir que el programador de Windows asigne los subprocesos a subprocesos de hardware inactivos.
La creación de subprocesos es una operación costosa. Los subprocesos se deben crear y destruir rara vez. Si quiere crear y destruir subprocesos con frecuencia, use un grupo de subprocesos que esperen por el trabajo en su lugar.
Sincronizar subprocesos
Para que varios subprocesos funcionen juntos, debe poder sincronizar subprocesos, pasar mensajes y solicitar acceso exclusivo a los recursos. Windows y Xbox 360 vienen con un amplio conjunto de primitivos de sincronización. Para más información sobre estos primitivos de sincronización, consulte la documentación de la plataforma.
Acceso exclusivo
Obtener acceso exclusivo a un recurso, estructura de datos o ruta de acceso de código es una necesidad común. Una opción para obtener acceso exclusivo es una exclusión mutua, cuyo uso típico se muestra aquí.
// Initialize
HANDLE mutex = CreateMutex( 0, FALSE, 0 );
// Use
void ManipulateSharedData()
{
WaitForSingleObject( mutex, INFINITE );
// Manipulate stuff...
ReleaseMutex( mutex );
}
// Destroy
CloseHandle( mutex );
The kernel guarantees that, for a particular mutex, only one thread at a time can
acquire it.
The main disadvantage to mutexes is that they are relatively expensive to acquire
and release. A faster alternative is a critical section.
// Initialize
CRITICAL_SECTION cs;
InitializeCriticalSection( &cs );
// Use
void ManipulateSharedData()
{
EnterCriticalSection( &cs );
// Manipulate stuff...
LeaveCriticalSection( &cs );
}
// Destroy
DeleteCriticalSection( &cs );
Las secciones críticas tienen una semántica similar a las exclusión mutuas, pero se pueden usar para sincronizarse solo dentro de un proceso, no entre procesos. Su principal ventaja es que se ejecutan aproximadamente veinte veces más rápido que las exclusión mutuas.
Eventos
Si dos subprocesos (quizás un subproceso de actualización y un subproceso de representación) toman turnos mediante un par de búferes de descripción de representación, necesitan una manera de indicar cuándo se han terminado con su búfer determinado. Esto se puede hacer asociando un evento (asignado con CreateEvent) con cada búfer. Cuando un subproceso se realiza con un búfer, puede usar SetEvent para indicarlo y, a continuación, puede llamar a WaitForSingleObject en el evento del otro búfer. Esta técnica extrapola fácilmente al triple almacenamiento en búfer de recursos.
Semáforos
Un semáforo se usa para controlar cuántos subprocesos se pueden ejecutar y se usa normalmente para implementar colas de trabajo. Un subproceso agrega trabajo a una cola y usa ReleaseSemaphore cada vez que agrega un nuevo elemento a la cola. Esto permite liberar un subproceso de trabajo del grupo de subprocesos en espera. Los subprocesos de trabajo simplemente llaman a WaitForSingleObject y, cuando devuelve, saben que hay un elemento de trabajo en la cola para ellos. Además, se debe usar una sección crítica u otra técnica de sincronización para garantizar el acceso seguro a la cola de trabajo compartida.
Evitar SuspendThread
A veces, cuando desea que un subproceso detenga lo que está haciendo, es tentador usar SuspendThread en lugar de los primitivos de sincronización correctos. Esto siempre es una mala idea y puede conducir fácilmente a interbloqueos y otros problemas. SuspendThread también interactúa mal con el depurador de Visual Studio. Evite SuspendThread. Use WaitForSingleObject en su lugar.
WaitForSingleObject y WaitForMultipleObjects
La función WaitForSingleObject es la función de sincronización más usada. Sin embargo, a veces desea que un subproceso espere hasta que se cumplan varias condiciones simultáneamente o hasta que se cumpla uno de un conjunto de condiciones. En este caso, debe usar WaitForMultipleObjects.
Funciones interbloqueadas y programación sin bloqueo
Hay una familia de funciones para realizar operaciones sencillas seguras para subprocesos sin usar bloqueos. Son la familia de funciones interbloqueadas, como InterlockedIncrement. Estas funciones, además de otras técnicas que usan una configuración cuidadosa de marcas, se conocen juntas como programación sin bloqueo. La programación sin bloqueo puede ser extremadamente complicada de hacer correctamente y es sustancialmente más difícil en Xbox 360 que en Windows.
Para obtener más información sobre la programación sin bloqueos, consulta Consideraciones de programación sin bloqueo para Xbox 360 y Microsoft Windows.
Minimizar la sincronización
Algunos métodos de sincronización son más rápidos que otros. Sin embargo, en lugar de optimizar el código eligiendo las técnicas de sincronización más rápidas posibles, normalmente es mejor sincronizar con menos frecuencia. Esto es más rápido que la sincronización con demasiada frecuencia y facilita la depuración de código más sencillo.
Algunas operaciones, como la asignación de memoria, pueden tener que usar primitivos de sincronización para que funcionen correctamente. Por lo tanto, al realizar asignaciones frecuentes del montón compartido predeterminado, se producirá una sincronización frecuente, lo que desperdiciará algún rendimiento. Evitar asignaciones frecuentes o usar montones por subproceso (mediante HEAP_NO_SERIALIZE si usa HeapCreate) puede evitar esta sincronización oculta.
Otra causa de la sincronización oculta es D3DCREATE_MULTITHREADED, lo que hace que D3D en Windows use la sincronización en muchas operaciones. (La marca se omite en Xbox 360).
Los datos por subproceso, también conocidos como almacenamiento local de subprocesos, pueden ser una manera importante de evitar la sincronización. Visual C++ permite declarar variables globales como por subproceso con la sintaxis __declspec(thread).
__declspec( thread ) int tls_i = 1;
Esto proporciona a cada subproceso del proceso su propia copia de tls_i, a la que se puede hacer referencia de forma segura y eficaz sin necesidad de sincronización.
La técnica __declspec(thread) no funciona con archivos DLL cargados dinámicamente. Si usa archivos DLL cargados dinámicamente, deberá usar la familia de funciones TLSAlloc para implementar el almacenamiento local del subproceso.
Destruir subprocesos
La única manera segura de destruir un subproceso es tener el propio subproceso salir, ya sea devolviendo de la función de subproceso principal o haciendo que el subproceso llame a ExitThread o _endthreadex. Si se crea un subproceso con _beginthreadex, debe usar _endthreadex o volver desde la función de subproceso principal, ya que el uso de ExitThread no liberará correctamente recursos de CRT. Nunca llame a la función TerminateThread , porque el subproceso no se limpiará correctamente. Los subprocesos siempre deben suicidarse: nunca deberían ser asesinados.
OpenMP
OpenMP es una extensión de lenguaje para agregar multithreading al programa mediante pragmas para guiar al compilador en bucles de paralelización. OpenMP es compatible con Visual C++ 2005 en Windows y Xbox 360 y se puede usar junto con la administración manual de subprocesos. OpenMP puede ser una manera cómoda de varias partes del código, pero es poco probable que sea la solución ideal, especialmente para los juegos. OpenMP puede ser más aplicable a tareas de producción de ejecución más prolongada, como el procesamiento de arte y otros recursos. Para obtener más información, consulte la documentación de Visual C++ o vaya al sitio web de OpenMP.
Generación de perfiles
La generación de perfiles multiproceso es importante. Es fácil terminar con largos puestos donde los subprocesos están esperando entre sí. Estos puestos pueden ser difíciles de encontrar y diagnosticar. Para ayudarles a identificarlos, considere la posibilidad de agregar instrumentación a las llamadas de sincronización. Un generador de perfiles de muestreo también puede ayudar a identificar estos problemas porque puede registrar información de tiempo sin modificar considerablemente.
Control de tiempo
La instrucción rdtsc es una manera de obtener información de tiempo precisa en Windows. Desafortunadamente, rdtsc tiene varios problemas que hacen que sea una mala elección para su título de envío. Los contadores rdtsc no se sincronizan necesariamente entre CPU, por lo que cuando el subproceso se mueve entre subprocesos de hardware, puede obtener grandes diferencias positivas o negativas. Dependiendo de la configuración de administración de energía, la frecuencia con la que se incrementa el contador rdtsc también puede cambiar a medida que se ejecuta el juego. Para evitar estas dificultades, debe preferir QueryPerformanceCounter y QueryPerformanceFrequency para un tiempo de alta precisión en el juego de envío. Para obtener más información sobre el tiempo, consulta Control de tiempo de juego y Procesadores de varios núcleos.
Depuración
Visual Studio es totalmente compatible con la depuración multiproceso para Windows y Xbox 360. La ventana Subprocesos de Visual Studio permite cambiar entre subprocesos para ver las diferentes pilas de llamadas y variables locales. La ventana subprocesos también le permite inmovilizar y descongelar determinados subprocesos.
En Xbox 360, puedes usar la meta variable @hwthread en la ventana de watch para mostrar el subproceso de hardware en el que se está ejecutando el subproceso de software seleccionado actualmente.
La ventana subprocesos es más fácil de usar si asigna un nombre a los subprocesos significativamente. Visual Studio y otros depuradores de Microsoft permiten asignar un nombre a los subprocesos. Implemente la siguiente función SetThreadName y llámala desde cada subproceso a medida que se inicia.
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // must be 0x1000
LPCSTR szName; // pointer to name (in user address space)
DWORD dwThreadID; // thread ID (-1 = caller thread)
DWORD dwFlags; // reserved for future use, must be zero
} THREADNAME_INFO;
void SetThreadName( DWORD dwThreadID, LPCSTR szThreadName )
{
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = szThreadName;
info.dwThreadID = dwThreadID;
info.dwFlags = 0;
__try
{
RaiseException( 0x406D1388, 0,
sizeof(info) / sizeof(DWORD),
(DWORD*)&info );
}
__except( EXCEPTION_CONTINUE_EXECUTION ) {
}
}
// Example usage:
SetThreadName(-1, "Main thread");
El depurador de kernel (KD) y WinDBG también admiten la depuración multiproceso.
Prueba
La programación multiproceso puede ser complicada y algunos errores multiproceso solo aparecen en raras ocasiones, lo que hace que sean difíciles de encontrar y corregir. Una de las mejores maneras de vaciarlas es probarlas en una amplia gama de equipos, especialmente aquellos con cuatro o más procesadores. El código multiproceso que funciona perfectamente en un equipo de un solo subproceso puede producir un error al instante en un equipo de cuatro procesadores. Las características de rendimiento y tiempo de las CPU AMD e Intel pueden variar considerablemente, por lo que asegúrese de probar en equipos multiprocesador basados en CPU de ambos proveedores.
Mejoras de Windows Vista y Windows 7
En el caso de los juegos destinados a las versiones más recientes de Windows, hay varias API que pueden simplificar la creación de aplicaciones multiproceso escalables. Esto es especialmente cierto con la nueva API ThreadPool y algunos primitivos de sincronización adicionales (variables de condición, bloqueo ligero de lectura y escritura y inicialización única). Puede encontrar información general sobre estas tecnologías en los siguientes artículos de MSDN Magazine:
- Mejora de la escalabilidad con las nuevas API de grupo de subprocesos
- Primitivos de sincronización nuevos en Windows Vista
Las aplicaciones que usan características de Direct3D 11 en estos sistemas operativos también pueden aprovechar el nuevo diseño para la creación simultánea de objetos y listas de comandos de contexto diferido para mejorar la escalabilidad de la representación multiproceso.
Resumen
Con un diseño cuidadoso que minimiza las interacciones entre subprocesos, puede obtener importantes ganancias de rendimiento de la programación multiproceso sin agregar una complejidad excesiva al código. Esto permitirá que el código del juego pase la siguiente oleada de mejoras de procesador y proporcionará experiencias de juego cada vez más atractivas.
Referencias
- Jim Beveridge & Robert Weiner, Multithreading Applications in Win32, Addison-Wesley, 1997
- Chuck Walbourn, Game Timing and Multicore Processors, Microsoft Corporation, 2005
- MSDN Library: GetLogicalProcessorInformation
- OpenMP