Es ist Zeit, dem auf den Grund zu gehen.
Nachrichtendienst
Zuerst wollte ich überprüfen, ob jemand das Problem bereits gelöst hat. Aber ich habe nur Geschichten über die große Komplexität des Spiels gefunden , weshalb das Laden so lange dauert , Geschichten, dass die Netzwerk-P2P-Architektur Müll ist (obwohl dies nicht der Fall ist), einige komplexe Möglichkeiten zum Laden in den Story-Modus und dann in eine einzelne Sitzung und ein paar weitere Mods, um das R * -Logo-Video beim Booten zu überspringen. Nachdem ich die Foren ein wenig mehr gelesen hatte, stellte ich fest, dass Sie satte 10 bis 30 Sekunden sparen können, wenn Sie alle diese Methoden zusammen anwenden!
Inzwischen auf meinem Computer ...
Benchmark
Laden der Szene: ~ 1m 10s Online-Laden: ~ 6m Kein Boot-Menü, vom R * -Logo bis zum Gameplay (kein Social Club-Login. Alter, aber anständiger Prozentsatz: AMD FX-8350 Günstige SSD: KINGSTON SA400S37120G RAM muss gekauft werden: 2x Kingston 8192 MB (DDR3-1337) 99U5471 Normale GPU: NVIDIA GeForce GTX 1070
Ich weiß, dass meine Hardware veraltet ist, aber was könnte meine Downloads im Internet um das 6-fache verlangsamen? Ich konnte den Unterschied beim Laden vom Story-Modus in den Online-Modus nicht messen, wie es andere getan haben . Auch wenn es funktioniert, ist der Unterschied gering.
ich bin nicht alleine
Laut dieser Umfrage ist das Problem weit genug verbreitet, um für über 80% der Spieler leicht ärgerlich zu sein. Es ist jetzt sieben Jahre her!
Ich habe ein wenig Suche nach Informationen über die , ~ 20% Glücklichen, die Last in weniger als drei Minuten, und fand einige Benchmarks mit Top - Gaming - PCs und einer Online - Ladezeit von etwa zwei Minuten. Ich hätte jemanden
Warum dauert das Laden ihres Story-Modus noch etwa eine Minute? (Videos mit Logos wurden beim Booten von M.2 NVMe übrigens nicht berücksichtigt). Außerdem dauert es nur eine Minute, bis sie online aus dem Story-Modus heruntergeladen sind, während ich ungefähr fünf habe. Ich weiß, dass ihre Hardware viel besser ist, aber nicht fünfmal.
Hochpräzise Messungen
Mit einem leistungsstarken Tool wie dem Task-Manager habe ich mich auf die Suche nach dem Engpass gemacht.
Das Laden gemeinsam genutzter Ressourcen, die sowohl für den Story-Modus als auch online benötigt werden (fast auf dem Niveau von Top-End-PCs), dauert fast eine Minute. Anschließend lädt GTA einen CPU-Kern vier Minuten lang vollständig und unternimmt nichts anderes.
Festplattennutzung? Nein! Netzwerknutzung? Es gibt ein wenig, aber nach ein paar Sekunden fällt es hauptsächlich auf Null (mit Ausnahme des Ladens rotierender Informationsbanner). GPU-Nutzung? Null. Erinnerung? Gar nichts ...
Was ist das, Bitcoin-Mining oder so? Ich kann den Code hier riechen. Sehr schlechter Code.
Einzelner Stream
Mein alter AMD-Prozessor hat acht Kerne und es ist immer noch großartig, aber es ist ein altes Modell. Es wurde gemacht, als AMDs Single-Thread-Leistung viel niedriger war als die von Intel. Dies ist wahrscheinlich der Hauptgrund für solche Unterschiede in den Ladezeiten.
Was seltsam ist, ist die Art und Weise, wie die CPU verwendet wird. Ich hatte eine große Anzahl von Festplattenlesevorgängen oder eine Menge Netzwerkanforderungen erwartet, um Sitzungen in einem P2P-Netzwerk einzurichten. Aber ist es? Hier liegt wahrscheinlich ein Fehler vor.
Profilerstellung
Ein Profiler ist eine großartige Möglichkeit, CPU-Engpässe zu finden. Es gibt nur ein Problem: Die meisten von ihnen verlassen sich auf die Quellcode-Instrumentierung, um ein perfektes Bild davon zu erhalten, was dabei passiert. Und ich habe keinen Quellcode. Ich brauche auch keine perfekten Mikrosekundenwerte, ich habe einen 4-minütigen Engpass .
Willkommen zum Stapeln von Stichproben. Für Closed-Source-Anwendungen ist dies die einzige Option. Setzen Sie den laufenden Prozessstapel und die Position des aktuellen Befehlszeigers zurück, um den Aufrufbaum in den angegebenen Intervallen zu erstellen. Überlagern Sie sie dann und erhalten Sie Statistiken darüber, was los ist. Ich kenne nur einen Profiler, der dies unter Windows kann. Und es wurde seit über zehn Jahren nicht mehr aktualisiert. Es ist Luke Stackwalker ! Jemand, bitte gib Luke etwas Liebe :)
Normalerweise würde Luke die gleichen Funktionen gruppieren, aber ich habe keine Debug-Symbole, also musste ich nach Adressen in der Nähe suchen, um nach gemeinsamen Orten zu suchen. Und was sehen wir? Nicht einer, sondern zwei Engpässe!
In den Kaninchenbau
Nachdem ich mir von einem Freund eine absolut legitime Kopie des Standard-Disassemblers geliehen hatte (nein, ich kann es mir wirklich nicht leisten ... ich werde die Hydra jemals beherrschen ), ging ich zum Zerlegen des GTA.
Sieht völlig falsch aus. Ja, die meisten Top-Spiele verfügen über einen integrierten Reverse Engineering-Schutz, um sie vor Piraten, Cheats und Moddern zu schützen. Nicht, dass es sie jemals aufgehalten hätte ...
Es sieht so aus, als ob hier eine Art Verschleierung / Verschlüsselung angewendet wurde, die die meisten Anweisungen durch Kauderwelsch ersetzt. Keine Sorge, Sie müssen nur den Speicher des Spiels zurücksetzen, während es den Teil ausführt, den wir sehen möchten. Anweisungen müssen vor dem Start auf die eine oder andere Weise deobfusciert werden. Ich hatte Process Dump in der Nähe , also habe ich es genommen, aber es gibt viele andere Tools für ähnliche Aufgaben.
Problem 1: Ist das ... strlen ?!
Eine weitere Analyse des Dumps ergab eine der Adressen mit einem bestimmten Etikett
strlen
, das von irgendwoher kommt! Wenn Sie den Aufrufstapel durchgehen, wird die vorherige Adresse als markiert
vscan_fn
, und danach werden die Beschriftungen angezeigt, obwohl ich mir ziemlich sicher bin, dass dies der Fall ist
sscanf
.
Er analysiert etwas. Aber was? Das logische Parsen wird ewig dauern, daher habe ich beschlossen, einige Beispiele aus dem laufenden Prozess mit x64dbg zu sichern . Nach ein paar Schritten des Debuggens stellt sich heraus, dass dies ... JSON ist! Es analysiert JSON. Eine satte zehn Megabyte JSON mit 63.000 Artikel .
...,
{
"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"]
},
...
Was ist das? Nach einigen Links zu urteilen, sind dies die Daten für das "Online-Handelsverzeichnis". Ich gehe davon aus, dass es eine Liste aller möglichen Artikel und Upgrades enthält, die Sie in GTA Online kaufen können.
Um einige Verwirrung zu beseitigen, glaube ich, dass dies Geldgegenstände im Spiel sind, die nicht direkt mit Mikrotransaktionen zusammenhängen .
10 Megabyte? Im Prinzip nicht so sehr. Obwohl
sscanf
nicht optimal genutzt, aber natürlich nicht so schlimm? Nun ...
Ja, ein solches Verfahren wird einige Zeit dauern ... Um ehrlich zu sein, hatte ich keine Ahnung, dass die meisten Implementierungen dies
sscanf
erfordern
strlen
Daher kann ich dem Entwickler, der das geschrieben hat, nicht wirklich die Schuld geben. Ich würde vermuten, dass es nur Byte für Byte scannte und bei anhalten konnte
NULL
.
Problem 2: Verwenden wir ein Hash ... Array?
Es stellt sich heraus, dass der zweite Verbrecher direkt nach dem ersten gerufen wird. Selbst im selben Konstrukt
if
, wie Sie aus dieser hässlichen Dekompilierung sehen können:
Alle Bezeichnungen gehören mir und ich habe keine Ahnung, wie die Funktionen / Parameter tatsächlich heißen.
Zweites Problem? Unmittelbar nach dem Parsen des Elements wird es in einem Array (oder einer C ++ - Inline-Liste? Nicht sicher) gespeichert. Jeder Eintrag sieht ungefähr so aus:
struct {
uint64_t *hash;
item_t *item;
} entry;
Und vor dem Speichern? Es überprüft das gesamte Array, indem es den Hash jedes Elements vergleicht, ob es in der Liste enthalten ist oder nicht. Mit 63.000 Einträgen ist dies ungefähr
(n^2+n)/2 = (63000^2+63000)/2 = 1984531500
, wenn ich mich in meinen Berechnungen nicht irre. Und das sind meistens nutzlose Schecks. Sie haben eindeutige Hashes. Verwenden Sie eine Hash-Tabelle.
Während des Reverse Engineering habe ich es benannt
hashmap
, aber es ist offensichtlich
_hashmap
. Und dann wird es noch interessanter. Diese Hash-Array-Liste ist vor dem Laden des JSON leer. Und alle Elemente in JSON sind einzigartig! Sie müssen nicht einmal überprüfen, ob sie auf der Liste stehen oder nicht! Sie haben sogar eine direkte Elementeinfügefunktion! Benutze es einfach! Ernsthaft, Leute, was zum Teufel !?
Konzeptioneller Beweiß
All dies ist großartig, aber niemand wird mich ernst nehmen, bis ich den eigentlichen Code schreibe, um das Laden zu beschleunigen und einen Clickbait-Titel für einen Beitrag zu erstellen.
Der Plan ist wie folgt. 1. Schreiben Sie .dll, 2. implementieren Sie es in GTA, 3. haken Sie einige Funktionen ein, 4. ???, 5. profit. Alles ist sehr einfach.
Das Problem mit JSON ist nicht trivial, ich kann ihren Parser nicht wirklich ersetzen. Es erscheint realistischer, sscanf durch eine zu ersetzen, die nicht von strlen abhängt. Aber es gibt noch einen einfacheren Weg.
- Haken strlen
- warte auf eine lange Schlange
- "Cache" Start und Länge
- Wenn ein anderer Aufruf in den Bereich der Zeichenfolge fällt, geben Sie den zwischengespeicherten Wert zurück
Etwas wie das:
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;
}
Was das Hash-Array-Problem betrifft, überspringen wir einfach alle Prüfungen vollständig und fügen die Elemente direkt ein, da wir wissen, dass die Werte eindeutig sind.
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;
}
Der vollständige PoC-Quellcode ist hier .
Ergebnisse
Wie funktioniert es?
Vorherige Ladezeit online: ca. 6m Zeit mit Patch-Prüfung auf Duplikate: 4m 30s Zeit mit JSON-Parser: 2m 50s Zeit mit zwei Patches zusammen: 1m 50s (6 * 60 - (1 * 60 + 50)) / (6 * 60) = 69,4% Zeitverbesserung (Klasse!)
Ja, verdammt, es hat funktioniert! :))
Dies wird wahrscheinlich nicht alle Bootprobleme lösen - es kann andere Engpässe auf verschiedenen Systemen geben, aber es ist ein so klaffendes Loch, dass ich keine Ahnung habe, wie R * es im Laufe der Jahre verpasst hat.
Zusammenfassung
- Beim Starten von GTA Online tritt ein Engpass mit einem Thread auf
- Es stellt sich heraus, dass GTA Probleme hat, eine 1-MB-JSON-Datei zu analysieren
- Der JSON-Parser selbst ist schlecht gemacht / naiv und
- Nach dem Parsen gibt es ein langsames Verfahren zum Entfernen von Duplikaten
R * bitte korrigieren
Wenn die Informationen die Rockstar-Ingenieure irgendwie erreichen, kann das Problem innerhalb weniger Stunden durch die Bemühungen eines Entwicklers gelöst werden. Bitte machen Sie etwas dagegen: <
Sie können entweder zu einer Hash-Tabelle gehen, um Duplikate zu entfernen, oder die Deduplizierung beim Start als schnelle Lösung ganz überspringen. Ersetzen Sie für einen JSON-Parser einfach die Bibliothek durch eine leistungsfähigere. Ich glaube nicht, dass es eine einfachere Option gibt.
ty <3