A alocação e a liberação de um grande pedaço de memória na inicialização "limpa a memória?"

18

O livro Game Coding Complete, quarta edição , capítulo 5 ( Inicialização e desligamento do jogo ), seção Verificando memória, contém este interessante exemplo de código:

bool CheckMemory(const DWORDLONG physicalRAMNeeded, const DWORDLONG virtualRAMNeeded)
{
    MEMORYSTATUSEX status; 
    GlobalMemoryStatusEx(&status);
    if (status.ullTotalPhys < physicalRAMNeeded) 
    {
        // you don’t have enough physical memory. Tell the player to go get a 
        // real computer and give this one to his mother. 
        GCC_ERROR("CheckMemory Failure: Not enough physical memory."); 
        return false;
    }
    // Check for enough free memory.
    if (status.ullAvailVirtual < virtualRAMNeeded) 
    {
        // you don’t have enough virtual memory available.
        // Tell the player to shut down the copy of Visual Studio running in the 
        // background, or whatever seems to be sucking the memory dry. 
        GCC_ERROR("CheckMemory Failure: Not enough virtual memory.");
        return false;
    }

    char *buff = GCC_NEW char[virtualRAMNeeded]; 
    if (buff)
    {
        delete[] buff;
    }
    else
    {
        // even though there is enough memory, it isn't available in one
        // block, which can be critical for games that manage their own memory 
        GCC_ERROR("CheckMemory Failure: Not enough contiguous memory."); 
        return false;
    }
}

Isso levanta algumas questões.

A primeira parte apenas pergunta ao sistema operacional (Windows) quanta RAM física está disponível. A parte curiosa é a segunda, que aloca um grande pedaço de memória e a libera imediatamente:

char *buff = GCC_NEW char[virtualRAMNeeded]; 
if (buff)
{
    delete[] buff;
}

O autor continua explicando:

... essa função aloca e libera imediatamente um enorme bloco de memória. Isso tem o efeito de fazer com que o Windows limpe qualquer lixo acumulado no gerenciador de memória e verifique novamente se você pode alocar um bloco contíguo do tamanho necessário. Se a chamada for bem-sucedida, você basicamente executará o equivalente a uma máquina Zamboni na memória do sistema, preparando-a para o seu jogo atingir o gelo ...

Mas tenho minhas reservas nisso.

"Limpando o lixo acumulado no gerenciador de memória?" Verdade? Se o jogo acabou de começar, não deveria haver lixo?

"Certificando-se de que você pode alocar um bloco contíguo?" No caso muito específico em que você mesmo gerenciará a memória, isso faria algum sentido, mas ainda assim, se você alocar muita memória diretamente do bastão, praticamente impossibilitará a execução de qualquer outro aplicativo o sistema enquanto o seu estiver ligado.

Além disso, não é provável que isso force o sistema operacional a comprometer toda essa memória e, como consequência, despeje muita memória no espaço em disco de troca, retardando muito a inicialização do aplicativo?

Esta é realmente uma boa prática?

glampert
fonte
3
A maioria dos sistemas operacionais modernos vai fazer absolutamente nada quando um aplicativo aloca uma grande área de memória, eles usam alocação otimista e realmente não fazer nada até que você preencha a memória, eu não posso imaginar isso fazendo nada mais do que ser um potencialmente lento no-op
Vality
Limpar uma longa faixa de terra na selva, esculpir um rádio, fones de ouvido etc. em madeira resulta em aviões pousando e entregando suprimentos?
Dan Neely
10
Jogo codificação completa contém um monte de bobagens emparelhado com um monte de não-entendimento-C ++ e C olhares-que-um-pouco-como-C ++ (também demonstrou nesta amostra, verificando o resultado de operator newpara nullptr), se me permite dizer. A melhor coisa que você pode fazer com esse livro é acender sua chaminé. Alocar e liberar um grande bloco de memória, é claro , não "limpa" a memória.
Damon
@ Damon, não verifiquei, mas desconfio que eles tenham sobrecarregado o newoperador global para retornar nulo em vez de jogar bad_alloc. Se não, então sim, esse código é ainda mais absurdo: P
glampert
11
@ glampert: Mesmo assumindo que é o caso, operator deleteé necessário aceitá-lo nullptre tratá-lo como não operacional. Qualquer sobrecarga global que não faz isso é interrompida. O que significa que é absurdo de qualquer maneira. Assim como assumir que alocar um enorme bloco de memória e liberá-lo "magicamente" fará algo bom. Na melhor das hipóteses, não causará nenhum dano (provavelmente, uma vez que as páginas nem sequer são tocadas ... caso contrário, poderá trocar algumas páginas do seu conjunto de trabalho que você precisará recarregar mais tarde).
Damon

Respostas:

12

Uma coisa que a pré-alocação de uma grande parte da memória pode fazer, se o computador estiver com pouca memória RAM, pode forçar o sistema operacional a liberar espaço extra trocando partes do espaço da memória de outros programas no disco.

Como essa troca geralmente é uma operação muito lenta que congela seu programa enquanto está acontecendo, pode haver alguma vantagem em garantir que, se isso acontecer de qualquer maneira, ocorrerá antes do início do jogo e não no meio da jogabilidade.

Dito isto, forçar uma troca para disco como este não é exatamente 100% confiável:

  • Em muitas implementações de memória virtual, apenas alocar memória não aciona nenhuma troca; apenas acontece quando você acessa pela primeira vez cada página da memória alocada. (Isso certamente é verdade nas versões modernas do Linux, com a confirmação de memória ativada, como é por padrão; não sei ao certo como as diferentes versões do Windows lidam com isso.)

    Portanto, se você realmente deseja que esse código "limpe a memória", provavelmente adicione uma memset()chamada ou equivalente para gravar dados em todas as partes da matriz (ou pelo menos nos primeiros physicalRAMNeededbytes dele) antes de liberá-lo.

  • Além disso, forçar o sistema operacional a trocar outros programas da RAM como esse não significa que eles ficarão fora dele. O Windows é um sistema operacional multitarefa e, assim que um desses programas trocados quiser executar novamente e usar seus dados trocados, ele será trocado novamente, potencialmente congelando seu jogo novamente.

    Portanto, esse truque geralmente é útil apenas se a RAM estiver sendo consumida por grandes pedaços de dados que não estão sendo acessados ​​ativamente (como documentos grandes deixados abertos em segundo plano em algum software de edição). Os navegadores da Web tendem a ser particularmente problemáticos nesse sentido, pois, mesmo que você não esteja usando uma página da Web aberta em alguma guia em segundo plano, a página ainda pode ter scripts em execução nela que forçam a manutenção na RAM.

    (Não são formas de um programa para realmente dizer o OS para bloquear uma parte do seu espaço de memória na RAM e impedem de ser trocados, mas estes normalmente exigem privilégios elevados. Em qualquer caso, o código que você postou não parece ser usando esses métodos.)

Basicamente, esse truque parece útil apenas nessa situação limítrofe relativamente estreita, na qual o usuário teria RAM suficiente para executar seu jogo (e qualquer outro software que esteja sendo executado ativamente em segundo plano) sem trocar, mas atualmente a RAM está cheia de arquivos abertos inativos e não utilizados ou outros dados que podem e devem ser trocados para o disco enquanto o jogo está em execução. Nessas situações, pode ser eficaz para tornar seu jogo mais suave e responsivo, substituindo operações ocasionais de troca pequena durante o jogo por uma única durante a inicialização.

Ilmari Karonen
fonte
11
Pelo meu conhecimento limitado de gerenciamento de memória de baixo nível, posso dizer que a decisão de trocar ou não uma página específica é muito mais complexa e leva muito mais fatores em consideração do que simplesmente a quantidade de memória solicitada e a quantidade de memória disponível . Simplificar isso demais, e geralmente confiar em suposições, não é muito sábio.
Panda Pajama
3
Eu acho que é importante lembrar que todas essas são suposições que podem funcionar, não funcionam ou parecem funcionar, mas por razões completamente não relacionadas. Até onde eu sei, nenhum dos comportamentos apontados nesta resposta está documentado, e seria imprudente confiar neles. Uma atualização do sistema operacional, alterações nas configurações, alterações no compilador ou praticamente qualquer coisa pode fazer com que isso pare de funcionar ou tenha um impacto negativo no desempenho.
Panda Pajama
26

Não sei quantos anos esse artigo tem, mas eu diria que é bem antigo. No Windows moderno (XP e mais recente, mas especialmente nas versões de 64 bits), eu diria que fazer algo assim terá pouco ou nenhum impacto negativo na inicialização do seu jogo.

Antes de tudo, lembre-se de que o espaço de endereço é virtualizado para cada processo. No que diz respeito ao seu processo, você tem todo o espaço de endereço para si e sempre terá memória contígua (virtual), independentemente de essa memória ser fisicamente contígua ou não.

De fato, se a memória é ou não contígua na memória física não é algo sobre o qual você tem controle. O sistema operacional escolherá como alocar os blocos físicos como achar melhor, e nada que você faça no nível do aplicativo poderá afetar os benefícios (se houver) de ter memória física contígua.

Você precisa se preocupar com a fragmentação da memória virtual se continuar alocando e liberando memória de tamanhos diferentes por um período muito longo. Obviamente, isso é muito menos relevante com um espaço de endereço de 64 bits. Os jogos não costumam rodar há meses, então você provavelmente não precisará se preocupar com isso, a menos que falemos de servidores de jogos de longa duração.

Mesmo se você alocar muita memória de tamanhos totalmente diferentes em uma máquina de 32 bits, as alocações de memória modernas são muito boas, portanto, é improvável que você tenha problemas de fragmentação da memória virtual, a menos que esteja procurando ativamente por elas.

Para ser sincero com você, não sei o que esse escritor está falando. Mesmo que o espaço de endereço tenha sido compartilhado e você tenha recebido um enorme bloco contíguo na memória física, liberando-o, você está dizendo que não se importa mais com ele. Existem outros programas sendo executados em segundo plano, fazendo suas próprias alocações; portanto, mesmo que seu grande malloc tenha sido bem-sucedido, ninguém garantirá que o próximo será bem-sucedido.

Eu acho que é um caso de "funcionou por algum motivo que eu realmente não entendo, então inventei algo para explicá-lo".

Panda Pajama
fonte
10
A RAM não é como um disco rígido rotativo, onde ter blocos contíguos é, de alguma forma, "melhor" do que não. Isso não é verdade. acesso à RAM contíguo é uma ordem de magnitude mais rápida que o acesso aleatório. Os chips de RAM são divididos em seções para as quais há uma certa latência para alternar e, mais importante, as falhas de cache L1 e L2 afetarão drasticamente seu desempenho quando a memória estiver dispersa.
CaptainCodeman
@CaptainCodeman: Eu tenho que aceitar que isso sai do meu campo de conhecimento, mas de qualquer forma, como programador de aplicativos para Windows, você não tem controle sobre se sua memória é fisicamente contígua ou não. Pedir um grande bloco de memória não mudará muito.
Panda Pajama
@CaptainCodeman realmente tem bloco virtual contíguo estar em mod contíguo bloco 2 ^ N na RAM irá acelerá-lo devido à alocação de linha de cache
catraca aberração
8
@CaptainCodeman Sim, o acesso seqüencial à DRAM é mais rápido, mas estamos falando do mapeamento virtual-> físico que ocorre em páginas de 4 KB (ou mais), portanto, se esse mapeamento é seqüencial ou não, não deve ter muito impacto na memória velocidade de leitura. Além disso, a pré-busca de cache e coisas assim funcionam no espaço de endereço virtual, não físico.
Nathan Reed
11
@NathanReed Fair bastante, e obrigado pelo esclarecimento. Eu estava apenas expressando que a velocidade de acesso à RAM não é totalmente uniforme no espaço de memória, como foi sugerido.
CaptainCodeman