Il est temps d'aller au fond des choses.
Service de renseignements
Je voulais d'abord vérifier si quelqu'un avait déjà résolu le problème. Mais je n'ai trouvé que des histoires sur la grande complexité du jeu , c'est pourquoi le chargement prend si longtemps, des histoires que l' architecture p2p du réseau est des déchets (bien que ce ne soit pas le cas), des moyens complexes de charger en mode histoire, puis dans une seule session et quelques autres mods pour ignorer la vidéo du logo R * au démarrage. Après avoir lu un peu plus les forums, j'ai découvert que vous pouvez économiser 10 à 30 secondes si vous utilisez toutes ces méthodes ensemble!
Pendant ce temps, sur mon ordinateur ...
Référence
Chargement de la scène: ~ 1m 10s Chargement en ligne: ~ 6m Pas de menu de démarrage, du logo R * au gameplay (pas de connexion au Social Club. Pourcentage ancien, mais décent: AMD FX-8350 SSD bon marché: KINGSTON SA400S37120G Besoin d'acheter de la RAM: 2x Kingston 8192 Mo (DDR3-1337) 99U5471 GPU normal: NVIDIA GeForce GTX 1070
Je sais que mon matériel est obsolète, mais diable, qu'est-ce qui pourrait ralentir mes téléchargements de 6x en ligne? Je n'ai pas pu mesurer la différence lors du chargement du mode histoire à en ligne comme d'autres l'ont fait . Même si cela fonctionne, la différence est minime.
je ne suis pas seul
Selon cette enquête , le problème est suffisamment répandu pour être légèrement ennuyeux pour plus de 80% des joueurs. Cela fait sept ans maintenant!
J'ai fait une petite recherche d'informations sur ces ~ 20% de chanceux qui se chargent en moins de trois minutes, et j'ai trouvé plusieurs points de repère avec les meilleurs PC de jeu et un temps de chargement en ligne d'environ deux minutes. J'aurais
Pourquoi leur mode histoire prend-il encore environ une minute à se charger? (d'ailleurs, les vidéos avec logos n'étaient pas prises en compte lors du démarrage à partir de M.2 NVMe). De plus, le téléchargement depuis le mode histoire en ligne ne prend qu'une minute, alors que j'en ai environ cinq. Je sais que leur matériel est bien meilleur, mais pas cinq fois.
Mesures de haute précision
Armé d'un outil puissant comme Task Manager , je me suis mis à trouver le goulot d'étranglement.
Il faut près d'une minute pour charger les ressources partagées, nécessaires à la fois en mode histoire et en ligne (presque à égalité avec les PC haut de gamme), puis GTA charge complètement un cœur de processeur pendant quatre minutes, sans rien faire d'autre.
Utilisation du disque? Pas! Utilisation du réseau? Il y en a un peu, mais au bout de quelques secondes il tombe principalement à zéro (sauf pour le chargement des bannières d'information rotatives). Utilisation du GPU? Zéro. Mémoire? Rien du tout ...
Qu'est-ce que c'est, l'exploitation minière de Bitcoin ou quelque chose comme ça? Je peux sentir le code ici. Très mauvais code.
Flux unique
Mon ancien processeur AMD a huit cœurs, et c'est toujours génial, mais c'est un ancien modèle. Il a été fait à l'époque où les performances d'un seul thread d'AMD étaient bien inférieures à celles d'Intel. C'est probablement la raison principale de ces différences de temps de chargement.
Ce qui est étrange, c'est la façon dont le processeur est utilisé. Je m'attendais à une énorme quantité de lectures de disque ou à une tonne de demandes réseau pour configurer des sessions sur un réseau p2p. Mais est-ce vrai? Il y a probablement une erreur ici.
Profilage
Un profileur est un excellent moyen de trouver les goulots d'étranglement du processeur. Il n'y a qu'un seul problème - la plupart d'entre eux s'appuient sur l'instrumentation du code source pour obtenir une image parfaite de ce qui se passe dans le processus. Et je n'ai pas le code source. Je n'ai pas non plus besoin de lectures parfaites en microsecondes, j'ai un goulot d'étranglement de 4 minutes .
Alors, bienvenue à l'échantillonnage de la pile. Pour les applications fermées, c'est la seule option. Réinitialisez la pile de processus en cours d'exécution et l'emplacement du pointeur d'instruction actuel pour créer l'arborescence des appels aux intervalles spécifiés. Ensuite, superposez-les et obtenez des statistiques sur ce qui se passe. Je ne connais qu'un seul profileur capable de le faire sous Windows. Et il n'a pas été mis à jour depuis plus de dix ans. C'est Luke Stackwalker ! Quelqu'un, s'il vous plaît, donnez de l'amour à Luke :)
Normalement, Luke regroupait les mêmes fonctions, mais je n'ai pas de symboles de débogage, donc j'ai dû regarder les adresses à proximité pour chercher des endroits communs. Et que voyons-nous? Pas un, mais deux goulots d'étranglement!
Dans le trou de lapin
Après avoir emprunté à un de mes amis une copie parfaitement légitime du désassembleur standard (non, je ne peux vraiment pas me le permettre ... je maîtriserai jamais l' hydre ), je suis allé démonter la GTA.
Ça a l'air complètement faux. Oui, la plupart des meilleurs jeux ont une protection intégrée contre l'ingénierie inverse pour les protéger des pirates, des tricheurs et des moddeurs. Non pas que cela les ait jamais arrêtés ...
On dirait qu'une sorte d'obfuscation / cryptage a été appliquée ici, remplaçant la plupart des instructions par du charabia. Ne vous inquiétez pas, il vous suffit de réinitialiser la mémoire du jeu pendant qu'il effectue la partie que nous voulons regarder. Les instructions doivent être désobfusquées avant le lancement d'une manière ou d'une autre. J'avais Process Dump à proximité , alors je l'ai pris, mais il existe de nombreux autres outils pour des tâches similaires.
Problème 1: est-ce ... strlen?!
Une analyse plus approfondie de la décharge a révélé l'une des adresses avec une certaine étiquette
strlen
qui vient de quelque part! En descendant la pile d'appels, l'adresse précédente est marquée comme
vscan_fn
, et après cela, les étiquettes finissent, même si je suis à peu près sûr que c'est le cas
sscanf
.
Il analyse quelque chose. Mais quoi? L'analyse logique prendra des années, j'ai donc décidé de vider certains échantillons du processus en cours en utilisant x64dbg . Après quelques étapes de débogage, il s'avère que c'est ... JSON! Il analyse JSON. Un énorme dix mégaoctets de JSON avec 63000 éléments .
...,
{
"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"]
},
...
Qu'est-ce que c'est? À en juger par certains des liens, il s'agit des données du «répertoire du commerce en ligne». Je suppose qu'il contient une liste de tous les éléments et mises à niveau possibles que vous pouvez acheter dans GTA Online.
Pour dissiper une certaine confusion, je pense que ce sont des objets en argent dans le jeu qui ne sont pas directement liés aux microtransactions .
10 mégaoctets? En principe, pas tellement. Bien que
sscanf
non utilisé de la manière la plus optimale, mais bien sûr ce n'est pas si mal? Eh bien ...
Oui, une telle procédure prendra du temps ... Pour être honnête, je ne savais pas que la plupart des implémentations
sscanf
appellent
strlen
donc je ne peux pas vraiment blâmer le développeur qui a écrit ceci. Je suppose qu'il analysait simplement octet par octet et pouvait s'arrêter
NULL
.
Problème 2: utilisons un tableau de hachage ...?
Il s'avère que le deuxième criminel est appelé juste après le premier. Même dans la même construction
if
, comme vous pouvez le voir à partir de cette vilaine décompilation:
toutes les étiquettes sont à moi et je n'ai aucune idée de ce que les fonctions / paramètres sont réellement appelés.
Deuxième problème? Immédiatement après l'analyse de l'élément, il est stocké dans un tableau (ou une liste en ligne C ++? Pas sûr). Chaque entrée ressemble à ceci:
struct {
uint64_t *hash;
item_t *item;
} entry;
Et avant d'économiser? Il vérifie l' ensemble du tableau en comparant le hachage de chaque élément, qu'il soit dans la liste ou non. Avec 63 mille entrées, c'est approximativement
(n^2+n)/2 = (63000^2+63000)/2 = 1984531500
, si je ne me trompe pas dans mes calculs. Et ce sont pour la plupart des chèques inutiles. Vous avez des hachages uniques, pourquoi ne pas utiliser une table de hachage.
Lors de la rétro-ingénierie, je l'ai nommé
hashmap
, mais c'est évident
_hashmap
. Et puis ça devient encore plus intéressant. Cette liste de tableaux de hachage est vide avant de charger le JSON. Et tous les éléments de JSON sont uniques! Ils n'ont même pas besoin de vérifier s'ils sont sur la liste ou non! Ils ont même une fonction d'insertion directe d'élément! Utilisez-le! Sérieusement, les gars, c'est quoi ce bordel!?
Preuve de concept
Tout cela est génial, mais personne ne me prendra au sérieux jusqu'à ce que j'écrive le code réel pour accélérer le chargement afin de créer un titre clickbait pour un article.
Le plan est le suivant. 1. Écrivez .dll, 2. implémentez-le dans GTA, 3. accrochez certaines fonctions, 4. ???, 5. profit. Tout est extrêmement simple.
Le problème avec JSON n'est pas trivial, je ne peux pas vraiment remplacer leur analyseur. Il semble plus réaliste de remplacer sscanf par un autre qui ne dépend pas de strlen. Mais il existe un moyen encore plus simple.
- crochet strlen
- attendre une longue file
- Début et durée du "cache"
- si un autre appel entre dans la plage de la chaîne, renvoie la valeur mise en cache
Quelque chose comme ça:
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;
}
En ce qui concerne le problème du tableau de hachage, nous ignorons entièrement toutes les vérifications et insérons les éléments directement, car nous savons que les valeurs sont uniques.
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;
}
Le code source complet du PoC est ici .
résultats
Alors, comment ça marche?
Temps de chargement précédent en ligne: environ 6m Durée de la vérification des patchs pour les doublons: 4 min 30 s Temps avec l'analyseur JSON: 2 min 50 s Temps avec deux patchs ensemble: 1 min 50 s (6 * 60 - (1 * 60 + 50)) / (6 * 60) = 69,4% d'amélioration dans le temps (classe!)
Oui, bon sang, ça a marché! :))
Cela ne résoudra probablement pas tous les problèmes de démarrage - il peut y avoir d'autres goulots d'étranglement sur différents systèmes, mais c'est un trou tellement béant que je n'ai aucune idée de la façon dont R * l'a manqué au fil des ans.
Résumé
- Il existe un goulot d'étranglement unique lors du lancement de GTA Online
- Il s'avère que GTA a du mal à analyser un fichier JSON de 1 Mo
- L'analyseur JSON lui-même est mal fait / naïf et
- Après l'analyse, il existe une procédure lente pour supprimer les doublons
R * veuillez corriger
Si les informations parviennent d'une manière ou d'une autre aux ingénieurs de Rockstar, le problème peut être résolu en quelques heures grâce aux efforts d'un développeur. Veuillez faire quelque chose à ce sujet: <
Vous pouvez soit accéder à une table de hachage pour supprimer les doublons, soit ignorer complètement la déduplication au démarrage comme solution rapide. Pour un analyseur JSON, remplacez simplement la bibliothèque par une bibliothèque plus performante. Je ne pense pas qu'il existe une option plus simple.
ty <3