Codificação para vários núcleos no Xbox 360 e no Windows
Durante anos, o desempenho dos processadores aumentou constantemente, e jogos e outros programas colheram os benefícios desse poder crescente sem precisar fazer nada especial.
As regras foram alteradas. O desempenho de núcleos de processador único agora está aumentando muito lentamente, se em tudo. No entanto, a potência de computação disponível em um computador ou console típico continua crescendo. A diferença é que a maior parte desse ganho de desempenho agora vem de ter vários núcleos de processador em um único computador, muitas vezes em um único chip. A CPU do Xbox 360 tem três núcleos de processador em um chip e cerca de 70% dos processadores de computadores vendidos em 2006 eram multi-núcleos.
Os aumentos no poder de processamento disponível são tão dramáticos quanto no passado, mas agora os desenvolvedores precisam escrever código multithread para usar esse poder. A programação com vários threads traz novos desafios de design e programação. Este tópico fornece alguns conselhos sobre como começar a usar a programação multithreaded.
A importância do bom design
Um bom design de programa multithread é crítico, mas pode ser muito difícil. Se você mover acidentalmente seus principais sistemas de jogos para threads diferentes, provavelmente descobrirá que cada thread passa a maior parte do tempo esperando nos outros threads. Esse tipo de design leva a maior complexidade e esforço significativo de depuração, sem praticamente nenhum ganho de desempenho.
Toda vez que os threads precisam sincronizar ou compartilhar dados, há o potencial de corrupção de dados, sobrecarga de sincronização, deadlocks e complexidade. Portanto, seu design multithread precisa documentar claramente todos os pontos de sincronização e comunicação e deve minimizar esses pontos o máximo possível. Quando os threads precisam se comunicar, o esforço de codificação aumentará, o que poderá reduzir a produtividade se afetar muito código-fonte.
A meta de design mais simples para multithreading é dividir o código em grandes partes independentes. Se você restringir essas partes a se comunicar apenas algumas vezes por quadro, verá uma aceleração significativa de multithreading, sem complexidade indevida.
Tarefas de thread típicas
Alguns tipos de tarefas provaram ser passíveis de serem colocadas em threads separados. A lista a seguir não se destina a ser exaustiva, mas deve dar algumas ideias.
Renderização
A renderização — que pode incluir a movimentação do grafo de cena ou, possivelmente, apenas a chamada de funções D3D — geralmente representa 50% ou mais do tempo de CPU. Portanto, mover a renderização para outro thread pode ter benefícios significativos. O thread de atualização pode preencher algum tipo de buffer de descrição de renderização, que o thread de renderização pode processar.
O thread de atualização de jogo está sempre um quadro à frente do thread de renderização, o que significa que ele leva dois quadros antes que as ações do usuário apareçam na tela. Embora esse aumento de latência possa ser um problema, o aumento da taxa de quadros da divisão da carga de trabalho geralmente mantém a latência total aceitável.
Na maioria dos casos, toda a renderização ainda é feita em um único thread, mas é um thread diferente da atualização do jogo.
Às vezes, o sinalizador D3DCREATE_MULTITHREADED é usado para permitir a renderização em um thread e a criação de recursos em outros threads; esse sinalizador é ignorado no Xbox 360 e você deve evitar usá-lo no Windows. No Windows, especificar esse sinalizador força o D3D a gastar uma quantidade significativa de tempo na sincronização, diminuindo assim a velocidade do thread de renderização.
Descompactação de arquivo
Os tempos de carga são sempre muito longos, e transmitir dados na memória sem afetar a taxa de quadros pode ser desafiador. Se todos os dados forem compactados agressivamente no disco, a velocidade de transferência de dados do disco rígido ou do disco óptico será menos provável que seja um fator limitante. Em um processador de thread único, geralmente não há tempo de processador suficiente disponível para compactação para ajudar a carregar tempos. Em um sistema multiprocessador, no entanto, a descompactação de arquivo usa ciclos de CPU que, de outra forma, seriam desperdiçados; ele melhora os tempos de carga e o streaming; e economiza espaço no disco.
Não use a descompactação de arquivo como uma substituição para processamento que deve ser feito durante a produção. Por exemplo, se você dedicar um thread extra à análise de dados XML durante o carregamento de nível, não usará multithreading para melhorar a experiência do jogador.
Ao usar um thread de descompactação de arquivo, você ainda deve usar E/S de arquivo assíncrono e leituras grandes para maximizar a eficiência de leitura de dados.
Gráficos Fofos
Há muitas gentilezas gráficas que melhoram a aparência do jogo, mas não são estritamente necessárias. Isso inclui coisas como animações de nuvem geradas processualmente, simulações de pano e cabelo, ondas processuais, vegetação processual, mais partículas ou física não gameplay.
Como esses efeitos não afetam a jogabilidade, eles não causam problemas de sincronização complicados— eles podem sincronizar com os outros threads uma vez por quadro ou com menos frequência. Além disso, em jogos para Windows, esses efeitos podem agregar valor para jogadores com CPUs multicore, enquanto silenciosamente são omitidos em computadores de núcleo único, proporcionando assim uma maneira fácil de dimensionar em uma ampla gama de recursos.
Física
A física geralmente não pode ser colocada em um thread separado para ser executada em paralelo com a atualização do jogo porque a atualização do jogo geralmente requer os resultados dos cálculos de física imediatamente. A alternativa para a física multithreading é executá-la em vários processadores. Embora isso possa ser feito, é uma tarefa complexa que exige acesso frequente a estruturas de dados compartilhadas. Se você puder manter sua carga de trabalho física baixa o suficiente para caber no thread main, seu trabalho será mais simples.
Bibliotecas que dão suporte à execução da física em vários threads estão disponíveis. No entanto, isso pode levar a um problema: quando o jogo está executando a física, ele usa muitos threads, mas o resto do tempo ele usa poucos. A execução da física em vários threads exigirá resolver isso para que a carga de trabalho seja distribuída uniformemente sobre o quadro. Se você escrever um mecanismo de física multithreaded, deverá prestar atenção a todas as suas estruturas de dados, pontos de sincronização e balanceamento de carga.
Exemplo de designs multithread
Os jogos para Windows precisam ser executados em computadores com diferentes números de núcleos de CPU. A maioria dos computadores de jogos ainda tem apenas um núcleo, embora o número de computadores de dois núcleos esteja crescendo rapidamente. Um jogo típico para Windows pode dividir sua carga de trabalho em um thread para atualização e renderização, com threads de trabalho opcionais para adicionar funcionalidade extra. Além disso, alguns threads em segundo plano para fazer E/S de arquivo e rede provavelmente seriam usados. A Figura 1 mostra os threads, juntamente com os pontos de transferência de dados main.
Figura 1. Design de threading em um jogo para Windows
Um jogo típico do Xbox 360 pode usar threads de software com uso intensivo de CPU adicionais, portanto, ele pode dividir sua carga de trabalho em um thread de atualização, thread de renderização e três threads de trabalho, conforme mostrado na Figura 2.
Figura 2. Design de threading em um jogo para Xbox 360
Com exceção da E/S do arquivo e da rede, todas essas tarefas têm o potencial de serem intensivas em CPU o suficiente para se beneficiarem de estarem em seu próprio thread de hardware. Essas tarefas também têm o potencial de serem independentes o suficiente para que possam ser executadas para um quadro inteiro sem se comunicar.
O thread de atualização de jogo gerencia a entrada, a IA e a física do controlador e prepara instruções para os outros quatro threads. Essas instruções são colocadas em buffers pertencentes ao thread de atualização de jogo, portanto, nenhuma sincronização é necessária à medida que as instruções são geradas.
No final do quadro, o thread de atualização do jogo entrega os buffers de instrução para os outros quatro threads e, em seguida, começa a trabalhar no próximo quadro, preenchendo outro conjunto de buffers de instrução.
Como os threads de atualização e renderização funcionam em lockstep uns com os outros, seus buffers de comunicação são simplesmente armazenados em buffer duplo: a qualquer momento, o thread de atualização está preenchendo um buffer enquanto o thread de renderização está lendo do outro.
Os outros threads de trabalho não estão necessariamente vinculados à taxa de quadros. Descompactar um dado pode levar muito menos do que um quadro ou pode levar muitos quadros. Mesmo a simulação de pano e cabelo pode não precisar ser executada exatamente na taxa de quadros porque atualizações menos frequentes podem ser bastante aceitáveis. Portanto, esses três threads precisam de estruturas de dados diferentes para se comunicar com o thread de atualização e o thread de renderização. Cada uma delas precisa de uma fila de entrada que possa conter solicitações de trabalho e o thread de renderização precisa de uma fila de dados que possa conter os resultados produzidos pelos threads. No final de cada quadro, o thread de atualização adicionará um bloco de solicitações de trabalho às filas dos threads de trabalho. Adicionar à lista apenas uma vez por quadro garante que o thread de atualização minimize a sobrecarga de sincronização. Cada um dos threads de trabalho extrai atribuições da fila de trabalho o mais rápido possível, usando um loop semelhante a este:
for(;;)
{
while( WorkQueueNotEmpty() )
{
RemoveWorkItemFromWorkQueue();
ProcessWorkItem();
PutResultInDataQueue();
}
WaitForSingleObject( hWorkSemaphore );
}
Como os dados vão dos threads de atualização para os threads de trabalho e, em seguida, para o thread de renderização, pode haver um atraso de três ou mais quadros antes de algumas ações chegarem à tela. No entanto, se você atribuir tarefas tolerantes à latência aos threads de trabalho, isso não deve ser um problema.
Um design alternativo seria ter vários threads de trabalho todos desenhando da mesma fila de trabalho. Isso daria balanceamento automático de carga e tornaria mais provável que todos os threads de trabalho permanecessem ocupados.
O thread de atualização de jogo deve ter cuidado para não dar muito trabalho aos threads de trabalho ou então as filas de trabalho podem crescer continuamente. A forma como o thread de atualização gerencia isso depende do tipo de tarefas que os threads de trabalho estão fazendo.
Multithreading simultâneo e número de threads
Todos os threads não são criados iguais. Dois threads de hardware podem estar em chips separados, no mesmo chip ou até mesmo no mesmo núcleo. A configuração mais importante para os programadores de jogos estarem cientes é de dois threads de hardware em um núcleo: SMT (Multi-Threading Simultâneo) ou tecnologia de Hyper-Threading (tecnologia HT).
Os threads de tecnologia SMT ou HT compartilham os recursos do núcleo da CPU. Como elas compartilham as unidades de execução, a velocidade máxima da execução de dois threads em vez de um normalmente é de 10 a 20%, em vez dos 100% possíveis de dois threads de hardware independentes.
Mais significativamente, os threads de tecnologia SMT ou HT compartilham as instruções L1 e os caches de dados. Se os padrões de acesso à memória forem incompatíveis, eles poderão acabar brigando pelo cache e causando muitos erros de cache. Na pior das hipóteses, o desempenho total do núcleo da CPU pode realmente diminuir quando um segundo thread é executado. No Xbox 360, esse é um problema bastante simples. A configuração do Xbox 360 é conhecida — três núcleos de CPU cada um com dois threads de hardware — e os desenvolvedores atribuem seus threads de software a threads específicos da CPU e podem medir se seu design de threading oferece desempenho extra.
No Windows, a situação é mais complicada. O número de threads e sua configuração variam de computador para computador e determinar a configuração é complicado. A função GetLogicalProcessorInformation fornece informações sobre a relação entre diferentes threads de hardware e essa função está disponível no Windows Vista, Windows 7 e Windows XP SP3. Portanto, por enquanto, você precisa usar a instrução CPUID e os algoritmos fornecidos por Intel e AMD para decidir quantos threads "reais" você tem disponíveis. Consulte as referências para obter mais informações.
O exemplo CoreDetection no SDK do DirectX contém um código de exemplo que usa a função GetLogicalProcessorInformation ou a instrução CPUID para retornar a topologia principal da CPU. A instrução CPUID será usada se GetLogicalProcessorInformation não tiver suporte na plataforma atual. CoreDetection pode ser encontrado nos seguintes locais:
-
Fonte:
-
Raiz do SDK do DirectX\Samples\C++\Misc\CoreDetection
-
Executável:
-
Root\Samples\C++ do SDK do DirectX\Misc\Bin\CoreDetection.exe
A suposição mais segura é não ter mais de um thread com uso intensivo de CPU por núcleo de CPU. Ter mais threads com uso intensivo de CPU do que núcleos de CPU oferece pouco ou nenhum benefício e traz a sobrecarga extra e a complexidade de threads adicionais.
Criando threads
A criação de threads é uma operação bastante simples, mas há muitos erros potenciais. O código a seguir mostra a maneira adequada de criar um thread, aguardando que ele seja encerrado e, em seguida, limpando.
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;
}
Ao criar um thread, você tem a opção de especificar o tamanho da pilha para o thread filho ou especificar zero; nesse caso, o thread filho herdará o tamanho da pilha do thread pai. No Xbox 360, em que as pilhas são totalmente confirmadas quando o thread é iniciado, especificar zero pode desperdiçar memória significativa, pois muitos threads filho não precisarão de tanta pilha quanto o pai. No Xbox 360, também é importante que o tamanho da pilha seja um múltiplo de 64 KB.
Se você usar a função CreateThread para criar threads, o CRT (runtime do C/C++) não será inicializado corretamente no Windows. Em vez disso, recomendamos que você use a função _beginthreadex CRT.
O valor retornado de CreateThread ou _beginthreadex é um identificador de thread. Esse thread pode ser usado para aguardar o término do thread filho, o que é muito mais simples e muito mais eficiente do que girar em um loop verificando o thread status. Para aguardar o término do thread, basta chamar WaitForSingleObject com o identificador de thread.
Os recursos para o thread não serão liberados até que o thread seja encerrado e o identificador de thread tenha sido fechado. Portanto, é importante fechar o identificador de thread com CloseHandle quando você terminar de fazer isso. Se você estiver esperando o thread terminar com WaitForSingleObject, não feche o identificador até que a espera seja concluída.
No Xbox 360, você deve atribuir explicitamente threads de software a um thread de hardware específico usando XSetThreadProcessor. Caso contrário, todos os threads filho permanecerão no mesmo thread de hardware que o pai. No Windows, você pode usar SetThreadAffinityMask para sugerir fortemente ao sistema operacional em quais threads de hardware seu thread deve ser executado. Essa técnica geralmente deve ser evitada no Windows, pois você não sabe quais outros processos podem estar em execução no sistema. Normalmente, é melhor permitir que o agendador do Windows atribua seus threads a threads de hardware ociosos.
A criação de threads é uma operação cara. Os threads devem ser criados e destruídos raramente. Se você quiser criar e destruir threads com frequência, use um pool de threads que esperam por trabalho.
Sincronizando threads
Para que vários threads trabalhem juntos, você deve ser capaz de sincronizar threads, passar mensagens e solicitar acesso exclusivo aos recursos. O Windows e o Xbox 360 vêm com um conjunto avançado de primitivos de sincronização. Para obter detalhes completos sobre esses primitivos de sincronização, consulte a documentação da plataforma.
Acesso Exclusivo
Obter acesso exclusivo a um recurso, estrutura de dados ou caminho de código é uma necessidade comum. Uma opção para obter acesso exclusivo é um mutex, cujo uso típico é mostrado aqui.
// 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 );
As seções críticas têm semântica semelhante aos mutexes, mas podem ser usadas para sincronizar somente dentro de um processo, não entre processos. Sua vantagem main é que eles executam cerca de vinte vezes mais rápido do que mutexes.
Eventos
Se dois threads, talvez um thread de atualização e um thread de renderização, estiverem se revezando usando um par de buffers de descrição de renderização, eles precisarão de uma maneira de indicar quando terminarem com o buffer específico. Isso pode ser feito associando um evento (alocado com CreateEvent) a cada buffer. Quando um thread é feito com um buffer, ele pode usar SetEvent para sinalizar isso e, em seguida, pode chamar WaitForSingleObject no evento do outro buffer. Essa técnica extrapola facilmente para o buffer triplo de recursos.
Semáforos
Um semáforo é usado para controlar quantos threads podem ser executados e é comumente usado para implementar filas de trabalho. Um thread adiciona trabalho a uma fila e usa ReleaseSemaphore sempre que adiciona um novo item à fila. Isso permite que um thread de trabalho seja liberado do pool de threads em espera. Os threads de trabalho apenas chamam WaitForSingleObject e, quando ele retorna, eles sabem que há um item de trabalho na fila para eles. Além disso, uma seção crítica ou outra técnica de sincronização deve ser usada para garantir o acesso seguro à fila de trabalho compartilhada.
Evitar SuspendThread
Às vezes, quando você deseja que um thread pare o que está fazendo, é tentador usar SuspendThread em vez dos primitivos de sincronização corretos. Essa é sempre uma má ideia e pode facilmente levar a deadlocks e outros problemas. SuspendThread também interage mal com o depurador do Visual Studio. Evite SuspendThread. Em vez disso, use WaitForSingleObject .
WaitForSingleObject e WaitForMultipleObjects
A função WaitForSingleObject é a função de sincronização mais usada. No entanto, às vezes, você deseja que um thread aguarde até que várias condições sejam atendidas simultaneamente ou até que um de um conjunto de condições seja atendido. Nesse caso, você deve usar WaitForMultipleObjects.
Funções interligadas e programação sem bloqueio
Há uma família de funções para executar operações simples de thread-safe sem usar bloqueios. Estas são a família interligada de funções, como InterlockedIncrement. Essas funções, além de outras técnicas que usam a configuração cuidadosa de sinalizadores, são conhecidas como programação sem bloqueio. A programação sem bloqueio pode ser extremamente complicada de se fazer corretamente e é substancialmente mais difícil no Xbox 360 do que no Windows.
Para obter mais informações sobre programação sem bloqueios, consulte Considerações de programação sem bloqueio para Xbox 360 e Microsoft Windows.
Minimizando a sincronização
Alguns métodos de sincronização são mais rápidos do que outros. No entanto, em vez de otimizar seu código escolhendo as técnicas de sincronização mais rápidas possíveis, geralmente é melhor sincronizar com menos frequência. Isso é mais rápido do que sincronizar com muita frequência e torna o código mais simples que é mais fácil de depurar.
Algumas operações, como alocação de memória, podem ter que usar primitivos de sincronização para funcionar corretamente. Portanto, fazer alocações frequentes do heap compartilhado padrão resultará em sincronização frequente, o que desperdiçará algum desempenho. Evitar alocações frequentes ou usar heaps por thread (usando HEAP_NO_SERIALIZE se você usar HeapCreate) pode evitar essa sincronização oculta.
Outra causa de sincronização oculta é D3DCREATE_MULTITHREADED, o que faz com que o D3D no Windows use a sincronização em muitas operações. (O sinalizador é ignorado no Xbox 360.)
Os dados por thread, também conhecidos como armazenamento local de thread, podem ser uma maneira importante de evitar a sincronização. O Visual C++ permite declarar variáveis globais como sendo por thread com a sintaxe __declspec(thread ).
__declspec( thread ) int tls_i = 1;
Isso fornece a cada thread no processo sua própria cópia de tls_i, que pode ser referenciada com segurança e eficiência sem a necessidade de sincronização.
A técnica __declspec(thread) não funciona com DLLs carregadas dinamicamente. Se você usar DLLs carregadas dinamicamente, precisará usar a família de funções TLSAlloc para implementar o armazenamento local do thread.
Destruindo threads
A única maneira segura de destruir um thread é fazer com que o thread saia, seja retornando da função de thread main ou fazendo com que o thread chame ExitThread ou _endthreadex. Se um thread for criado com _beginthreadex, ele deverá usar _endthreadex ou retornar da função de thread main, pois o uso de ExitThread não liberará corretamente os recursos de CRT. Nunca chame a função TerminateThread , pois o thread não será limpo corretamente. Os threads sempre devem cometer suicídio. Eles nunca devem ser assassinados.
OpenMP
O OpenMP é uma extensão de linguagem para adicionar multithreading ao programa usando pragmas para orientar o compilador na paralelização de loops. O OpenMP é compatível com o Visual C++ 2005 no Windows e no Xbox 360 e pode ser usado em conjunto com o gerenciamento manual de threads. O OpenMP pode ser uma maneira conveniente para partes multithread do seu código, mas é improvável que seja a solução ideal, especialmente para jogos. O OpenMP pode ser mais aplicável a tarefas de produção de execução mais longa, como arte de processamento e outros recursos. Para obter mais informações, consulte a documentação do Visual C++ ou acesse o site do OpenMP.
Criação de perfil
A criação de perfil multithread é importante. É fácil acabar com longas barracas em que os threads estão esperando uns pelos outros. Essas barracas podem ser difíceis de encontrar e diagnosticar. Para ajudar a identificá-los, considere adicionar instrumentação às chamadas de sincronização. Um criador de perfil de amostragem também pode ajudar a identificar esses problemas porque pode registrar informações de tempo sem alterá-las substancialmente.
Timing
A instrução rdtsc é uma maneira de obter informações precisas de tempo no Windows. Infelizmente, o rdtsc tem vários problemas que o tornam uma escolha ruim para seu título de remessa. Os contadores rdtsc não são necessariamente sincronizados entre CPUs, portanto, quando o thread se move entre threads de hardware, você pode obter grandes diferenças positivas ou negativas. Dependendo das configurações de gerenciamento de energia, a frequência com que o contador rdtsc é incrementado também pode mudar conforme o jogo é executado. Para evitar essas dificuldades, você deve preferir QueryPerformanceCounter e QueryPerformanceFrequency para tempo de alta precisão em seu jogo de envio. Para obter mais informações sobre o tempo, consulte Tempo do jogo e Processadores multicore.
Depuração
O Visual Studio dá suporte total à depuração multithread para Windows e Xbox 360. A janela threads do Visual Studio permite alternar entre threads para ver as diferentes pilhas de chamadas e variáveis locais. A janela threads também permite congelar e descongelar threads específicos.
No Xbox 360, você pode usar a meta-variável @hwthread na janela watch para mostrar o thread de hardware no qual o thread de software atualmente selecionado está em execução.
A janela threads será mais fácil de usar se você nomear seus threads de forma significativa. O Visual Studio e outros depuradores da Microsoft permitem nomear seus threads. Implemente a função SetThreadName a seguir e chame-a de cada thread à medida que ele for iniciado.
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");
O KD (depurador de kernel) e o WinDBG também dão suporte à depuração multithreaded.
Teste
A programação multithread pode ser complicada, e alguns bugs multithread aparecem apenas raramente, dificultando a localização e a correção. Uma das melhores maneiras de liberá-los é testar em uma ampla variedade de computadores, especialmente aqueles com quatro ou mais processadores. O código multithread que funciona perfeitamente em um computador de thread único pode falhar instantaneamente em um computador de quatro processadores. As características de desempenho e tempo das CPUs AMD e Intel podem variar substancialmente, portanto, certifique-se de testar em computadores multiprocessadores com base em CPUs de ambos os fornecedores.
Melhorias do Windows Vista e do Windows 7
Para jogos direcionados às versões mais recentes do Windows, há várias APIs que podem simplificar a criação de aplicativos multithread escalonáveis. Isso é particularmente verdadeiro com a nova API threadPool e alguns primitivos de sincronização adicionais (variáveis de condição, o bloqueio de leitura/gravador fino e inicialização única). Você pode encontrar uma visão geral dessas tecnologias nos seguintes artigos da MsDN Magazine:
- Melhorar a escalabilidade com novas APIs do pool de threads
- Primitivos de sincronização novos para o Windows Vista
Os aplicativos que usam recursos do Direct3D 11 nesses sistemas operacionais também podem aproveitar o novo design para criação simultânea de objetos e listas de comandos de contexto adiadas para melhor escalabilidade para renderização multithread.
Resumo
Com um design cuidadoso que minimiza as interações entre threads, você pode obter ganhos substanciais de desempenho com a programação multithread sem adicionar complexidade excessiva ao seu código. Isso permitirá que o código do jogo acompanhe a próxima onda de melhorias de processador e forneça experiências de jogos cada vez mais atraentes.
Referências
- Jim Beveridge & Robert Weiner, Multithreading Applications in Win32, Addison-Wesley, 1997
- Chuck Walbourn, Tempo de Jogo e Processadores Multicore, Microsoft Corporation, 2005
- Biblioteca MSDN: GetLogicalProcessorInformation
- OpenMP