CPUSets para el desarrollo de juegosCPUSets for game development

IntroducciónIntroduction

La Plataforma universal de Windows (UWP) es el núcleo de una amplia gama de dispositivos electrónicos de consumo.The Universal Windows Platform (UWP) is at the core of a wide range of consumer electronic devices. Como tal, requiere un API de uso general para satisfacer las necesidades de todos los tipos de aplicaciones, desde juegos y aplicaciones incrustadas hasta software empresarial que se ejecuta en servidores.As such, it requires a general purpose API to address the needs of all types of applications from games to embedded apps to enterprise software running on servers. Al aprovechar la información correcta proporcionada por la API, puedes asegurarte de que tu juego se ejecuta el mejor rendimiento en cualquier hardware.By leveraging the right information provided by the API, you can ensure your game runs at its best on any hardware.

API de CPUSetsCPUSets API

La API de CPUSets proporciona control sobre los conjuntos de CPU que están disponibles para los subprocesos en los que se realizará la programación.The CPUSets API provides control over which CPU sets are available for threads to be scheduled on. Existen dos funciones disponibles para controlar dónde se programan los subprocesos:Two functions are available to control where threads are scheduled:

  • SetProcessDefaultCpuSets: esta función puede usarse para especificar los nuevos subprocesos de conjuntos de CPU que pueden ejecutarse si no se asignan a determinados conjuntos de CPU.SetProcessDefaultCpuSets – This function can be used to specify which CPU sets new threads may run on if they are not assigned to specific CPU sets.
  • SetThreadSelectedCpuSets: esta función permite limitar los conjuntos de CPU que los que se puede ejecutar un subproceso específico.SetThreadSelectedCpuSets – This function allows you to limit the CPU sets a specific thread may run on.

Si la función SetProcessDefaultCpuSets nunca se ha usado, los subprocesos recién creados se pueden programar en cualquier conjunto de CPU disponible para el proceso.If the SetProcessDefaultCpuSets function is never used, newly created threads may be scheduled on any CPU set available to your process. En esta sección se explican los conceptos básicos de la API de CPUSets.This section goes over the basics of the CPUSets API.

GetSystemCpuSetInformationGetSystemCpuSetInformation

La primera API usada para recopilar información es la función GetSystemCpuSetInformation.The first API used for gathering information is the GetSystemCpuSetInformation function. Esta función rellena la información de una matriz de objetos SYSTEM_CPU_SET_INFORMATION que proporciona el código de título.This function populates information in an array of SYSTEM_CPU_SET_INFORMATION objects provided by title code. La memoria del destino debe asignarse al código del juego, el tamaño del cual se determina mediante una llamada a GetSystemCpuSetInformation.The memory for the destination must be allocated by game code, the size of which is determined by calling GetSystemCpuSetInformation itself. Se requieren dos llamadas a GetSystemCpuSetInformation tal como se muestra en el siguiente ejemplo.This requires two calls to GetSystemCpuSetInformation as demonstrated in the following example.

unsigned long size;
HANDLE curProc = GetCurrentProcess();
GetSystemCpuSetInformation(nullptr, 0, &size, curProc, 0);

std::unique_ptr<uint8_t[]> buffer(new uint8_t[size]);

PSYSTEM_CPU_SET_INFORMATION cpuSets = reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(buffer.get());
  
GetSystemCpuSetInformation(cpuSets, size, &size, curProc, 0);

Cada instancia de SYSTEM_CPU_SET_INFORMATION devuelta contiene información sobre una unidad de procesamiento única, también conocida como un conjunto de CPU.Each instance of SYSTEM_CPU_SET_INFORMATION returned contains information about one unique processing unit, also known as a CPU set. Esto no significa necesariamente que representa un único componente físico de hardware.This does not necessarily mean that it represents a unique physical piece of hardware. Las CPU que usan hyperthreading tendrán varios núcleos lógicos ejecutándose en un único núcleo de procesamiento físico.CPUs that utilize hyperthreading will have multiple logical cores running on a single physical processing core. La programación de varios subprocesos en diferentes núcleos lógicos que residen en el mismo núcleo físico permite optimizar los recursos de nivel de hardware que, de lo contrario, requerirían más trabajo en el nivel del kernel.Scheduling multiple threads on different logical cores that reside on the same physical core allows hardware-level resource optimization that would otherwise require extra work to be done at the kernel level. Dos subprocesos programados en núcleos lógicos independientes en el mismo núcleo físico deben compartir el tiempo de CPU, pero se ejecutarían de forma más eficaz que si se programaran en el mismo núcleo lógico.Two threads scheduled on separate logical cores on the same physical core must share CPU time, but would run more efficiently than if they were scheduled to the same logical core.

SYSTEM_CPU_SET_INFORMATIONSYSTEM_CPU_SET_INFORMATION

La información de cada instancia de esta estructura de datos que devuelve GetSystemCpuSetInformation contiene información sobre una unidad de procesamiento única en la que se pueden programar los subprocesos.The information in each instance of this data structure returned from GetSystemCpuSetInformation contains information about a unique processing unit that threads may be scheduled on. Dada la amplia gama de dispositivos de destino posible, una gran parte de la información de la estructura de datos SYSTEM_CPU_SET_INFORMATION puede no ser aplicable al desarrollo de juegos.Given the possible range of target devices, a lot of the information in the SYSTEM_CPU_SET_INFORMATION data structure may not applicable for game development. La Tabla 1 proporciona una explicación de los miembros de datos que son útiles para el desarrollo de juegos.Table 1 provides an explanation of data members that are useful for game development.

Tabla 1. Miembros de datos útiles para el desarrollo de juegos.Table 1. Data members useful for game development.

Nombre del miembroMember name Tipo de datosData type DescripciónDescription
TipoType CPU_SET_INFORMATION_TYPECPU_SET_INFORMATION_TYPE Tipo de información de la estructura.The type of information in the structure. Si su valor no es CpuSetInformation, debe omitirse.If the value of this is not CpuSetInformation, it should be ignored.
IdentificadorId unsigned longunsigned long Identificador de conjunto de CPU especificado.The ID of the specified CPU set. Este es el identificador que debe usarse con las funciones de conjunto de CPU como SetThreadSelectedCpuSets.This is the ID that should be used with CPU set functions such as SetThreadSelectedCpuSets.
AgruparGroup unsigned shortunsigned short Especifica el "grupo de procesadores" del conjunto de CPU.Specifies the “processor group” of the CPU set. Los grupos de procesadores permiten que un equipo tenga más de 64 núcleos lógicos, así como el intercambio directo de las CPU mientras se ejecuta el sistema.Processor groups allow a PC to have more than 64 logical cores, and allow for hot swapping of CPUs while the system is running. Es poco común ver un equipo que no es un servidor con más de un grupo.It is uncommon to see a PC that is not a server with more than one group. A menos que escribas aplicaciones destinadas a ejecutarse en servidores grandes o granjas de servidores, es mejor usar conjuntos de CPU en un solo grupo porque la mayoría de equipos de consumo solo tienen un grupo de procesadores.Unless you are writing applications meant to run on large servers or server farms, it is best to use CPU sets in a single group because most consumer PCs will only have one processor group. Todos los demás valores de esta estructura guardan relación con el miembro Group.All other values in this structure are relative to the Group.
LogicalProcessorIndexLogicalProcessorIndex unsigned charunsigned char Índice relativo de grupo del conjunto de CPU.Group relative index of the CPU set
CoreIndexCoreIndex unsigned charunsigned char Índice relativo de grupo del núcleo de la CPU física donde se encuentra el conjunto de CPU.Group relative index of the physical CPU core where the CPU set is located
LastLevelCacheIndexLastLevelCacheIndex unsigned charunsigned char Índice relativo de grupo de la última memoria caché asociada a este conjunto de CPU.Group relative index of the last cache associated with this CPU set. Esta es la memoria caché más lenta, a menos que el sistema use nodos NUMA, normalmente la caché L2 o L3.This is the slowest cache unless the system utilizes NUMA nodes, usually the L2 or L3 cache.

Los otros miembros de datos proporcionan información que es bastante improbable que describa las CPU en equipos de consumo u otros dispositivos de consumo y es poco probable que resulte útil.The other data members provide information that is unlikely to describe CPUs in consumer PCs or other consumer devices and is unlikely to be useful. La información que proporcionan los datos devueltos puede usarse para organizar subprocesos de diversas maneras.The information provided by the data returned can then be used to organize threads in various ways. En la sección Consideraciones para el desarrollo de juegos de estas notas del producto se detallan algunas formas de aprovechar estos datos para optimizar la asignación de subprocesos.The Considerations for game development section of this white paper details a few ways to leverage this data to optimize thread allocation.

A continuación se incluyen algunos ejemplos del tipo de información recopilada de las aplicaciones para UWP que se ejecutan en distintos tipos de hardware.The following are some examples of the type of information gathered from UWP applications running on various types of hardware.

Tabla 2. Información devuelta desde una aplicación de UWP que se ejecuta en un Microsoft Lumia 950. Este es un ejemplo de un sistema que tiene varias memorias caché de último nivel. La característica Lumia 950 es un proceso de Snapdragon de Qualcomm 808 que contiene una CPU de doble núcleo Cortex A57 y cuatro núcleos de ARM Cortex A53.Table 2. Information returned from a UWP app running on a Microsoft Lumia 950. This is an example of a system that has multiple last level caches. The Lumia 950 features a Qualcomm 808 Snapdragon process that contains a dual core ARM Cortex A57 and quad core ARM Cortex A53 CPUs.

Tabla 2

Tabla 3. Información devuelta desde una aplicación de UWP que se ejecuta en un equipo típico. Este es un ejemplo de un sistema que utiliza hyperthreading; cada núcleo físico tiene dos núcleos lógicos en los que se pueden programar subprocesos. En este caso, el sistema contenía una CPU Intel xenón E5-2620.Table 3. Information returned from a UWP app running on a typical PC. This is an example of a system that uses hyperthreading; each physical core has two logical cores onto which threads can be scheduled. In this case, the system contained an Intel Xenon CPU E5-2620.

Tabla 3

Tabla 4. La información devuelta desde una aplicación de UWP que se ejecuta en un núcleo cuádruple de Microsoft Surface Pro 4. Este sistema tenía una CPU Intel Core i5-6300.Table 4. Information returned from a UWP app running on a quad core Microsoft Surface Pro 4. This system had an Intel Core i5-6300 CPU.

Tabla 4

SetThreadSelectedCpuSetsSetThreadSelectedCpuSets

Ahora que está disponible la información sobre los conjuntos de CPU, puede usarse para organizar los subprocesos.Now that information about the CPU sets is available, it can be used to organize threads. El identificador de un subproceso creado con CreateThread se pasa a esta función junto con una matriz de identificadores de conjuntos de CPU en los que se puede programar el subproceso.The handle of a thread created with CreateThread is passed to this function along with an array of IDs of the CPU sets that the thread can be scheduled on. Se muestra un ejemplo de su uso en el siguiente código.One example of its usage is demonstrated in the following code.

HANDLE audioHandle = CreateThread(nullptr, 0, AudioThread, nullptr, 0, nullptr);
unsigned long cores [] = { cpuSets[0].CpuSet.Id, cpuSets[1].CpuSet.Id };
SetThreadSelectedCpuSets(audioHandle, cores, 2);

En este ejemplo, se crea un subproceso basado en una función que se declara como AudioThread.In this example, a thread is created based on a function declared as AudioThread. Posteriormente, este subproceso se puede programar en uno de los dos conjuntos de CPU.This thread is then allowed to be scheduled on one of two CPU sets. La propiedad del subproceso del conjunto de CPU no es exclusiva.Thread ownership of the CPU set is not exclusive. Los subprocesos creados sin bloquearse en un conjunto de CPU específico pueden tomar tiempo de AudioThread.Threads that are created without being locked to a specific CPU set may take time from the AudioThread. De igual modo, también se pueden bloquear otros subprocesos creados en uno o ambos conjuntos de CPU posteriormente.Likewise, other threads created may also be locked to one or both of these CPU sets at a later time.

SetProcessDefaultCpuSetsSetProcessDefaultCpuSets

El contrario de SetThreadSelectedCpuSets es SetProcessDefaultCpuSets.The converse to SetThreadSelectedCpuSets is SetProcessDefaultCpuSets. Cuando se crean subprocesos, no es necesario bloquearlos en determinados conjuntos de CPU.When threads are created, they do not need to be locked into certain CPU sets. Si no quieres que estos subprocesos se ejecuten en conjuntos de CPU específicos (aquellos que usan el subproceso de representación o el subproceso de audio, por ejemplo), puedes usar esta función para especificar en qué núcleos se pueden programar estos subprocesos.If you do not want these threads to run on specific CPU sets (those used by your render thread or audio thread for example), you can use this function to specify which cores these threads are allowed to be scheduled on.

Consideraciones para el desarrollo de juegosConsiderations for game development

Como hemos visto, la API de CPUSets proporciona una gran cantidad de información y flexibilidad para la programación de subprocesos.As we've seen, the CPUSets API provides a lot of information and flexibility when it comes to scheduling threads. En lugar de adoptar el enfoque de abajo arriba de intentar encontrar usos para estos datos, resulta más eficaz adoptar el enfoque de arriba a abajo de averiguar cómo se pueden usar los datos para admitir escenarios comunes.Instead of taking the bottom-up approach of trying to find uses for this data, it is more effective to take the top-down approach of finding how the data can be used to accommodate common scenarios.

Trabajar con hyperthreading y subprocesos críticos en el tiempoWorking with time critical threads and hyperthreading

Este método es eficaz si el juego tiene algunos subprocesos que deben ejecutarse en tiempo real junto con otros subprocesos de trabajo que requieren relativamente poco tiempo de CPU.This method is effective if your game has a few threads that must run in real time along with other worker threads that require relatively little CPU time. Algunas tareas, como la música de fondo continua, deben ejecutarse sin interrupciones para una experiencia de juego perfecta.Some tasks, like continuous background music, must run without interruption for an optimal gaming experience. Incluso un único fotograma de colapso de un subproceso de audio puede causar la aparición de mensajes o problemas, por lo cual es muy importante que reciba la cantidad de tiempo de CPU en cada fotograma.Even a single frame of starvation for an audio thread may cause popping or glitching, so it is critical that it receives the necessary amount of CPU time every frame.

Al usar SetThreadSelectedCpuSets junto con SetProcessDefaultCpuSets puedes garantizar que los subprocesos intensos no se interrumpan a causa de subprocesos de trabajo.Using SetThreadSelectedCpuSets in conjunction with SetProcessDefaultCpuSets can ensure your heavy threads remain uninterrupted by any worker threads. SetThreadSelectedCpuSets puede usarse para asignar subprocesos intensos a conjuntos de CPU específicos.SetThreadSelectedCpuSets can be used to assign your heavy threads to specific CPU sets. SetProcessDefaultCpuSets puede usarse para garantizar que los subprocesos sin asignar creados se coloquen en otros conjuntos de CPU.SetProcessDefaultCpuSets can then be used to make sure any unassigned threads created are put on other CPU sets. En el caso de las CPU que usan hyperthreading, también es importante para tener en cuenta los núcleos lógicos en el mismo núcleo físico.In the case of CPUs that utilize hyperthreading, it's also important to account for logical cores on the same physical core. Los subprocesos de trabajo no deberían poder ejecutarse en núcleos lógicos que comparten el mismo núcleo físico que un subproceso que quieres ejecutar con la capacidad de respuesta en tiempo real.Worker threads should not be allowed to run on logical cores that share the same physical core as a thread that you want to run with real time responsiveness. El siguiente código muestra cómo determinar si un equipo usa hyperthreading.The following code demonstrates how to determine whether a PC uses hyperthreading.

unsigned long retsize = 0;
(void)GetSystemCpuSetInformation( nullptr, 0, &retsize,
    GetCurrentProcess(), 0);
 
std::unique_ptr<uint8_t[]> data( new uint8_t[retsize] );
if ( !GetSystemCpuSetInformation(
    reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>( data.get() ),
    retsize, &retsize, GetCurrentProcess(), 0) )
{
    // Error!
}
 
std::set<DWORD> cores;
std::vector<DWORD> processors;
uint8_t const * ptr = data.get();
for( DWORD size = 0; size < retsize; ) {
    auto info = reinterpret_cast<const SYSTEM_CPU_SET_INFORMATION*>( ptr );
    if ( info->Type == CpuSetInformation ) {
         processors.push_back( info->CpuSet.Id );
         cores.insert( info->CpuSet.CoreIndex );
    }
    ptr += info->Size;
    size += info->Size;
}
 
bool hyperthreaded = processors.size() != cores.size();

Si el sistema usa hyperthreading, es importante que el conjunto de conjuntos de CPU predeterminados no incluya núcleos lógicos en el mismo núcleo físico que los subprocesos en tiempo real.If the system utilizes hyperthreading, it is important that the set of default CPU sets does not include any logical cores on the same physical core as any real time threads. Si el sistema no usa hyperthreading, solo es necesario para asegurarse de que los conjuntos de CPU predeterminados no incluyen el mismo núcleo que el conjunto de CPU que ejecuta el subproceso de audio.If the system is not hyperthreading, it is only necessary to make sure that the default CPU sets do not include the same core as the CPU set running your audio thread.

Un ejemplo de organización de subprocesos basada en núcleos físicos puede encontrarse en el ejemplo de CPUSets disponible en el repositorio de GitHub, cuyo link se encuentra en la sección Recursos adicionales.An example of organizing threads based on physical cores can be found in the CPUSets sample available on the GitHub repository linked in the Additional resources section.

Reducir el costo de la coherencia de caché con la memoria caché de último nivelReducing the cost of cache coherence with last level cache

La coherencia de caché es el concepto en que la memoria caché es la misma en varios recursos de hardware que actúan en los mismos datos.Cache coherency is the concept that cached memory is the same across multiple hardware resources that act on the same data. Si los subprocesos se programan en diferentes núcleos, pero que funcionan en los mismos datos, es posible que estén funcionando en copias independientes de los datos en memorias caché diferentes.If threads are scheduled on different cores, but work on the same data, they may be working on separate copies of that data in different caches. Para obtener los resultados correctos, estas cachés deben mantenerse coherentes entre sí.In order to get correct results, these caches must be kept coherent with each other. Mantener la coherencia entre varias cachés es relativamente costoso, pero es necesario para que cualquier sistema de varios núcleos funcione.Maintaining coherency between multiple caches is relatively expensive, but necessary for any multi-core system to operate. Además, está completamente fuera del control del código de cliente; el sistema subyacente funciona independientemente para mantener las memorias caché actualizadas mediante el acceso a los recursos de memoria compartidos entre núcleos.Additionally, it is completely out of the control of client code; the underlying system works independently to keep caches up to date by accessing shared memory resources between cores.

Si el juego tiene varios subprocesos que comparten una cantidad considerable de datos, puedes reducir al mínimo el costo de la coherencia de caché. Para ello, asegúrate de que estén programados en conjuntos de CPU que comparten una caché de último nivel.If your game has multiple threads that share an especially large amount of data, you can minimize the cost of cache coherency by ensuring that they are scheduled on CPU sets that share a last level cache. La caché de último nivel es la más lenta disponible para un núcleo en sistemas que no usan nodos NUMA.The last level cache is the slowest cache available to a core on systems that do not utilize NUMA nodes. Es extremadamente raro que un equipo de juegos use nodos NUMA.It is extremely rare for a gaming PC to utilize NUMA nodes. Si los núcleos no comparten una caché de último nivel, mantener la coherencia requerirá acceder a recursos de memoria de mayor nivel y, por tanto, más lentos.If cores do not share a last level cache, maintaining coherency would require accessing higher level, and therefore slower, memory resources. El bloqueo de dos subprocesos para separar conjuntos de CPU que comparten una caché y un núcleo físico puede proporcionar un rendimiento aún mayor que su programación en núcleos físicos independientes si no requieren más del 50 % del tiempo en un fotograma determinado.Locking two threads to separate CPU sets that share a cache and a physical core may provide even better performance than scheduling them on separate physical cores if they do not require more than 50% of the time in any given frame.

Este ejemplo de código muestra cómo determinar si los subprocesos que se comunican con frecuencia pueden compartir una caché de último nivel.This code example shows how to determine whether threads that communicate frequently can share a last level cache.

unsigned long retsize = 0;
(void)GetSystemCpuSetInformation(nullptr, 0, &retsize,
    GetCurrentProcess(), 0);
 
std::unique_ptr<uint8_t[]> data(new uint8_t[retsize]);
if (!GetSystemCpuSetInformation(
    reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(data.get()),
    retsize, &retsize, GetCurrentProcess(), 0))
{
    // Error!
}
 
unsigned long count = retsize / sizeof(SYSTEM_CPU_SET_INFORMATION);
bool sharedcache = false;
 
std::map<unsigned char, std::vector<SYSTEM_CPU_SET_INFORMATION>> cachemap;
for (size_t i = 0; i < count; ++i)
{
    auto cpuset = reinterpret_cast<PSYSTEM_CPU_SET_INFORMATION>(data.get())[i];
    if (cpuset.Type == CPU_SET_INFORMATION_TYPE::CpuSetInformation)
    {
        if (cachemap.find(cpuset.CpuSet.LastLevelCacheIndex) == cachemap.end())
        {
            std::pair<unsigned char, std::vector<SYSTEM_CPU_SET_INFORMATION>> newvalue;
            newvalue.first = cpuset.CpuSet.LastLevelCacheIndex;
            newvalue.second.push_back(cpuset);
            cachemap.insert(newvalue);
        }
        else
        {
            sharedcache = true;
            cachemap[cpuset.CpuSet.LastLevelCacheIndex].push_back(cpuset);
        }
    }
}

El diseño de caché que se muestra en la figura 1 es un ejemplo del tipo de diseño que se puede ver de un sistema.The cache layout illustrated in Figure 1 is an example of the type of layout you might see from a system. Esta figura es una ilustración de las cachés de un Microsoft Lumia 950.This figure is an illustration of the caches found in a Microsoft Lumia 950. La comunicación entre subprocesos que se produce entre la CPU 256 y la CPU 260 supondría una sobrecarga significativa porque requeriría que el sistema mantuviese la coherencia de sus cachés L2.Inter-thread communication occurring between CPU 256 and CPU 260 would incur significant overhead because it would require the system to keep their L2 caches coherent.

Figura 1. Arquitectura de caché encontrada en un dispositivo Microsoft Lumia 950.Figure 1. Cache architecture found on a Microsoft Lumia 950 device.

Caché del Lumia 950

ResumenSummary

La API de CPUSets disponible para el desarrollo de UWP proporciona una cantidad considerable de información y control sobre las opciones de multithreading.The CPUSets API available for UWP development provides a considerable amount of information and control over your multithreading options. La complejidad adicional en comparación con las API multiproceso de desarrollo de Windows presenta alguna curva de aprendizaje, pero la mayor flexibilidad permite, en última instancia, un rendimiento superior en una amplia gama de equipos de consumo y otros destinos de hardware.The added complexities compared to previous multithreaded APIs for Windows development has some learning curve, but the increased flexibility ultimately allows for better performance across a range of consumer PCs and other hardware targets.

Recursos adicionalesAdditional resources