É hora de chegar ao fundo disso.
Serviço de inteligência
Queria primeiro verificar se alguém já havia resolvido o problema. Mas eu só encontrei histórias sobre a grande complexidade do jogo , e é por isso que leva tanto tempo para carregar, histórias de que a arquitetura p2p da rede é lixo (embora não seja), algumas maneiras complexas de carregar no modo de história e depois em uma única sessão e mais alguns mods para pular o vídeo do logotipo R * no momento da inicialização. Depois de ler um pouco mais os fóruns, descobri que você pode economizar 10-30 segundos se usar todos esses métodos juntos!
Enquanto isso, no meu computador ...
Benchmark
Carregamento de cena: ~ 1m 10s Carregamento online: ~ 6m Sem menu de inicialização, do logotipo R * à jogabilidade (sem login no Social Club. Por cento antigo, mas decente: AMD FX-8350 SSD barato: KINGSTON SA400S37120G Precisa comprar RAM: 2x Kingston 8192 MB (DDR3-1337) 99U5471 GPU normal: NVIDIA GeForce GTX 1070
Eu sei que meu hardware está desatualizado, mas que diabos, o que poderia atrasar meus downloads em 6x quando online? Não consegui medir a diferença ao carregar do modo de história para o online, como outros fizeram . Mesmo que funcione, a diferença é pequena.
Eu não estou sozinho
De acordo com esta pesquisa , o problema é generalizado o suficiente para ser um pouco incômodo para mais de 80% dos jogadores. Já se passaram sete anos!
Eu fiz uma pequena busca por informações sobre aqueles 20% sortudos que carregam em menos de três minutos e encontrei vários benchmarks com PCs de jogos de topo e um tempo de carregamento online de cerca de dois minutos. Eu teria
Por que o modo de história deles ainda leva cerca de um minuto para carregar? (a propósito, vídeos com logotipos não foram levados em consideração ao inicializar a partir do M.2 NVMe). Além disso, leva apenas um minuto para fazer o download do Story Mode online, enquanto eu tenho cerca de cinco. Eu sei que o hardware deles é muito melhor, mas não cinco vezes.
Medições de alta precisão
Armado com uma ferramenta poderosa como o Gerenciador de Tarefas , comecei a encontrar o gargalo.
Demora quase um minuto para carregar recursos compartilhados, que são necessários para o modo de história e online (quase no mesmo nível dos PCs de última geração), então o GTA carrega completamente um núcleo da CPU por quatro minutos, sem fazer mais nada.
Uso de disco? Não! Uso da rede? Há um pouco, mas depois de alguns segundos cai principalmente para zero (exceto para o carregamento de banners de informação rotativos). Uso da GPU? Zero. Memória? Absolutamente nada ...
O que é, mineração de Bitcoin ou algo assim? Eu posso sentir o cheiro do código aqui. Código muito ruim.
Stream único
Meu antigo processador AMD tem oito núcleos e ainda é ótimo, mas é um modelo antigo. Foi feito quando o desempenho de single thread da AMD era muito inferior ao da Intel. Esta é provavelmente a principal razão para tais diferenças nos tempos de carregamento.
O que é estranho é a forma como a CPU é usada. Eu esperava uma grande quantidade de leituras de disco ou uma tonelada de solicitações de rede para configurar sessões em uma rede p2p. Mas é isso? Provavelmente há algum engano aqui.
Profiling
Um criador de perfil é uma ótima maneira de encontrar gargalos de CPU. Há apenas um problema - a maioria deles depende da instrumentação do código-fonte para obter uma imagem perfeita do que está acontecendo no processo. E eu não tenho o código-fonte. Eu também não preciso de leituras perfeitas de microssegundos, tenho um gargalo de 4 minutos .
Então, bem-vindo ao empilhar a amostragem. Para aplicativos de código fechado, esta é a única opção. Redefina a pilha do processo em execução e a localização do ponteiro de instrução atual para construir a árvore de chamada nos intervalos especificados. Em seguida, sobreponha-os e obtenha estatísticas sobre o que está acontecendo. Só conheço um criador de perfil que pode fazer isso no Windows. E não é atualizado há mais de dez anos. É Luke Stackwalker ! Alguém, por favor, dê um pouco de amor ao Luke :)
Normalmente, Luke agruparia as mesmas funções, mas não tenho símbolos de depuração, então tive que olhar os endereços próximos para procurar lugares comuns. E o que vemos? Não um, mas dois gargalos!
No buraco do coelho
Depois de pegar emprestado de um amigo meu uma cópia perfeitamente legítima do desmontador padrão (não, eu realmente não posso pagar ... Eu nunca vou dominar a hidra ), fui desmontar o GTA.
Parece completamente errado. Sim, a maioria dos principais jogos tem proteção de engenharia reversa integrada para mantê-los protegidos de piratas, trapaceiros e modders. Não que isso os tenha impedido ...
Parece que algum tipo de ofuscação / criptografia foi aplicado aqui, substituindo a maioria das instruções por rabiscos. Não se preocupe, você só precisa reiniciar a memória do jogo enquanto ele executa a parte que queremos assistir. As instruções devem ser desofuscadas antes do lançamento, de uma forma ou de outra. Eu tinha o Process Dump por perto , então peguei, mas há muitas outras ferramentas para tarefas semelhantes.
Problema 1: isso é ... strlen?!
Uma análise mais aprofundada do despejo revelou um dos endereços com um determinado rótulo
strlen
que vem de algum lugar! Descendo a pilha de chamadas, o endereço anterior é marcado como
vscan_fn
, e depois disso os rótulos acabam, embora eu tenha quase certeza que sim
sscanf
.
Ele analisa algo. Mas o que? A análise lógica vai demorar muito, então decidi descartar alguns exemplos do processo em execução usando x64dbg . Após algumas etapas de depuração, descobriu-se que este é ... JSON! Ele analisa JSON. A whopping megabytes dez de JSON com 63.000 itens .
...,
{
"key": "WP_WCT_TINT_21_t2_v9_n2",
"price": 45000,
"statName": "CHAR_KIT_FM_PURCHASE20",
"storageType": "BITFIELD",
"bitShift": 7,
"bitSize": 1,
"category": ["CATEGORY_WEAPON_MOD"]
},
...
O que é isso? A julgar por alguns dos links, estes são os dados para o "diretório de comércio online". Presumo que contenha uma lista de todos os itens e atualizações possíveis que você pode comprar no GTA Online.
Para esclarecer alguma confusão, acredito que esses sejam itens de dinheiro do jogo que não estão diretamente relacionados às microtransações .
10 megabytes? Em princípio, nem tanto. Embora
sscanf
não seja usado da maneira mais ideal, mas é claro que não é tão ruim? Bem ...
Sim, esse procedimento vai levar algum tempo ... Para ser honesto, eu não tinha ideia de que a maioria das implementações
sscanf
chamam
strlen
então não posso culpar o desenvolvedor que escreveu isso. Eu acho que foi apenas escaneando byte por byte e poderia parar em
NULL
.
Problema 2: vamos usar um hash ... array?
Acontece que o segundo criminoso é chamado logo após o primeiro. Mesmo na mesma construção
if
, como você pode ver nesta descompilação feia:
todos os rótulos são meus e não tenho ideia de como as funções / parâmetros são realmente chamados.
Segundo problema? Imediatamente após o elemento ser analisado, ele é armazenado em uma matriz (ou lista embutida C ++? Não tenho certeza). Cada entrada se parece com isto:
struct {
uint64_t *hash;
item_t *item;
} entry;
E antes de salvar? Ele verifica todo o array comparando o hash de cada elemento, esteja ele na lista ou não. Com 63 mil lançamentos, isso é aproximadamente
(n^2+n)/2 = (63000^2+63000)/2 = 1984531500
, se não me engano nos meus cálculos. E esses são cheques inúteis. Você tem hashes exclusivos, por que não usar uma tabela de hash.
Durante a engenharia reversa, dei um nome
hashmap
, mas é óbvio
_hashmap
. E então fica ainda mais interessante. Esta lista de matriz hash está vazia antes de carregar o JSON. E todos os elementos em JSON são únicos! Eles nem precisam verificar se estão na lista ou não! Eles ainda têm um recurso de inserção direta de elementos! Basta usá-lo! Sério, pessoal, que porra é essa !?
Prova de conceito
Tudo isso é ótimo, mas ninguém vai me levar a sério até que eu escreva o código real para acelerar o carregamento e fazer um título clickbait para uma postagem.
O plano é o seguinte. 1. Escreva .dll, 2. implemente-o no GTA, 3. conecte algumas funções, 4. ???, 5. lucro. Tudo é extremamente simples.
O problema com JSON não é trivial, não consigo realmente substituir seu analisador. Parece mais realista substituir sscanf por um que não dependa de strlen. Mas existe uma maneira ainda mais fácil.
- gancho strlen
- espere por uma longa fila
- Início e comprimento do "cache"
- se outra chamada vier dentro do intervalo da string, retorne o valor em cache
Algo assim:
size_t strlen_cacher(char* str)
{
static char* start;
static char* end;
size_t len;
const size_t cap = 20000;
// ""
if (start && str >= start && str <= end) {
// calculate the new strlen
len = end - str;
// ,
//
if (len < cap / 2)
MH_DisableHook((LPVOID)strlen_addr);
// !
return len;
}
//
// JSON
// strlen
len = builtin_strlen(str);
//
//
if (len > cap) {
start = str;
end = str + len;
}
// ,
return len;
}
Quanto ao problema da matriz hash, simplesmente ignoramos todas as verificações e inserimos os elementos diretamente, pois sabemos que os valores são únicos.
char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item)
{
//
uint64_t not_a_hashmap = catalog + 88;
// , ,
if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))
return 0;
//
netcat_insert_direct(not_a_hashmap, key, &item);
//
// .dll, :)
if (*key == 0x7FFFD6BE) {
MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);
unload();
}
return 1;
}
O código-fonte PoC completo está aqui .
resultados
Então, como isso funciona?
Tempo de carregamento anterior online: cerca de 6m Tempo com verificação de patch para duplicatas: 4m 30s Tempo com analisador JSON: 2m 50s Tempo com dois patches juntos: 1m 50s (6 * 60 - (1 * 60 + 50)) / (6 * 60) = 69,4% de melhoria no tempo (aula!)
Sim, droga, funcionou! :))
Isso provavelmente não resolverá todos os problemas de inicialização - pode haver outros gargalos em sistemas diferentes, mas é um buraco tão grande que não tenho ideia de como R * não percebeu isso ao longo dos anos.
Resumo
- Há um único gargalo ao lançar GTA Online
- Acontece que o GTA está lutando para analisar um arquivo JSON de 1 MB
- O próprio analisador JSON é mal feito / ingênuo e
- Após a análise, há um procedimento lento para remover duplicatas
R * por favor corrija
Se a informação chegar de alguma forma aos engenheiros da Rockstar, o problema pode ser resolvido em poucas horas pelos esforços de um desenvolvedor. Por favor, rapazes, façam algo sobre isso: <
Você pode ir para uma tabela de hash para remover duplicatas ou pular a deduplicação na inicialização como uma solução rápida. Para um analisador JSON, basta substituir a biblioteca por uma com mais desempenho. Não acho que haja uma opção mais fácil.
ty <3