UnityゲームのGC(ガベージコレクション)の最適化



Gc Optimization



元の: https://unity3d.com/cn/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games

Unityバージョン:5.5



前書き

ゲームは、実行中にメモリを使用してデータを保存します。データが使用されなくなると、データを格納しているメモリが解放され、後で再利用できるようになります。ガベージは、役に立たないデータを格納するメモリの用語です。 GC(ガベージコレクション)は、これらのメモリを再利用できるプロセスです。



GCはUnityのマネージドメモリの一部であり、GCの過度の負担のためにゲームのパフォーマンスが低下する可能性があるため、GCはパフォーマンスの問題の一般的な原因です。

この記事では、GCがどのように機能するか、どのような状況でGCがトリガーされるか、メモリを効率的に使用してGCがゲームに与える影響を減らす方法について説明します。

GC問題診断



GCによって引き起こされるパフォーマンスの問題は、低フレームレート、激しいフレームレート、または断続的な紙詰まりとして現れる可能性があります。しかし、他の問題も同様の症状を引き起こす可能性があります。ゲームにこれらのパフォーマンスの問題がある場合は、最初にUnityのプロファイラーツールを使用して、これらの問題がGCによって引き起こされていることを確認する必要があります。

プロファイラーツールを使用してパフォーマンスの問題の原因を特定する方法については、次を参照してください。 このチュートリアル

Unityメモリ管理の概要

GCがどのように機能し、いつトリガーされるかを理解する前に、Unityのメモリ使用量を理解する必要があります。まず、Unityが独自のコアエンジンコードを実行するときと、スクリプトで記述したコードを実行するときに、さまざまなメソッドを使用することを知っておく必要があります。

Unityが独自のコアエンジンコードを実行しているときに使用されます 手動メモリ管理 これは、コアエンジンコードがメモリの使用方法を明確に述べている必要があることを意味します。手動メモリ管理はGCを使用しないため、この記事では紹介しません。

Unityが作成したスクリプトコードを実行するときに使用されます 自動メモリ管理 これは、コードを書くときにUnityにメモリの管理方法を明示的に指示する必要がないことを意味します。 Unityは自動的にこれを行います。

基本的に、Unityの自動メモリ管理は次のように機能します。

  • Unityは2つのメモリプールにアクセスできます。 スタックスタック (としても知られている マネージヒープ )。スタックは小さなデータの短期保存に使用され、ヒープは長期保存と大きなデータセグメントに使用されます。
  • 変数を作成するとき、Unityはスタックまたはヒープからメモリを要求します。
  • 変数が 範囲 内部(コードから引き続きアクセス可能)では、割り当てられたメモリはまだ使用されており、メモリのこの部分は 分布 。スタック内の変数を呼び出します スタックオブジェクト 、ヒープ内の変数は呼び出されます ヒープオブジェクト
  • 変数がスコープ外になると、メモリは使用されなくなり、元のメモリプールに戻すことができます。メモリが元のメモリプールに戻されると、メモリを呼び出します 解放された 。スタックメモリは、変数がスコープ外になるとリアルタイムで解放され、ヒープメモリは解放されず、変数がスコープ外になった後も割り当てられたままになります。
  • ガベージコレクター (ガベージコレクター)未使用のヒープメモリを特定して解放します。ガベージコレクターは、ヒープをクリーンアップするために定期的に実行されます。

イベントの流れを理解したところで、スタックの割り当てと解放、およびヒープの割り当てと解放の違いを詳しく見ていきましょう。

スタックが割り当てられて解放されたときに何が起こったのか

スタックの割り当てと解放は迅速かつ簡単です。これは、スタックが小さなデータを短時間で保存するためにのみ使用されるためです。割り当てと解放は常に予測可能な順序で行われ、予測可能なサイズになります。

スタックは次のように機能します スタックデータ型 :これは要素の単純なコレクションです。この場合はメモリブロックであり、要素の追加と削除は厳密な順序でのみ行うことができます。この単純さと厳密さにより、非常に高速になります。変数がスタックに格納されると、そのメモリはスタックの最上位から単純に割り当てられます。スタック変数がスコープ外の場合、変数を格納するために使用されたメモリは、再利用のためにすぐにスタックに戻されます。

ヒープ割り当て中に何が起こったのか

ヒープ割り当ては、スタック割り当てよりもはるかに複雑です。ヒープは、長期および短期のデータとさまざまなタイプおよびサイズのデータ​​を格納するために使用できるためです。割り当てと解放も常に予測可能な順序で実行されるとは限らず、大きなサイズのメモリブロックが必要になる場合があります。

ヒープ変数が作成されると、次の手順が実行されます。

  • まず、Unityはヒープに十分な空きメモリがあるかどうかを確認し、十分な空きメモリがある場合は、変数のメモリが割り当てられます。
  • そうでない場合、UnityはGCをトリガーして、未使用のヒープメモリを解放しようとします。これは遅くなる可能性があります。 GC後にヒープメモリが十分である場合、変数のメモリが割り当てられます。
  • GC後もヒープに十分な空きメモリがない場合、Unityはヒープサイズを増やすためにオペレーティングシステムからより多くのメモリを要求します。この操作は遅くなる可能性があります。次に、この変数のメモリが割り当てられます。

特にGCを実行してヒープサイズを拡張する必要がある場合は、ヒープの割り当てが遅くなる可能性があります。

GCで何が起こったのか

ヒープ変数がスコープ外になっても、変数を格納しているメモリはすぐには解放されません。不要なヒープメモリは、GCの実行時にのみ解放されます。

GCが実行されるたびに、次の手順が実行されます。

  • ガベージコレクタは、ヒープ上の各オブジェクトを取得します。
  • ガベージコレクタは、現在のすべてのオブジェクト参照を検索して、ヒープ上のオブジェクトがまだスコープ内にあるかどうかを判断します。
  • スコープ内にないオブジェクトは、削除対象としてマークされます。
  • マークされたオブジェクトを削除し、メモリをヒープに戻します。

GCは時間のかかる操作です。ヒープ上のオブジェクトが多いほど、コード内の参照が多くなり、GCにかかる時間が長くなります。

GCはいつトリガーされますか?

GCは、次の3つの場合にトリガーされます。

  • GCは、ヒープ割り当て時にヒープに十分な空きメモリがない場合にトリガーされます。
  • GCは時々自動的に実行されます(頻度はプラットフォームによって異なります)。
  • GC呼び出しを手動で強制する

GCは頻繁にトリガーされる場合があります。 GCは、使用可能なヒープメモリからヒープ割り当てを実装できない場合にトリガーされます。つまり、ヒープの割り当てと解放が頻繁に行われると、GCが頻繁に発生する可能性があります。

GCの問題

Unityのメモリ管理におけるGCの役割を理解したので、発生する可能性のある問題の種類を検討できます。

最も明らかな問題は、GCの実行に長い時間がかかる可能性があることです。ヒープ上に多数のオブジェクトがあり、チェックするオブジェクト参照が多数ある場合、それらすべてをチェックするプロセスが遅くなる可能性があります。これにより、ゲームがスタックしたり、実行が遅くなったりする可能性があります。

もう1つの問題は、GCがタイムリーにトリガーされない可能性があることです。ゲームのパフォーマンスの重要な部分でCPUがすでに完全にロードされている場合、わずかなGCオーバーヘッドでも、フレームレートが低下し、パフォーマンスの問題が発生する可能性があります。

もう1つのあまり明白でない問題は ヒープデブリ 。ヒープからメモリが割り当てられると、メモリは、格納する必要のあるデータのサイズに応じて、さまざまなサイズのブロックの空き領域から取得されます。これらのメモリブロックがヒープに戻されると、ヒープは割り当てブロックで区切られた多くの小さな空きブロックに分割される場合があります。これは、使用可能なメモリの合計量が多い場合でも、断片化されすぎて連続したメモリブロックを割り当てることができないことを意味します。 GCがトリガーされるか、ヒープサイズを増やす必要があります。

ヒープメモリの断片化には2つの結果があります。まず、ゲームのメモリサイズは、実際に必要なサイズよりもはるかに大きくなります。次に、GCがより頻繁にトリガーされます。ヒープの断片化の詳細については、を参照してください。 このUnityパフォーマンスのベストプラクティスガイド

ヒープ割り当てを見つける

ゲームでGCに問題がある場合は、コードのどの部分が原因であるかを知る必要があります。ヒープ上の変数がスコープ外になると、メモリのこの部分が再利用されるガベージメモリになるため、変数がヒープに割り当てられるタイミングを知る必要があります。

スタックとヒープには何が割り当てられていますか?

Unity中央値タイプのローカル変数はスタックに割り当てられ、ヒープに割り当てられます。値型と参照型の違いがわからない場合は、を参照してください。 このチュートリアル

次のコードは、スタック割り当ての例です。 localInt変数は、ローカル値型変数です。この変数に割り当てられたメモリは、関数呼び出しが終了した直後に再利用されます。

void ExampleFunction()
{{
int localInt = 5

}

次のコードはヒープ割り当ての例です。localList変数はローカル変数ですが、参照型です。この変数に割り当てられたメモリは、次のGCでリサイクルされます。

void ExampleFunction()
{{
リストlocalList = new List()

}

プロファイラーツールを使用して、ヒープ割り当てを見つけます

プロファイラーツールを使用して、コードのどの部分がヒープ割り当てを生成したかを確認できます。

[CPU使用率]を選択してから、任意のフレームを選択して、プロファイラーウィンドウの下部にフレームのCPU使用率データを表示します。列の1つはと呼ばれます GC割り当て、 この列には、このフレームのヒープ割り当て情報が表示されます。列ヘッダーをクリックして列を並べ替えると、現在のフレーム内のどの関数が最も多くのヒープ割り当てを生成したかをより直感的に確認できます。これにより、ヒープ割り当てを生成するこれらの関数を調べることができます。

関数内のどのコードがガベージを引き起こしているのかがわかれば、問題を解決してガベージの生成量を最小限に抑える方法を決定できます。

GCの影響を減らす

一言で言えば、次の3つの方法でGCがゲームに与える影響を減らすことができます。

  • GC時間を短縮する
  • GCの頻度を減らす
  • シーンの読み込みなど、ゲームプレイのパフォーマンスの重要なポイントを回避するために、意図的にGCをトリガーします

これらの考慮事項に基づいて、次の3つの戦略を使用できます。

  • ゲームを整理して、ヒープの割り当てとオブジェクト参照を減らすことができます。ヒープ上のオブジェクトと参照が少なくなるGCが起動するときに実行にかかる時間が短くなることを確認します。
  • 特にパフォーマンスポイントで、ヒープの割り当てと解放の頻度を減らすことができます。割り当てとリリースが少ないということは、GCのトリガーが少ないことを意味します。これにより、ヒープの断片化の問題も軽減されます。
  • GCを手動でトリガーし、ヒープサイズを拡張して、GCを制御可能にし、必要に応じてトリガーできるようにすることができます。このアプローチはより困難で信頼性がありませんが、全体的なメモリ管理戦略の一部として、GCの影響を減らすことができます。

発生するゴミの量を減らす

いくつかの手法を使用して、コードで生成されるガベージの量を減らすことができます。

キャッシュ

コードがヒープ割り当てを生成する関数を繰り返し呼び出してから結果を破棄すると、不要なガベージが生成されます。このために、これらのオブジェクトへの参照を保存して再利用する必要があります。このテクニックはと呼ばれます キャッシュ

次の関数を呼び出すたびに、呼び出しごとに新しい配列が生成されるため、ヒープが割り当てられます。

void OnTriggerEnter(Collider other)
{{
Renderer [] allRenderers = FindObjectsOfType()
ExampleFunction(allRenderers)

}

次のコードでは、割り当ての作成後に配列がキャッシュされるため、ヒープ割り当ては1つだけになります。キャッシュされたアレイはスパムなしで再利用できます。

プライベートレンダラー[] allRenderers

void Start()
{{
allRenderers = FindObjectsOfType()
}


void OnTriggerEnter(Collider other)
{{
ExampleFunction(allRenderers)

}

頻繁に呼び出される関数には割り当てないでください

MonoBehaviourでヒープメモリを割り当てる必要がある場合、頻繁に呼び出される関数の中で割り当ては最悪です。フレームごとに呼び出される関数など 更新()LateUpdate() 、これらの場所に配布されると、ゴミは非常に速く蓄積します。これらのオブジェクトへの参照を可能な限りStart()またはAwake()にキャッシュするか、メモリを割り当てるコードが必要な場合にのみ実行されるようにする必要があります。

簡単な例を見てみましょう。次のコードは毎回です 更新() 呼び出すと、ヒープ割り当てを引き起こす関数が呼び出され、ガベージが非常にすばやく生成されます。

void Update()
{{
ExampleGarbageGeneratingFunction(transform.position.x)

}

簡単な変更の後、ヒープ割り当てを生成する関数が、transform.position.xの値が変更されたときにのみ呼び出されるようにすることができます。これは、必要な場合にのみヒープ割り当てを生成し、すべてのフレームを生成するわけではありません。

プライベートフロートpreviousTransformPositionX

void Update()
{{
float transformPositionX = transform.position.x
if(transformPositionX!= previousTransformPositionX)
{{
ExampleGarbageGeneratingFunction(transformPositionX)
previousTransformPositionX = transformPositionX
}

}

Update()関数で生成されるガベージの量を減らす別の方法は、タイマーを使用することです。これは、頻繁に呼び出す必要があり、フレームごとに呼び出す必要がないガベージメモリを生成するコードに適用されます。

次のサンプルコードは、ガベージメモリを生成する関数をフレームごとに呼び出します。

void Update()

{{

ExampleGarbageGeneratingFunction()

}

次のコードは、タイマーを使用して、ガベージメモリを生成する関数が1秒に1回だけ呼び出されるようにします。

プライベートフロートtimeSinceLastCalled

プライベートフロート遅延= 1f

void Update()
{{
timeSinceLastCalled + = Time.deltaTime
if(timeSinceLastCalled> delay)
{{
ExampleGarbageGeneratingFunction()
timeSinceLastCalled = 0f
}

}

このように頻繁に呼び出される関数に小さな変更を加えると、生成されるガベージの量を大幅に減らすことができます。

空のコンテナ

コンテナクラスを作成すると、ヒープが割り当てられます。コード内で同じコンテナー変数を複数回作成する場合は、コンテナー参照をキャッシュして、重複して作成する代わりにClear()操作を使用する必要があります。

次の例では、* new *操作がヒープ割り当てを生成するたびに

void Update()
{{
リストmyList = new List()
PopulateList(myList)

}

次の例では、ヒープの割り当てはコンテナが作成または拡張されたときにのみ発生し、生成されるガベージの量を大幅に削減します。

プライベートリストmyList = new List()
void Update()
{{
myList.Clear()
PopulateList(myList)

}

オブジェクトプール

スクリプト内のヒープ割り当てが削減された場合でも、実行時に多数のオブジェクトを作成および破棄すると、GCの問題が発生します。 オブジェクトプーリングは、オブジェクトを繰り返し作成および破棄するのではなく、オブジェクトを再利用することによって割り当てと解放を減らすための手法です。 オブジェクトプールはゲームで広く使用されており、銃が弾丸を発射する場合など、同様のオブジェクトが頻繁に生成および破棄される状況に最適です。

オブジェクトプールの完全なガイドはこの記事の範囲を超えていますが、これは非常に便利な手法であり、試す価値があります。 Unity LearningWebサイトのUnityについて このチュートリアル Unityにオブジェクトプールシステムを実装するための良いガイドです

不要なヒープ割り当ての一般的な原因

ローカルの値型変数はスタックに割り当てられ、その他の変数はヒープに割り当てられることがわかっています。しかし、多くの場合、ヒープの割り当ては驚くべきものになる可能性があります。不要なヒープ割り当ての一般的な理由をいくつか見て、これを減らす最善の方法を検討しましょう。

ストリング

C#では、 ストリング これらは参照型であり、値型ではありませんが、文字列の「値」を保持しているように見えます。これは、文字列の作成と削除がスパムになる可能性があることを意味します。文字列は多くのコードで一般的に使用されているため、これらのゴミが蓄積する可能性があります。

C#の文字列も 不変 これは、最初の作成後にそれらの値を変更できないことを意味します。文字列を操作するたびに(たとえば、+演算子を使用して2つの文字列を結合することにより)、Unityは更新された値を含む新しい文字列を作成し、古い文字列を破棄します。これはゴミを生成します。

いくつかの簡単なルールに従って、文字列によって生成されるゴミを最小限に抑えることができます。これらのルールを見てから、それらを適用する例を見てみましょう。

  • 不要な文字列の作成を減らします。同じ文字列値を複数回使用する場合は、文字列を1回作成して、値をキャッシュする必要があります。
  • 不要な文字列操作を減らします。たとえば、頻繁に更新され、接続された文字列を含むテキストコンポーネントがある場合は、それを2つのテキストコンポーネントに分割することを検討してください。
  • 実行時に文字列を作成する必要がある場合は、StringBuilderクラスを使用する必要があります。 StringBuilderクラスは、ヒープによって割り当てられない文字列を作成し、複雑な文字列を接続するときに生成されるガベージの量を減らすために使用されます。
  • Debug.Log()の呼び出しは、デバッグが不要な場合はすぐに削除されます。 Debug.Log()の呼び出しは、何も出力されなくても実行されます。 Debug.Log()を呼び出して、少なくとも1つの文字列を作成および処理します。したがって、ゲームにこれらの呼び出しが多数含まれていると、ガベージが蓄積されます。

文字列を非効率的に使用して不要なガベージを生成するコードの例を見てみましょう。次のコードでは、Update()で、「TIME:」の値をfloatタイマーに接続してスコアを表示する文字列を作成します。これにより、不要なゴミが作成されます。

パブリックテキストtimerText
プライベートフロートタイマー

void Update()
{{
タイマー+ = Time.deltaTime
timerText.text = 'TIME:' + timer.ToString()

}

以下にいくつかの改善を加えましょう。 「TIME:」という単語を別のテキストコンポーネントに配置し、その値をStart()で設定します。したがって、Update()では、接続文字列はもう必要ありません。ごみの発生を大幅に減らすことができます。

パブリックテキストtimerHeaderText
パブリックテキストtimerValueText
プライベートフロートタイマー

void Start()
{{
timerHeaderText.text = '時間:'
}

void Update()
{{
timerValueText.text = timer.toString()

}

Unity関数呼び出し

Unityでもプラグインでも、自分で記述していないコードを呼び出すと、ガベージが生成される可能性があることに注意してください。一部のUnity関数を呼び出すとヒープが割り当てられるため、不要なゴミを避けるように注意して使用してください。

避けるべき機能のリストはありません。各関数は、ある場合には有用であり、他の状況ではあまり有用ではありません。したがって、ゲームを注意深く分析し、ゴミが発生する場所を特定し、その対処方法を慎重に検討することをお勧めします。関数の結果をキャッシュできる場合もあれば、関数を呼び出す頻度を減らすことができる場合もあります。別の関数を使用するようにコードをリファクタリングするのが最善です。そうは言っても、ヒープ割り当てを引き起こすいくつかの一般的なUnity関数を見て、それらをより適切に処理する方法を検討しましょう。

配列を返すUnity関数にアクセスするたびに、新しい配列が作成され、戻り値として渡されます。この動作は、特に関数が次の場合に、常に明白または予測可能であるとは限りません。 アクセサー 時間(たとえば Mesh.normals )。

次のコードでは、ループが繰り返されるたびに新しい配列が生成されます。

void ExampleFunction()
{{
for(int i = 0 i{{
Vector3 normal = myMesh.normals [i]
}

}

この場合、割り当てを減らすのは簡単です。配列への参照をキャッシュするだけです。これにより、配列が1つだけ作成され、それに応じて生成されるガベージの量が削減されます。

以下のコードはこれを示しています。この場合、Mesh.normalsを呼び出し、ループの前に参照をキャッシュするため、配列のみを作成します。

void ExampleFunction()
{{
Vector3 [] meshNormals = myMesh.normals
for(int i = 0 i{{
Vector3 normal = meshNormals [i]
}

}

GameObject.nameまたはGameObject.tagにアクセスするためのヒープ割り当てもあります。これらは両方とも新しい文字列を返すアクセサです。つまり、これらの関数を呼び出すとスパムが発生します。この値をキャッシュすると便利な場合がありますが、この場合、関連するUnity関数を使用できます。スパムなしでGameObjectタグの値を確認するには、次を使用できます。 GameObject.CompareTag()

以下のサンプルコードでは、GameObject.tagにアクセスするとガベージメモリが生成されます。

プライベート文字列playerTag = 'Player'

void OnTriggerEnter(Collider other)
{{
bool isPlayer = other.gameObject.tag == playerTag

}

GameObject.CompareTag()を使用する場合、関数はスパムを送信しません。

プライベート文字列playerTag = 'Player'

void OnTriggerEnter(Collider other)
{{
bool isPlayer = other.gameObject.CompareTag(playerTag)

}

GameObject.CompareTag 1つだけではなく、多くのUnity関数にはヒープ割り当てのない代替バージョンがあります。たとえば、 Input.GetTouch()Input.touchCount 交換 Input.touches 、または使用 Physics.SphereCastNonAlloc() 交換 Physics.SphereCastAll()

梱包

梱包 値型変数が参照型変数として使用されている場合に実行される操作です。値型変数(intやfloatなど)をオブジェクト型パラメーターを持つ関数に渡すと、通常、Object.Equals()関数などのボックス化が発生します。

たとえば、関数String.Format()は、文字列とオブジェクト引数を取ります。文字列とintを渡すと、intはボックス化されます。次のコードには、ボクシングの例が含まれています。

void ExampleFunction()
{{
intコスト= 5
string displayString = String.Format( '価格:{0}ゴールド'、コスト)

}

ボクシングは、バックグラウンド操作からゴミを作成します。値型変数がボックス化されている場合、Unityはヒープ上に一時的なSystem.Objectを作成して、値型変数をラップします。 System.Objectは参照型変数であるため、この一時オブジェクトが処理されるときにガベージが生成されます。

ボクシングは、不要なヒープ割り当ての一般的な原因です。コードに変数を直接ロードしない場合でも、ボクシングを引き起こすプラグインを使用する可能性があり、ボクシングは他の関数のバックグラウンドで発生する可能性があります。ベストプラクティスは、ボクシングをできるだけ避け、ボクシングの原因となる関数呼び出しをすべて削除することです。

コルーチン

Unityはコルーチンのインスタンスを管理するいくつかのクラスを作成する必要があるため、StartCoroutine()を呼び出すと少量のガベージが生成されます。したがって、StartCoroutine()の呼び出しは、ゲームが相互作用しているとき、またはパフォーマンスのホットスポットで制限する必要があります。この方法で生成されるコルーチンを減らすには、パフォーマンスホットスポットで実行する必要のあるコルーチンを早期に開始する必要があります。また、StartCoroutine()の呼び出しが遅れる可能性のあるネストされたコルーチンを使用する場合は注意が必要です。

コルーチンのyieldステートメントは、ヒープ割り当て自体を生成しませんが、yieldステートメントに渡す値は、不要なヒープ割り当てを生成する可能性があります。たとえば、次のコードはスパムになります。

イールドリターン0

int変数0がボックス化されているため、このコードはスパムです。この場合、ヒープ割り当てを発生させずにフレームを待機するだけの場合は、次のコードを使用するのが最善の方法です。

歩留まりはnullを返します

コルーチンでよくあるもう1つの間違いは、同じ値を複数回使用するときに新しい操作を使用することです。たとえば、次のコードは、ループが繰り返されるたびに、WaitForSecondsオブジェクトを繰り返し作成および破棄します。

while(!isComplete)
{{
歩留まりは新しいWaitForSeconds(1f)を返します

}

WaitForSecondsオブジェクトをキャッシュして再利用すると、生成されるガベージの量を減らすことができます。次のサンプルコードを参照してください。

WaitForSeconds delay = new WaitForSeconds(1f)

while(!isComplete)
{{
歩留まりリターン遅延

}

コルーチンが原因でコードが大量のガベージを生成する場合は、コルーチン以外のものを使用してコードをリファクタリングすることを検討してください。コードのリファクタリングは複雑な問題であり、各プロジェクトは固有ですが、コルーチンの問題に役立つ可能性のある一般的なツールがいくつかあります。たとえば、主にコルーチンを使用して時間を管理する場合、Update()関数に時間を記録するだけで済みます。ゲームで発生することの順序を制御するために主にコルーチンを使用する場合、オブジェクトが通信できるようにするための何らかのメッセージングシステムを作成できます。 1つの方法ですべての問題を解決できるわけではありませんが、コードで同じことを行う方法はたくさんあることを覚えておく価値があります。

Foreachループ

Unity 5.5より前のバージョンでは、foreachを使用して配列外のすべてのコレクションを反復処理していましたが、バックグラウンドのボクシング操作のため、ループの最後にガベージが生成されていました。ループが開始してループが終了すると、System.Objectオブジェクトがヒープに割り当てられます。これはUnity5.5で修正されました。

5.5より前のUnityバージョンでは、次のコードのループでガベージが生成されました。

void ExampleFunction(List listOfInts)
{{
foreach(listOfIntsのint currentInt)
{{
DoSomething(currentInt)
}

}

Unityバージョンをアップグレードできない場合は、この問題に対する簡単な解決策があります。 forループとwhileループはバックグラウンドでボクシングを引き起こさないため、ガベージは生成されません。反復が配列のコレクションでない場合は、最初にそれらを使用する必要があります。

次のコードはスパムではありません。

void ExampleFunction(List listOfInts)
{{
for(int i = 0 i{{
int currentInt = listOfInts [i]
DoSomething(currentInt)
}

}

関数リファレンス

関数リファレンス、それがリファレンスであるかどうか 匿名関数 または、Unityの参照型変数である名前付き関数。それらはヒープ割り当てになります。無名関数をに変換する 閉鎖 (匿名関数は、作成時にスコープ内の変数にアクセスできます)メモリ使用量とヒープ割り当てが大幅に増加します。

関数参照とクロージャがメモリを割り当てる方法の正確な詳細は、プラットフォームとコンパイラの設定によって異なりますが、GCが問題になる場合は、ゲーム中の関数参照とクロージャの使用を最小限に抑えるのが最善です。 このUnityパフォーマンスのベストプラクティスガイド このトピックに関する技術的な詳細があります。

LINQと正規表現

LINQと正規表現は、バックグラウンドでのボクシング操作のためにガベージを生成します。パフォーマンス要件がある場合は使用しないことをお勧めします。同じ、 このUnityパフォーマンスのベストプラクティスガイド このトピックに関する技術的な詳細が利用可能です。

GCの影響を最小限に抑えるコードを作成する

コードの作成方法はGCに影響を与える可能性があります。コードにヒープ割り当てがない場合でも、GCの負担が増える可能性があります。

GCを増加させる可能性のある負担の1つは、チェックすべきでないものをチェックするように依頼することです。構造体は値型変数ですが、参照型変数を含む構造体がある場合、ガベージコレクターは構造体全体をチェックする必要があります。そのような構造が多数ある場合、ガベージコレクターは多くの余分な作業を追加します。

この例では、次の構造体に参照型の文字列が含まれています。ガベージコレクターの実行中に、構造体の配列全体をチェックする必要があります。

public struct ItemData
{{
パブリック文字列名
publicintコスト
パブリックVector3の位置

}

プライベートItemData [] itemData

この例では、データを別の配列に格納します。ガベージコレクターを実行すると、文字列の配列をチェックし、他の配列を無視するだけで済みます。これにより、ガベージコレクタの作業が軽減されます。

プライベート文字列[] itemNames
private int [] itemCosts

プライベートVector3 [] itemPositions

GCの負担を増やす可能性のある別の操作は、不要なオブジェクト参照を使用することです。ガベージコレクターがヒープ上のオブジェクトへの参照を検索するとき、コード内の現在のすべてのオブジェクト参照をチェックする必要があります。ヒープ上のオブジェクトの総数を減らさなくても、オブジェクト参照が少ないということは作業が少ないことを意味します。

この例では、ダイアログにデータを入力するクラスがあります。ユーザーがダイアログを表示すると、別のダイアログが表示されます。このコードには、表示する必要があるDialogDataの次のインスタンスへの参照が含まれています。つまり、ガベージコレクターはその操作でこの参照を確認する必要があります。

パブリッククラスDialogData
{{
プライベートDialogDatanextDialog

public DialogData GetNextDialog()
{{
nextDialogを返す
}

}

ここでは、インスタンス自体ではなく、次のDialogDataインスタンスを見つけるための識別子を返すようにコードをリファクタリングします。これはオブジェクト参照ではないため、ガベージコレクターにかかる時間が長くなることはありません。

パブリッククラスDialogData
{{
private int nextDialogID

public int GetNextDialogID()
{{
nextDialogIDを返す
}

}

これは小さな例です。ただし、ゲーム内に他のオブジェクトへの参照を含むオブジェクトが多数ある場合は、この方法でコードをリファクタリングすることで、ヒープの複雑さを大幅に軽減できます。

タイミングGC

手動でGCを強制する

最後に、GCを自分でトリガーしたい場合があります。ヒープメモリが割り当てられているが使用されていないことがわかっている場合(たとえば、リソースのロード時にコードがガベージを生成する場合)、ガベージコレクションのフリーズがプレーヤーに影響を与えないことがわかっている場合(たとえば、ロードインターフェイスがまだある場合)表示されます)、次のコードを使用してGCをリクエストできます。

System.GC.Collect()

これにより、GCが強制的に実行され、都合の良いときに未使用のメモリが解放されます。

結論として

UnityでGCがどのように機能するか、なぜそれがパフォーマンスの問題を引き起こす可能性があるのか​​、そしてゲームへの影響を最小限に抑える方法を見てきました。これらの知識と分析ツールを使用して、GC関連のパフォーマンスの問題を解決し、メモリを効率的に管理するゲームを構築できます。