GTAオンラインの読み込み時間を70%短縮する方法

画像


GTAオンラインは 読み込み速度が遅いことで有名です最近、新しいレイドミッションを完了するためにゲームを立ち上げましたが、7年前にリリースされたときと同じように、読み込みが遅いことショック受けました。



時は来た。今のところ、これの理由を理解してください。



インテリジェンスサービス



まず、この問題を誰かがすでに解決しているかどうかを確認したかったのです。見つかった結果のほとんどは、 ゲームの難易度、長い間ロードする必要があったこと、p2pネットワークアーキテクチャの跛行に関するストーリー (これは真実です)、ストーリーモードロードする複雑な方法 、および次に、単一のセッションにまとめて、R *ロゴの付いたオープニングビデオをスキップできるようにするmodをペアにします。一部の情報源は、これらすべての方法を一緒に使用すると、10〜30秒も節約できると報告しています。



一方、私のPCでは...



基準



: 1 10

-: 6

, R* ( social club ).



, : AMD FX-8350

SSD: KINGSTON SA400S37120G

: 2 Kingston 8192 (DDR3-1337) 99U5471

GPU: NVIDIA GeForce GTX 1070


車が古くなっていることは知っていますが、オンラインモードの読み込みが6倍遅くなるのはなぜですか?他の人が私の前に行ったように、「ストーリーを最初に、次にオンライン」のアップロード手法に違いは見つかり ませんでした。しかし、それが機能したとしても、結果は誤差の範囲内になります。



私は一人じゃない



この世論調査よると、 この問題は非常に広範囲に及んでいるため、プレーヤーベースの80%以上をわずかに激怒させています。R *の皆さん、実は7年が経ちました!





プレーヤーの18.8%が最も強力なコンピューターまたはコンソールを持っており、81.2%はかなり悲しい、35.1%はかなり悲しいです。



ロードに3分もかからない幸運なものの20%を検索した後、強力なゲーミングPCと約2分のオンラインロード時間を備えたベンチマークいくつか見つけまし 。 2分のロード時間を取得するには ハッキングをすべて殺します。読み込み時間はハードウェアによって異なるようですが、どういうわけか数字が足りません...



これらのベンチマークを実行している人々がストーリーモードをロードするのにまだ約1分かかるのはどうしてですか?(ちなみに、M.2のベンチマークでは、最初のロゴの表示時間は考慮されていません。)さらに、ストーリーモードからオンラインモードへの読み込みには1分しかかかりませんが、私の場合は5分以上かかります。彼らのテクニックは私のものよりはるかに優れていることを私は知っていますが、間違いなく5倍ではありません。



非常に正確な測定



タスクマネージャーのような強力なツールを備えた 私は、どのリソースがボトルネックになる可能性があるかを調査し始めました。





1分以内に、ストーリーモードの標準リソースが読み込まれ、その後、ゲームは4分以上プロセッサを読み込みます。



ストーリーモードとオンラインモードの両方で使用される共有リソース(強力なPCのベンチマークにほぼ等しいインジケーター)を1分間ロードした後、GTAは私のマシンの1つのコアを4分間ロードし、それ以外は何もしないことにしました。



ディスクアクセス?彼はそこにいません!ネットワークの使用?それほど多くはありませんが、ほんの数秒でトラフィックはほぼゼロになります(回転するバナーに情報をロードする場合を除く)。 GPUの使用?ゼロで。メモリ使用量?完全にフラットなグラフ...



何が起こっているのか、ゲームは暗号通貨か何かをマイニングしていますか?コードの臭いがし始めます。 非常に悪いコード



1つのストリームを制限する



私の古いAMDCPUには8つのコアがあり、それでも十分に機能しますが、それは昔に構築されました。当時、AMDプロセッサシングルスレッドパフォーマンス、Intelプロセッサのパフォーマンスはるかに下回っていました。これはロード時間のすべての違いを説明するわけではありませんが、最も重要なことを説明するはずです。



奇妙なことに、ゲームはCPUのみを使用し ます。私は、ディスクからロードされた大量のリソース、またはp2pネットワーク上にセッションを作成するための一連のネットワーク要求を期待していました。でもこれは?これはおそらくバグです。



プロファイリング



プロファイラーは、CPUのボトルネックを見つけるための優れた方法です。問題は1つだけです。それらのほとんどは、ソースコードを使用して、プロセスで何が起こっているのかを完全に把握しています。そして、私はそれを持っていません。ただし、マイクロ秒単位の正確な読み取り値も必要ありません。ボトルネックは4分間続きます。



スタックサンプリングが登場します。これは、クローズドソースアプリケーションを探索する唯一の方法です。実行中のプロセスと現在のコマンドポインタの場所のスタックダンプを実行して、指定された間隔で呼び出しツリーを構築します。次に、それらを合計して、何が起こっているかについての統計を取得します。 Windowsでこれを実行できるプロファイラーは1つだけです(ここでは間違っている可能性があります)。そしてそれは10年以上更新されていません。これは、 ルークStackwalker誰かがこのプロジェクトに彼らの愛を与えましょう。





犯人#1と#2。



Lukeは通常同じ関数をグループ化しますが、デバッグシンボルがないため、最も近いアドレスを目で調べて、それらが同じ場所であることを理解する必要があります。そして、私たちは何を見ますか?1つではなく、2つのボトルネック!



不思議な方向へ転がる



人気のある逆アセンブラーの完全に合法的なコピーを 友人から借りので (いいえ、私はそれを買う余裕がありません...私はどういうわけかギドラを学ぶ必要があります )、GTAを分解し始めました。





それはすべて完全に間違っているようです。多くの高予算のゲームには、海賊、詐欺師、改造者から身を守るためのリバースエンジニアリング保護が組み込まれています(言うまでもなく、それらを阻止します)。



ここでは、ある種の難読化/暗号化が使用されているようです。そのため、ほとんどのコマンドがギブリッシュに置き換えられています。ただし、心配しないでください。学習したい部分を実行するときに、ゲームのメモリをダンプする必要があります。コマンドを実行する前に、何らかの方法でコマンドの難読化を解除する必要があります。Process Dumpを手元に置いていまし たが、同様のことを実行できるツールは他にもたくさんあります。



問題#1:これは... strlen?!



難読化されなくなったダンプを逆アセンブルすると、アドレスの1つにどこからともなく取得されたラベルが付いていることがわかります。それ strlen



ですか?コールスタックの次の1つは、としてマークされ vscan_fn



、その後、ラベルがなくなりますが、確かにそうです sscanf









彼らは何かをこすります。しかし、何ですか?分解されたコードの解析には無限大がかかるため、x64dbgを使用して実行中のプロセスからいくつかのサンプルをダンプすることにし ました少しデバッグした後、これが... JSONであることがわかりました!JSONを解析します。なんと10メガバイトのJSONデータと63,000 近くの アイテム



...,
{
    "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"]
},
...
      
      





それは何ですか?一部の情報源によると、これは「オンラインストアディレクトリ」データのように見えます。 GTAオンラインで購入できるすべてのアイテムとアップグレードのリストが含まれていると仮定します。



明確化:これらはゲーム内のお金で購入されたアイテムであり、マイクロトランザクションに直接関係していないと思います



しかし、10メガバイトは些細なことです!そして、使用 sscanf



は最適ではないかもしれませんが、それはそれほど悪くはありませんか?上手 ...





メモリ内の10メガバイトのC文字列。1.ポインタを次の値に数バイト移動します。2.と呼びますsscanf(p, "%d", ...)



3.各小さな値(!?)を読み取りながら、各文字を10メガバイトで読み取ります。4.スキャンした値を返します。




はい、時間がかかります...正直なところ、ほとんどの実装が何をsscanf



呼び出して いるのかわから strlen



なかったので、これを書いた開発者のせいにすることはできません。このデータは単にバイトごとにスキャンされ、処理はで停止する可能性があることをお勧めし NULL



ます。



問題#2:ハッシュ...配列を使用しましょう?



2番目の犯人が最初の犯人のすぐ隣に呼ばれていることが判明しました。if



この醜い逆コンパイルで理解できるように、これらは両方とも同じステートメント呼び出され ます。





両方の問題は、すべてのアイテムの1つの大きな解析ループ内にあります。問題#1は解析であり、問​​題#2は保存です。



すべてのラベルは私が指定しています。関数とパラメーターが実際に何と呼ばれているのかわかりません。



2番目の問題は何ですか?アイテムが解析された直後に、それは配列(またはC ++埋め込みリスト?完全に明確ではありません)に保存されます。各アイテムは次のようになります。



struct {
    uint64_t *hash;
    item_t   *item;
} entry;
      
      





しかし、保存する前に何が起こりますか?このコードは、配列全体を要素ごとにチェックし 、アイテムのハッシュを比較して、リストにあるかどうかを確認します。私の計算が正しければ、約63千の要素でこれが(n^2+n)/2 = (63000^2+63000)/2 = 1984531500



チェック行います。それらのほとんどは役に立たない。独自の ハッシュがあるので、ハッシュマップを使ってみません か?





プロファイラーは、最初の2行がプロセッサーをロードしていることを示しています。ステートメントif



は最後にのみ実行されます。最後から2番目の行が件名を挿入します。




リバースエンジニアリングでは、この構造に名前を付けました hashmap



が、それは明らかです not_a_hashmap



そして、すべてが良くなります。このハッシュ/配列/リストは、JSONをロードする前は空です。そして、JSONのすべてのアイテムはユニークです!コードは、アイテムがリストにあるかどうかチェックする必要さえありませ アイテムを直接挿入する機能もあり、使うだけ!真剣に、なんてこった!?



コンセプトの証明



もちろん、これはすべて素晴らしいことですが、投稿のクリックベイトの見出しを書くことができるようにテストするまで、誰も私を真剣に受け止めません。



どんな計画ですか?書く .dll



、彼女のGTAを注入する、 いくつかの機能を傍受する、???、PROFIT!



JSONの問題は混乱を招き、パーサーの置き換えには非常に時間がかかります。sscanf



依存しない関数 に置き換えようとする方がはるかに現実的 strlen



です。しかし、さらに簡単な方法があります。



  • strlenをインターセプト
  • 長蛇の列を待つ
  • その開始と長さを「キャッシュ」する
  • 文字列内で再度呼び出された場合は、キャッシュされた値を返します


このようなもの:



size_t strlen_cacher(char* str)
{
  static char* start;
  static char* end;
  size_t len;
  const size_t cap = 20000;

  // if we have a "cached" string and current pointer is within it
  if (start && str >= start && str <= end) {
    // calculate the new strlen
    len = end - str;

    // if we're near the end, unload self
    // we don't want to mess something else up
    if (len < cap / 2)
      MH_DisableHook((LPVOID)strlen_addr);

    // super-fast return!
    return len;
  }

  // count the actual length
  // we need at least one measurement of the large JSON
  // or normal strlen for other strings
  len = builtin_strlen(str);

  // if it was the really long string
  // save it's start and end addresses
  if (len > cap) {
    start = str;
    end = str + len;
  }

  // slow, boring return
  return len;
}
      
      





ハッシュ配列の問題に関しては、対処が簡単です-値が一意であることがわかっているため、重複するチェックを完全にスキップしてアイテムを直接挿入することができます。



char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item)
{
  // didn't bother reversing the structure
  uint64_t not_a_hashmap = catalog + 88;

  // no idea what this does, but repeat what the original did
  if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))
    return 0;

  // insert directly
  netcat_insert_direct(not_a_hashmap, key, &item);

  // remove hooks when the last item's hash is hit
  // and unload the .dll, we are done here :)
  if (*key == 0x7FFFD6BE) {
    MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);
    unload();
  }

  return 1;
}
      
      





概念実証の完全なソースは、 ここにあります



結果



それで、それはどのように機能しましたか?



オンラインモードの初期ロード時間:約6分

パッチされた重複チェックのみの時間:4分30秒

JSONパーサーパッチのみの

時間:2分50秒 両方の問題のパッチの時間:1分50秒



(6 * 60-(1 * 60 + 50))/(6 * 60)=ロード時間が69.4%減少しました(素晴らしい!)


そうそう、それはどのように機能したのか!



ほとんどの場合、これによってすべてのプレーヤーのロード時間が短縮されるわけではありません。他のシステムに他のボトルネックがある可能性がありますが、これは明らかな問題であるため、R *がここ数年気づいていないことを理解できません。



tl; dr



  • シングルスレッド実行のため、GTAオンラインを起動するときにCPUのボトルネックがあります
  • 現時点では、GTAが10MBのJSONファイルの解析と戦っていることがわかりました。
  • JSONパーサー自体は、適切に記述されておらず、単純に実装されており、
  • 解析後、重複するアイテムがないことを確認するために遅い手順が実行されます


R *問題を解決してください



この記事がどういうわけかRockstarに到達した場合、1人の開発者がこれらの問題を修正するのに1日以上かかることはありません。それについて何かしてください。



ハッシュマップに切り替えて重複を排除するか、このチェックを完全にスキップすることができます。これにより高速になります。JSONパーサーで、ライブラリをより効率的なライブラリに置き換えます。ここにもっと簡単な解決策があるとは思いません。



感謝。



All Articles