[Unity脚本优化] Optimizing garbage collection in Unity games

Introduction

When our game runs, it uses memory to store data. When this data is no longer needed, the memory that stored that data is freed up so that it can be reused. Garbage is the term for memory that has been set aside to store data but is no longer in use. Garbage collection is the name of the process that makes that memory available again for reuse.
当我们的游戏运行时,它使用内存来存储数据。 当不再需要此数据时,存储该数据的内存将被释放,以便可以重复使用。 垃圾是已预留用于存储数据但不再使用的内存的术语。 垃圾收集是使该内存再次可供重用的过程的名称。
Unity uses garbage collection as part of how it manages memory. Our game may perform poorly if garbage collection happens too often or has too much work to do, which means that garbage collection is a common cause of performance problems.
Unity 使用垃圾收集作为其管理内存的一部分。 如果垃圾收集发生得太频繁或有太多工作要做,我们的游戏可能会表现不佳,这意味着垃圾收集是性能问题的常见原因。
In this article, we’ll learn how garbage collection works, when garbage collection happens and how to use memory efficiently so that we minimize the impact of garbage collection on our game.
在本文中,我们将了解垃圾收集的工作原理、垃圾收集发生的时间以及如何有效地使用内存,从而最大限度地减少垃圾收集对游戏的影响。

Diagnosing problems with garbage collection

Performance problems caused by garbage collection can manifest as low frame rates, jerky performance or intermittent freezes. However, other problems can cause similar symptoms. If our game has performance problems like this, the first thing we should do is to use Unity’s Profiler window to establish whether the problems we are seeing are actually due to garbage collection.
垃圾收集引起的性能问题可能表现为低帧率、不稳定的性能或间歇性冻结。 但是,其他问题可能会导致类似的症状。 如果我们的游戏有这样的性能问题,我们应该做的第一件事是使用 Unity 的 Profiler 窗口来确定我们看到的问题是否实际上是由于垃圾收集造成的。
To learn how to use the Profiler window to find the cause of your performance problems, please follow this tutorial.
要了解如何使用 Profiler 窗口查找性能问题的原因,请遵循本教程。

A brief introduction to memory management in Unity

To understand how garbage collection works and when it happens, we must first understand how memory usage works in Unity. Firstly, we must understand that Unity uses different approaches when running its own core engine code and when running the code that we write in our scripts.
要了解垃圾收集是如何工作的以及何时发生,我们必须首先了解 Unity 中内存使用的工作原理。 首先,我们必须了解 Unity 在运行自己的核心引擎代码和运行我们在脚本中编写的代码时使用不同的方法。
The way Unity manages memory when running its own core Unity Engine code is called manual memory management. This means that the core engine code must explicitly state how memory is used. Manual memory management does not use garbage collection and won’t be covered further in this article.
Unity 在运行自己的核心 Unity Engine 代码时管理内存的方式称为手动内存管理。 这意味着核心引擎代码必须明确说明内存的使用方式。 手动内存管理不使用垃圾回收,本文不会进一步介绍。
The way that Unity manages memory when running our code is called automatic memory management. This means that our code doesn’t need to explicitly tell Unity how to manage memory in a detailed way. Unity takes care of this for us.
Unity 在运行我们的代码时管理内存的方式称为自动内存管理。 这意味着我们的代码不需要明确告诉 Unity 如何以详细的方式管理内存。 Unity 会为我们解决这个问题。
At its most basic level, automatic memory management in Unity works like this:

  • Unity has access to two pools of memory: the stack and the heap (also known as the managed heap. The stack is used for short term storage of small pieces of data, and the heap is used for longer term storage and larger pieces of data.
    Unity 可以访问两个内存池:堆栈和堆(也称为托管堆。堆栈用于短期存储小块数据,堆用于长期存储和较大块数据.
  • When a variable is created, Unity requests a block of memory from either the stack or the heap.
    创建变量时,Unity 从堆栈或堆中请求一块内存。
  • As long as the variable is in scope (still accessible by our code), the memory assigned to it remains in use. We say that this memory has been allocated. We describe a variable held in stack memory as an object on the stack and a variable held in heap memory as an object on the heap.
    只要变量在范围内(我们的代码仍然可以访问),分配给它的内存就一直在使用。我们说这块内存已经被分配了。我们将栈内存中的变量描述为栈上的对象,将堆内存中的变量描述为堆上的对象。
  • When the variable goes out of scope, the memory is no longer needed and can be returned to the pool that it came from. When memory is returned to its pool, we say that the memory has been deallocated. Memory from the stack is deallocated as soon as the variable it refers to goes out of scope. Memory from the heap, however, is not deallocated at this point and remains in an allocated state even though the variable it refers to is out of scope.
    当变量超出范围时,不再需要内存并且可以将其返回到它来自的池中。当内存返回到它的池时,我们说内存已被释放。一旦它引用的变量超出范围,堆栈中的内存就会被释放。然而,堆中的内存此时并没有被释放,即使它引用的变量超出范围,它仍然保持分配状态。
  • The garbage collector identifies and deallocates unused heap memory. The garbage collector is run periodically to clean up the heap.
    垃圾收集器识别并释放未使用的堆内存。垃圾收集器定期运行以清理堆。
    Now that we understand the flow of events, let’s take a closer look at how stack allocations and deallocations differ from heap allocations and deallocations.
    现在我们了解了事件的流程,让我们仔细看看堆栈分配和解除分配与堆分配和解除分配有何不同。

What happens during stack allocation and deallocation?

Stack allocations and deallocations are quick and simple. This is because the stack is only used to store small data for short amounts of time. Allocations and deallocations always happen in a predictable order and are of a predictable size.
堆栈分配和释放既快速又简单。 这是因为堆栈仅用于在短时间内存储少量数据。 分配和解除分配总是以可预测的顺序发生并且具有可预测的大小。

The stack works like a stack data type: it is a simple collection of elements, in this case blocks of memory, where elements can only be added and removed in a strict order. This simplicity and strictness is what makes it so quick: when a variable is stored on the stack, memory for it is simply allocated from the “end” of the stack. When a stack variable goes out of scope, the memory used to store that variable is immediately returned to the stack for reuse.
堆栈就像堆栈数据类型一样工作):它是元素的简单集合,在这种情况下是内存块,其中元素只能以严格的顺序添加和删除。 这种简单性和严格性使它如此快速:当变量存储在堆栈上时,它的内存只是从堆栈的“末端”分配。 当堆栈变量超出范围时,用于存储该变量的内存会立即返回堆栈以供重用。

What happens during a heap allocation?

A heap allocation is much more complex than a stack allocation. This is because the heap can be used to store both long term and short term data, and data of many different types and sizes. Allocations and deallocations don’t always happen in a predictable order and may require very different sized blocks of memory.
堆分配比堆栈分配复杂得多。 这是因为堆可用于存储长期和短期数据,以及许多不同类型和大小的数据。 分配和解除分配并不总是以可预测的顺序发生,并且可能需要大小非常不同的内存块。
When a heap variable is created, the following steps take place:

  • First, Unity must check if there is enough free memory in the heap. If there is enough free memory in the heap, the memory for the variable is allocated.
    首先,Unity 必须检查堆中是否有足够的空闲内存。 如果堆中有足够的空闲内存,则为变量分配内存。

  • If there is not enough free memory in the heap, Unity triggers the garbage collector in an attempt to free up unused heap memory. This can be a slow operation. If there is now enough free memory in the heap, the memory for the variable is allocated.
    如果堆中没有足够的可用内存,Unity 会触发垃圾收集器以尝试释放未使用的堆内存。 这可能是一个缓慢的操作。 如果堆中现在有足够的空闲内存,则为变量分配内存。

  • If there isn’t enough free memory in the heap after garbage collection, Unity increases the amount of memory in the heap. This can be a slow operation. The memory for the variable is then allocated.
    如果垃圾回收后堆中没有足够的空闲内存,Unity 会增加堆中的内存量。 这可能是一个缓慢的操作。 然后为变量分配内存。

Heap allocations can be slow, especially if the garbage collector must run and the heap must be expanded.
堆分配可能很慢,尤其是在垃圾收集器必须运行并且堆必须扩展的情况下。

What happens during garbage collection?

When a heap variable goes out of scope, the memory used to store it is not immediately deallocated. Unused heap memory is only deallocated when the garbage collector runs.
当堆变量超出范围时,用于存储它的内存不会立即释放。 未使用的堆内存仅在垃圾收集器运行时才被释放。
Every time the garbage collector runs, the following steps occur:
每次垃圾收集器运行时,都会发生以下步骤:

扫描二维码关注公众号,回复: 14744446 查看本文章
  • The garbage collector examines every object on the heap.
    垃圾收集器检查堆上的每个对象。

  • The garbage collector searches all current object references to determine if the objects on the heap are still in scope.
    垃圾收集器搜索所有当前对象引用以确定堆上的对象是否仍在范围内。

  • Any object which is no longer in scope is flagged for deletion.
    任何不再在范围内的对象都被标记为删除。

  • Flagged objects are deleted and the memory that was allocated to them is returned to the heap.
    标记的对象被删除,分配给它们的内存返回到堆中。

Garbage collection can be an expensive operation. The more objects on the heap, the more work it must do and the more object references in our code, the more work it must do.
垃圾收集可能是一项昂贵的操作。 堆上的对象越多,它必须做的工作就越多,我们代码中的对象引用越多,它必须做的工作就越多。

When does garbage collection happen?

Three things can cause the garbage collector to run:
三件事可能导致垃圾收集器运行:

  • The garbage collector runs whenever a heap allocation is requested that cannot be fulfilled using free memory from the heap.
    每当请求的堆分配无法使用堆中的空闲内存完成时,垃圾收集器就会运行。

  • The garbage collector runs automatically from time to time (although the frequency varies by platform).
    垃圾收集器会不时自动运行(尽管频率因平台而异)。

  • The garbage collector can be forced to run manually.
    垃圾收集器可以强制手动运行。

Garbage collection can be a frequent operation. The garbage collector is triggered whenever a heap allocation cannot be fulfilled from available heap memory, which means that frequent heap allocations and deallocations can lead to frequent garbage collection.
垃圾收集可能是一项频繁的操作。 每当无法从可用堆内存中完成堆分配时,就会触发垃圾收集器,这意味着频繁的堆分配和释放会导致频繁的垃圾收集。

Problems with garbage collection

Now that we understand the role that garbage collection plays in memory management in Unity, we can consider the types of problems that might occur.
现在我们了解了垃圾回收在 Unity 内存管理中的作用,我们可以考虑可能出现的问题类型。

The most obvious problem is that the garbage collector can take a considerable amount of time to run. If the garbage collector has a lot of objects on the heap and/or a lot of object references to examine, the process of examining all of these objects can be slow. This can cause our game to stutter or run slowly.
最明显的问题是垃圾收集器可能需要相当长的时间才能运行。如果垃圾收集器在堆上有很多对象和/或有很多对象引用要检查,那么检查所有这些对象的过程可能会很慢。这会导致我们的游戏卡顿或运行缓慢。

Another problem is that the garbage collector may run at inconvenient times. If the CPU is already working hard in a performance-critical part of our game, even a small amount of additional overhead from garbage collection can cause our frame rate to drop and performance to noticeably change.
另一个问题是垃圾收集器可能在不方便的时候运行。如果 CPU 已经在我们游戏的性能关键部分努力工作,即使垃圾收集产生的少量额外开销也会导致我们的帧速率下降和性能显着变化。

Another problem that is less obvious is heap fragmentation. When memory is allocated from the heap it is taken from the free space in blocks of different sizes depending on the size of data that must be stored. When these blocks of memory are returned to the heap, the heap can get split up into lots of small free blocks separated by allocated blocks. This means that although the total amount of free memory may be high, we are unable to allocate large blocks of memory without running the garbage collector and/or expanding the heap because none of the existing blocks are large enough.
另一个不太明显的问题是堆碎片。当从堆中分配内存时,它会根据必须存储的数据大小从不同大小的块中的空闲空间中获取。当这些内存块返回到堆时,堆可以分成许多由分配块分隔的小空闲块。这意味着尽管可用内存总量可能很高,但我们无法在不运行垃圾收集器和/或扩展堆的情况下分配大块内存,因为现有块都不够大。

There are two consequences to a fragmented heap. The first is that our game’s memory usage will be higher than it needs to be and the second is that the garbage collector will run more frequently. For a more detailed discussion of heap fragmenation, see this Unity best practice guide on performance.
碎片堆有两个后果。第一个是我们游戏的内存使用量会比它需要的高,第二个是垃圾收集器会更频繁地运行。有关堆碎片的更详细讨论,请参阅 this Unity best practice guide on performance

Finding heap allocations

If we know that garbage collection is causing problems in our game, we need to know is which parts of our code are generating garbage. Garbage is generated when heap variables go out of scope, so first we need to know what causes a variable to be allocated on the heap.
如果我们知道垃圾收集在我们的游戏中造成了问题,我们需要知道我们的代码的哪些部分正在产生垃圾。 当堆变量超出范围时会产生垃圾,所以首先我们需要知道是什么导致变量在堆上分配。

What is allocated on the stack and the heap?

In Unity, value-typed local variables are allocated on the stack and everything else is allocated on the heap.
在 Unity 中,值类型的局部变量分配在堆栈上,而其他所有变量都分配在堆上。

The following code is an example of a stack allocation, as the variable localInt is both local and value-typed. The memory allocated for this variable will be deallocated from the stack immediately after this function has finished running.
以下代码是堆栈分配的示例,因为变量 localInt 既是本地的又是值类型的。 为该变量分配的内存将在该函数完成运行后立即从堆栈中释放。

void ExampleFunction() 
{
    
     
	int localInt = 5; 
}

The following code is an example of a heap allocation, as the variable localList is local but reference-typed. The memory allocated for this variable will be deallocated when the garbage collector runs.
以下代码是堆分配的示例,因为变量 localList 是本地的但引用类型的。 当垃圾收集器运行时,分配给这个变量的内存将被释放。

void ExampleFunction() 
{
    
     
	List localList = new List(); 
}

Using the Profiler window to find heap allocations

We can see where our code is creating heap allocations with the Profiler window.
在这里插入图片描述

With the CPU usage profiler selected, we can select any frame to see CPU usage data about that frame in the bottom part of the Profiler window. One of the columns of data is called GC alloc. This column shows heap allocations that are being made in that frame. If we select the column header we can sort the data by this statistic, making it easy to see which functions in our game are causing the most heap allocations. Once we know which function causes the heap allocations, we can examine that function.
选择 CPU 使用分析器后,我们可以选择任何帧以在 Profiler 窗口的底部查看有关该帧的 CPU 使用数据。 其中一列数据称为 GC alloc。 此列显示在该帧中进行的堆分配。 如果我们选择列标题,我们可以按此统计数据对数据进行排序,从而很容易看出我们游戏中的哪些函数导致了最多的堆分配。 一旦我们知道哪个函数导致了堆分配,我们就可以检查那个函数。

Once we know what code within the function is causing garbage to be generated, we can decide how to solve this problem and minimize the amount of garbage generated.
一旦我们知道函数中的哪些代码导致产生垃圾,我们就可以决定如何解决这个问题并最大限度地减少产生的垃圾量。

Reducing the impact of garbage collection

Broadly speaking, we can reduce the impact of garbage collection on our game in three ways:
从广义上讲,我们可以通过三种方式减少垃圾收集对我们游戏的影响:

  • We can reduce the time that the garbage collector takes to run.
    我们可以减少垃圾收集器运行的时间。

  • We can reduce the frequency with which the garbage collector runs.
    我们可以降低垃圾收集器运行的频率。

  • We can deliberately trigger the garbage collector so that it runs at times that are not performance-critical, for example during a loading screen.
    我们可以故意触发垃圾收集器,使其在对性能不重要的时候运行,例如在加载屏幕期间。

With that in mind, there are three strategies that will help us here:
考虑到这一点,这里有三种策略可以帮助我们:

  • We can organise our game so we have fewer heap allocations and fewer object references. Fewer objects on the heap and fewer references to examine means that when garbage collection is triggered, it takes less time to run.
    我们可以组织我们的游戏,从而减少堆分配和对象引用。堆上更少的对象和更少的要检查的引用意味着当垃圾收集被触发时,它需要更少的时间来运行。

  • We can reduce the frequency of heap allocations and deallocations, particularly at performance-critical times. Fewer allocations and deallocations means fewer occasions that trigger garbage collection. This also reduces risk of heap fragmentation.
    我们可以减少堆分配和释放的频率,尤其是在性能关键时刻。更少的分配和释放意味着触发垃圾收集的机会更少。这也降低了堆碎片的风险。

  • We can attempt to time garbage collection and heap expansion so that they happen at predictable and convenient times. This is a more difficult and less reliable approach, but when used as part of an overall memory management strategy can reduce the impact of garbage collection.
    我们可以尝试对垃圾收集和堆扩展进行计时,以便它们在可预测和方便的时间发生。这是一种更困难且不太可靠的方法,但当用作整体内存管理策略的一部分时,可以减少垃圾收集的影响。

Reducing the amount of garbage created

Let’s examine a few techniques that will help us to reduce the amount of garbage generated by our code.
让我们研究一些有助于减少代码产生的垃圾量的技术。

Caching

If our code repeatedly calls functions that lead to heap allocations and then discards the results, this creates unnecessary garbage. Instead, we should store references to these objects and reuse them. This technique is known as caching.
如果我们的代码重复调用导致堆分配的函数然后丢弃结果,这会产生不必要的垃圾。 相反,我们应该存储对这些对象的引用并重用它们。 这种技术被称为caching
In the following example, the code causes a heap allocation each time it is called. This is because a new array is created.
在下面的示例中,代码每次调用时都会导致堆分配。 这是因为创建了一个新数组。

void OnTriggerEnter(Collider other) 
{
    
     
	Renderer[] allRenderers = FindObjectsOfType<Renderer>(); 
	ExampleFunction(allRenderers); 
}

The following code causes only one heap allocation, as the array is created and populated once and then cached. The cached array can be reused again and again without generating more garbage.
以下代码仅导致一次堆分配,因为数组被创建和填充一次,然后被缓存。 缓存的数组可以一次又一次地重复使用,而不会产生更多的垃圾。

private Renderer[] allRenderers; 
void Start() 
{
    
     
	allRenderers = FindObjectsOfType<Renderer>(); 
} 

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

Don’t allocate in functions that are called frequently

If we have to allocate heap memory in a MonoBehaviour, the worst place we can do it is in functions that run frequently. Update() and LateUpdate(), for example, are called once per frame, so if our code is generating garbage here it will quickly add up. We should consider caching references to objects in Start() or Awake() where possible, or ensuring that code that causes allocations only runs when it needs to.
如果我们必须在 MonoBehaviour 中分配堆内存,我们能做到的最糟糕的地方是在频繁运行的函数中。 例如,Update() 和 LateUpdate() 每帧调用一次,所以如果我们的代码在这里生成垃圾,它会很快加起来。 我们应该考虑在可能的情况下在 Start() 或 Awake() 中缓存对对象的引用,或者确保导致分配的代码仅在需要时运行。
Let’s look at a very simple example of moving code so that it only runs when things change. In the following code, a function that causes an allocation is called every time Update() is called, creating garbage frequently:
让我们看一个非常简单的移动代码示例,以便它只在事情发生变化时运行。 在下面的代码中,每次调用 Update() 时都会调用一个导致分配的函数,从而频繁地创建垃圾:

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

With a simple change, we now ensure that the allocating function is called only when the value of transform.position.x has changed. We are now only making heap allocations when necessary rather than in every single frame.
通过一个简单的更改,我们现在确保仅当 transform.position.x 的值发生更改时才调用分配函数。 我们现在只在必要时进行堆分配,而不是在每一帧中。

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

Another technique for reducing garbage generated in Update() is to use a timer. This is suitable for when we have code that generates garbage that must run regularly, but not necessarily every frame.
另一种减少 Update() 中产生的垃圾的技术是使用计时器。 这适用于当我们有生成垃圾的代码必须定期运行但不一定每帧运行时。
Small changes like this, when made to code that runs frequently, can greatly reduce the amount of garbage generated.
当对频繁运行的代码进行这样的小改动时,可以大大减少产生的垃圾量。

Clearing collections

Creating new collections causes allocations on the heap. If we find that we’re creating new collections more than once in our code, we should cache the reference to the collection and use Clear() to empty its contents instead of calling new repeatedly.
创建新集合会导致堆上的分配。 如果我们发现我们在代码中不止一次地创建了新集合,我们应该缓存对集合的引用并使用 Clear() 来清空其内容,而不是重复调用 new。
In the following example, a new heap allocation occurs every time new is used.
在以下示例中,每次使用 new 时都会发生新的堆分配。

void Update() 
{
    
     
	List myList = new List(); 
	PopulateList(myList); 
}

In the following example, an allocation occurs only when the collection is created or when the collection must be resized behind the scenes. This greatly reduces the amount of garbage generated.
在以下示例中,仅在创建集合或必须在后台调整集合大小时才进行分配。 这大大减少了垃圾的产生量。

private List myList = new List(); 
void Update() 
{
    
     
	myList.Clear(); 
	PopulateList(myList); 
}

Object pooling

Even if we reduce allocations within our scripts, we may still have garbage collection problems if we create and destroy a lot of objects at runtime. Object pooling is a technique that can reduce allocations and deallocations by reusing objects rather than repeatedly creating and destroying them. Object pooling is used widely in games and is most suitable for situations where we frequently spawn and destroy similar objects; for example, when shooting bullets from a gun.
即使我们减少脚本中的分配,如果我们在运行时创建和销毁大量对象,我们仍然可能会遇到垃圾收集问题。 对象池是一种通过重用对象而不是重复创建和销毁对象来减少分配和释放的技术。 对象池在游戏中被广泛使用,最适合我们频繁产生和销毁类似对象的情况; 例如,用枪射击子弹时。
A full guide to object pooling is beyond the scope of this article, but it is a really useful technique and one worth learning. This tutorial on object pooling on the Unity Learn site is a great guide to implementing an object pooling system in Unity.
对象池的完整指南超出了本文的范围,但它是一种非常有用的技术,值得学习。 Unity Learn 站点上有关对象池的本教程是在 Unity 中实现对象池系统的绝佳指南。

Common causes of unnecessary heap allocations

We understand that local, value-typed variables are allocated on the stack and that everything else is allocated on the heap. However, there are lots of situations where heap allocations may take us by surprise. Let’s take a look at a few common causes of unnecessary heap allocations and consider how best to reduce these.
我们知道本地的、值类型的变量是在堆栈上分配的,而其他所有变量都是在堆上分配的。 但是,在很多情况下,堆分配可能会让我们感到意外。 让我们看一下不必要的堆分配的一些常见原因,并考虑如何最好地减少这些。

Strings

In C#, strings are reference types not value types, even though they seem to hold the “value” of a string. This means that creating and discarding strings creates garbage. As strings are commonly used in a lot of code, this garbage can really add up.
在 C# 中,字符串是引用类型而不是值类型,即使它们似乎包含字符串的“值”。 这意味着创建和丢弃字符串会产生垃圾。 由于字符串在很多代码中都很常用,所以这些垃圾确实会加起来。
Strings in C# are also immutable, which means that their value can’t be changed after they are first created. Every time we manipulate a string (for example, by using the + operator to concatenate two strings), Unity creates a new string with the updated value and discards the old string. This creates garbage.
C# 中的字符串也是不可变的,这意味着它们的值在首次创建后无法更改。 每次我们操作一个字符串(例如,通过使用 + 运算符连接两个字符串)时,Unity 都会使用更新后的值创建一个新字符串并丢弃旧字符串。 这会产生垃圾。
We can follow a few simple rules to keep garbage from strings to a minimum. Let’s consider these rules, then look at an example of how to apply them.
我们可以遵循一些简单的规则来将字符串中的垃圾保持在最低限度。 让我们考虑这些规则,然后看一个如何应用它们的示例。

  • We should cut down on unnecessary string creation. If we are using the same string value more than once, we should create the string once and cache the value.
    我们应该减少不必要的字符串创建。 如果我们多次使用相同的字符串值,我们应该创建一次字符串并缓存该值。

  • We should cut down on unnecessary string manipulations. For example, if we have a Text component that is updated frequently and contains a concatenated string we could consider separating it into two Text components.
    我们应该减少不必要的字符串操作。 例如,如果我们有一个经常更新的 Text 组件并包含一个连接的字符串,我们可以考虑将它分成两个 Text 组件。

  • If we have to build strings at runtime, we should use the StringBuilder class). The StringBuilder class is designed for building strings without allocations and will save on the amount of garbage we produce when concatenating complex strings.
    如果我们必须在运行时构建字符串,我们应该使用 StringBuilder 类)。 StringBuilder 类设计用于构建没有分配的字符串,并将节省我们在连接复杂字符串时产生的垃圾量。

  • We should remove calls to Debug.Log() as soon as they are no longer needed for debugging purposes. Calls to Debug.Log() still execute in all builds of our game, even if they do not output to anything. A call to Debug.Log() creates and disposes of at least one string, so if our game contains many of these calls, the garbage can add up.
    一旦不再需要用于调试目的,我们就应该删除对 Debug.Log() 的调用。 对 Debug.Log() 的调用仍然在我们游戏的所有构建中执行,即使它们没有输出到任何东西。 对 Debug.Log() 的调用会创建并处理至少一个字符串,因此如果我们的游戏包含许多此类调用,则垃圾会累积起来。
    Let’s examine an example of code that generates unnecessary garbage through inefficient use of strings. In the following code, we create a string for a score display in Update() by combining the string "TIME:“ with the value of the float timer. This creates unnecessary garbage.
    让我们来看一个通过低效使用字符串而产生不必要垃圾的代码示例。 在下面的代码中,我们通过将字符串“TIME:”与浮点计时器的值相结合,在 Update() 中创建一个用于显示分数的字符串。这会产生不必要的垃圾。

public Text timerText; 
private float timer; 
void Update() 
{
    
     
	timer += Time.deltaTime; 
	timerText.text = "TIME:" + timer.ToString(); 
}

In the following example, we have improved things considerably. We put the word “TIME:” in a separate Text component, and set its value in Start(). This means that in Update(), we no longer have to combine strings. This reduces the amount of garbage generated considerably.
在下面的示例中,我们对事情进行了很大改进。 我们将单词“TIME:”放在一个单独的 Text 组件中,并在 Start() 中设置它的值。 这意味着在 Update() 中,我们不再需要组合字符串。 这大大减少了产生的垃圾量。

public Text timerHeaderText; 
public Text timerValueText; 
private float timer; 
void Start() 
{
    
     
	timerHeaderText.text = "TIME:"; 
} 
void Update() 
{
    
     
	timerValueText.text = timer.toString(); 
}

原来是这样:C#中字符串的内存分配与驻留池

Unity function calls

It’s important to be aware that whenever we call code that we didn’t write ourselves, whether that’s in Unity itself or in a plugin, we could be generating garbage. Some Unity function calls create heap allocations, and so should be used with care to avoid generating unnecessary garbage.
重要的是要知道,每当我们调用不是我们自己编写的代码时,无论是在 Unity 本身还是在插件中,我们都可能产生垃圾。一些 Unity 函数调用会创建堆分配,因此应小心使用以避免产生不必要的垃圾。

There is no list of functions that we should avoid. Every function can be useful in some situations and less useful in others. As ever, it’s best to profile our game carefully, identify where garbage is being created and think carefully about how to handle it. In some cases, it may be wise to cache the results of the function; in other cases, it may be wise to call the function less frequently; in other cases, it may be best to refactor our code to use a different function. Having said that, let’s look at a couple of common examples of Unity functions that cause heap allocations and consider how best to handle them.
没有我们应该避免的功能列表。每个功能在某些情况下都可能有用,而在其他情况下则不太有用。与以往一样,最好仔细分析我们的游戏,确定垃圾在哪里产生,并仔细考虑如何处理它。在某些情况下,缓存函数的结果可能是明智之举;在其他情况下,不那么频繁地调用该函数可能是明智的;在其他情况下,最好重构我们的代码以使用不同的函数。话虽如此,让我们看一些导致堆分配的 Unity 函数的常见示例,并考虑如何最好地处理它们。

Every time we access a Unity function that returns an array, a new array is created and passed to us as the return value. This behaviour isn’t always obvious or expected, especially when the function is an accessor (for example, Mesh.normals).
每次我们访问一个返回数组的 Unity 函数时,都会创建一个新数组并作为返回值传递给我们。这种行为并不总是明显或预期的,尤其是当函数是 accessor(例如,Mesh.normals)。

In the following code, a new array is created for each iteration of the loop.
在以下代码中,为循环的每次迭代创建一个新数组。

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

It’s easy to reduce allocations in cases like this: we can simply cache a reference to the array. When we do this, only one array is created and the amount of garbage created is reduced accordingly.
在这种情况下减少分配很容易:我们可以简单地缓存对数组的引用。 当我们这样做时,只会创建一个数组,并且创建的垃圾量会相应减少。
The following code demonstrates this. In this case, we call Mesh.normals before the loop runs and cache the reference so that only one array is created.
下面的代码演示了这一点。 在这种情况下,我们在循环运行之前调用 Mesh.normals 并缓存引用,以便只创建一个数组。

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

Another unexpected cause of heap allocations can be found in the functions GameObject.name or GameObject.tag. Both of these are accessors that return new strings, which means that calling these functions will generate garbage. Caching the value may be useful, but in this case there is a related Unity function that we can use instead. To check a GameObject’s tag against a value without generating garbage, we can use GameObject.CompareTag().
堆分配的另一个意外原因可以在函数 GameObject.name 或 GameObject.tag 中找到。 这两个都是返回新字符串的访问器,这意味着调用这些函数会产生垃圾。 缓存值可能很有用,但在这种情况下,我们可以使用相关的 Unity 函数。 为了在不产生垃圾的情况下检查游戏对象的标签,我们可以使用 GameObject.CompareTag()。
In the following example code, garbage is created by the call to GameObject.tag:

private string playerTag = "Player"; 
void OnTriggerEnter(Collider other) 
{
    
     
	bool isPlayer = other.gameObject.tag == playerTag; 
}

If we use GameObject.CompareTag(), this function no longer generates any garbage:

private string playerTag = "Player"; 
void OnTriggerEnter(Collider other) 
{
    
     
	bool isPlayer = other.gameObject.CompareTag(playerTag); 
}

GameObject.CompareTag isn’t unique; many Unity function calls have alternative versions that cause no heap allocations. For example, we could use Input.GetTouch() and Input.touchCount in place of Input.touches, or Physics.SphereCastNonAlloc() in place of Physics.SphereCastAll().
GameObject.CompareTag 不是唯一的; 许多 Unity 函数调用都有替代版本,不会导致堆分配。 例如,我们可以使用 Input.GetTouch() 和 Input.touchCount 代替 Input.touches,或 Physics.SphereCastNonAlloc() 代替 Physics.SphereCastAll()。

Boxing

Boxing is the term for what happens when a value-typed variable is used in place of a reference-typed variable. Boxing usually occurs when we pass value-typed variables, such as ints or floats, to a function with object parameters such as Object.Equals().
装箱是使用值类型变量代替引用类型变量时发生的情况的术语。 当我们将值类型的变量(例如整数或浮点数)传递给具有对象参数(例如 Object.Equals())的函数时,通常会发生装箱。
For example, the function String.Format() takes a string and an object parameter. When we pass it a string and an int, the int must be boxed. Therefore the following code contains an example of boxing:
例如,函数 String.Format() 接受一个字符串和一个对象参数。 当我们向它传递一个字符串和一个 int 时,这个 int 必须被装箱。 因此,以下代码包含一个装箱示例:

void ExampleFunction() 
{
    
     
	int cost = 5; 
	string displayString = String.Format("Price: {0} gold", cost); 
}

Boxing creates garbage because of what happens behind the scenes. When a value-typed variable is boxed, Unity creates a temporary System.Object on the heap to wrap the value-typed variable. A System.Object is a reference-typed variable, so when this temporary object is disposed of this creates garbage.
由于幕后发生的事情,装箱会产生垃圾。 当值类型变量被装箱时,Unity 在堆上创建一个临时 System.Object 来包装值类型变量。 System.Object 是一个引用类型的变量,所以当这个临时对象被释放时,这会产生垃圾。
Boxing is an extremely common cause of unnecessary heap allocations. Even if we don’t box variables directly in our code, we may be using plugins that cause boxing or it may be happening behind the scenes of other functions. It’s best practice to avoid boxing wherever possible and to remove any function calls that lead to boxing.
装箱是不必要的堆分配的一个极其常见的原因。 即使我们没有直接在代码中装箱变量,我们也可能使用导致装箱的插件,或者它可能发生在其他函数的幕后。 最好的做法是尽可能避免装箱并删除任何导致装箱的函数调用。

Coroutines

Calling StartCoroutine() creates a small amount of garbage, because of the classes that Unity must create instances of to manage the coroutine. With that in mind, calls to StartCoroutine() should be limited while our game is interactive and performance is a concern. To reduce garbage created in this way, any coroutines that must run at performance-critical times should be started in advance and we should be particularly careful when using nested coroutines that may contain delayed calls to StartCoroutine().
由于 Unity 必须创建实例来管理协程,调用 StartCoroutine() 会产生少量垃圾。 考虑到这一点,当我们的游戏是交互式的并且性能是一个问题时,应该限制对 StartCoroutine() 的调用。 为了减少以这种方式产生的垃圾,任何必须在性能关键时刻运行的协程都应该提前启动,并且在使用可能包含对 StartCoroutine() 的延迟调用的嵌套协程时我们应该特别小心。
yield statements within coroutines do not create heap allocations in their own right; however, the values we pass with our yield statement could create unnecessary heap allocations. For example, the following code creates garbage:
协程中的 yield 语句不会自行创建堆分配; 然而,我们通过 yield 语句传递的值可能会造成不必要的堆分配。 例如,以下代码会创建垃圾:

yield return 0;

This code creates garbage because the int with a value of 0 is boxed. In this case, if we wish to simply wait for a frame without causing any heap allocations, the best way to do so is with this code:
此代码创建垃圾,因为值为 0 的 int 已装箱。 在这种情况下,如果我们希望简单地等待一个帧而不引起任何堆分配,那么最好的方法是使用以下代码:

yield return null;

Another common mistake with coroutines is to use new when yielding with the same value more than once. For example, the following code will create and then dispose of a WaitForSeconds object each time the loop iterates:
协程的另一个常见错误是在多次使用相同的值时使用 new。 例如,以下代码将在每次循环迭代时创建并释放 WaitForSeconds 对象:

while (!isComplete) 
{
    
     
	yield return new WaitForSeconds(1f); 
}

If we cache and reuse the WaitForSeconds object, much less garbage is created. The following code shows this as an example:
如果我们缓存并重用 WaitForSeconds 对象,创建的垃圾就会少得多。 下面的代码作为一个例子展示了这一点:

WaitForSeconds delay = new WaitForSeconds(1f); 
while (!isComplete) 
{
    
     
	yield return delay; 
}

If our code generates a lot of garbage due to coroutines, we may wish to consider refactoring our code to use something other than coroutines. Refactoring code is a complex subject and every project is unique, but there are a couple of common alternatives to coroutines that we may wish to bear in mind. For example, if we are using coroutines mainly to manage time, we may wish to simply keep track of time in an Update() function. If we are using coroutines mainly to control the order in which things happen in our game, we may wish to create some sort of messaging system to allow objects to communicate. There is no one size fits all approach to this, but it is useful to remember that there is often more than one way to achieve the same thing in code.
如果我们的代码由于协程而产生大量垃圾,我们不妨考虑重构我们的代码以使用协程以外的东西。 重构代码是一个复杂的主题,每个项目都是独一无二的,但是我们可能希望牢记一些协同程序的常见替代方案。 例如,如果我们主要使用协程来管理时间,我们可能希望在 Update() 函数中简单地跟踪时间。 如果我们主要使用协程来控制游戏中事物发生的顺序,我们可能希望创建某种消息系统来允许对象进行通信。 没有一种适合所有方法的方法,但记住在代码中实现相同目标的方法通常不止一种。

一个空协程产生60B的GC,使用yield return 0 额外多20B的GC,使用yield return null 则不会产生多余gc

foreach loops

In versions of Unity prior to 5.5, a foreach loop iterating over anything other than an array generates garbage each time the loop terminates. This is due to boxing that happens behind the scenes. A System.Object is allocated on the heap when the loop begins and disposed of when the loop terminates. This problem was fixed in Unity 5.5.
在 5.5 之前的 Unity 版本中,foreach 循环遍历数组以外的任何内容,每次循环终止时都会产生垃圾。 这是由于发生在幕后的装箱。 System.Object 在循环开始时在堆上分配,在循环终止时被释放。 这个问题在 Unity 5.5 中得到修复。
For example, in versions of Unity prior to 5.5, the loop in the following code generates garbage:
例如,在 5.5 之前的 Unity 版本中,以下代码中的循环会产生垃圾:

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

Function references

References to functions, whether they refer to anonymous methods or named methods, are reference-typed variables in Unity. They will cause heap allocations. Converting an anonymous method to a closure (where the anonymous method has access to the variables in scope at the time of its creation) significantly increases the memory usage and the number of heap allocations.
对函数的引用,无论是匿名方法还是命名方法,都是 Unity 中的引用类型变量。 它们将导致堆分配。 将匿名方法转换为闭包)(匿名方法在创建时可以访问范围内的变量)显着增加了内存使用量和堆分配的数量。

The precise details of how function references and closures allocate memory vary depending on platform and compiler settings, but if garbage collection is a concern then it’s best to minimize the use of function references and closures during gameplay. This Unity best practice guide on performance goes into greater technical detail on this topic.
函数引用和闭包如何分配内存的确切细节因平台和编译器设置而异,但如果垃圾收集是一个问题,那么最好在游戏过程中尽量减少函数引用和闭包的使用。 本 Unity 性能最佳实践指南详细介绍了该主题的技术细节。
C# 本地函数与 Lambda 表达式

LINQ and Regular Expressions

Both LINQ and Regular Expressions generate garbage due to boxing that occurs behind the scenes. It is best practice to avoid using these altogether where performance is a concern. Again, this Unity best practice guide on performance provides greater technical detail about this subject.
由于发生在幕后的装箱,LINQ 和正则表达式都会产生垃圾。 在关注性能的情况下,最好避免完全使用这些。 同样,此 Unity 性能最佳实践指南提供了有关此主题的更多技术细节。

Structuring our code to minimize the impact of garbage collection

The way that our code is structured can impact garbage collection. Even if our code does not create heap allocations, it can add to the garbage collector’s workload.
我们代码的结构方式会影响垃圾收集。 即使我们的代码没有创建堆分配,它也会增加垃圾收集器的工作量。
One way that our code can unnecessarily add to the garbage collector’s workload is by requiring it to examine things that it should not have to examine. Structs are value-typed variables, but if we have a struct that contains contains a reference-typed variable then the garbage collector must examine the whole struct. If we have a large array of these structs, then this can create a lot of additional work for the garbage collector.
我们的代码不必要地增加垃圾收集器工作量的一种方法是要求它检查它不应该检查的东西。 结构是值类型变量,但如果我们有一个包含引用类型变量的结构,那么垃圾收集器必须检查整个结构。 如果我们有大量这些结构体,那么这会给垃圾收集器带来很多额外的工作。
In this example, the struct contains a string, which is reference-typed. The whole array of structs must now be examined by the garbage collector when it runs.
在此示例中,结构包含一个字符串,它是引用类型的。 现在,垃圾收集器在运行时必须检查整个结构数组。


public struct ItemData 
{
    
     
	public string name; 
	public int cost; 
	public Vector3 position; 
}


private ItemData[] itemData;

In this example, we store the data in separate arrays. When the garbage collector runs, it need only examine the array of strings and can ignore the other arrays. This reduces the work that the garbage collector must do.
在此示例中,我们将数据存储在单独的数组中。 当垃圾收集器运行时,它只需要检查字符串数组,可以忽略其他数组。 这减少了垃圾收集器必须做的工作。

private string[] itemNames; 
private int[] itemCosts; 
private Vector3[] itemPositions;

Another way that our code can unnecessarily add to the garbage collector’s workload is by having unnecessary object references. When the garbage collector searches for references to objects on the heap, it must examine every current object reference in our code. Having fewer object references in our code means that it has less work to do, even if we don’t reduce the total number of objects on the heap.
我们的代码可能不必要地添加到垃圾收集器的工作负载的另一种方式是使用不必要的对象引用。 当垃圾收集器在堆上搜索对对象的引用时,它必须检查我们代码中的每个当前对象引用。 在我们的代码中拥有更少的对象引用意味着它有更少的工作要做,即使我们不减少堆上的对象总数。
In this example, we have a class that populates a dialog box. When the user has viewed the dialog, another dialog box is displayed. Our code contains a reference to the next instance of DialogData that should be displayed, meaning that the garbage collector must examine this reference as part of its operation:
在此示例中,我们有一个填充对话框的类。 当用户查看对话框时,会显示另一个对话框。 我们的代码包含对应该显示的下一个 DialogData 实例的引用,这意味着垃圾收集器必须检查这个引用作为其操作的一部分:

public class DialogData 
{
    
     
	private DialogData nextDialog; 
	public DialogData GetNextDialog() 
	{
    
     
		return nextDialog; 
	} 
}

Here, we have restructured the code so that it returns an identifier that is used to look up the next instance of DialogData, instead of the instance itself. This is not an object reference, so it does not add to the time taken by the garbage collector.
在这里,我们重新构造了代码,使其返回一个标识符,该标识符用于查找 DialogData 的下一个实例,而不是实例本身。 这不是对象引用,因此它不会增加垃圾收集器所花费的时间。

public class DialogData 
{
    
     
	private int nextDialogID; 
	public int GetNextDialogID() 
	{
    
     
		return nextDialogID; 
	} 
}

On its own, this example is fairly trivial. However, if our game contains a great many objects that hold references to other objects, we can considerably reduce the complexity of the heap by restructuring our code in this fashion.
就其本身而言,这个例子是相当微不足道的。 但是,如果我们的游戏包含大量包含对其他对象的引用的对象,我们可以通过以这种方式重构我们的代码来显着降低堆的复杂性。

Timing garbage collection

Manually forcing garbage collection

Finally, we may wish to trigger garbage collection ourselves. If we know that heap memory has been allocated but is no longer used (for example, if our code has generated garbage when loading assets) and we know that a garbage collection freeze won’t affect the player (for example, while the loading screen is still showing), we can request garbage collection using the following code:
最后,我们不妨自己触发垃圾回收。 如果我们知道堆内存已分配但不再使用(例如,如果我们的代码在加载资产时产生了垃圾)并且我们知道垃圾回收冻结不会影响播放器(例如,在加载屏幕时 仍在显示),我们可以使用以下代码请求垃圾收集:
System.GC.Collect();
This will force the garbage collector to run, freeing up the unused memory at a time that is convenient for us.
这将强制垃圾收集器运行,在我们方便的时候释放未使用的内存。

Conclusion

We’ve learned how garbage collection works in Unity, why it can cause performance problems and how to minimize its impact on our game. Using this knowledge and our profiling tools, we can fix performance problems related to garbage collection and structure our games so that they manage memory efficiently.
我们已经了解了 Unity 中垃圾收集的工作原理、它为什么会导致性能问题以及如何将其对我们游戏的影响降到最低。 使用这些知识和我们的分析工具,我们可以解决与垃圾收集相关的性能问题并构建我们的游戏,以便它们有效地管理内存。
The links below provide further information on the topics covered in this article.
下面的链接提供了有关本文所涵盖主题的更多信息。

Further reading

Memory management and garbage collection in Unity

Unity Manual: Understanding Optimization in Unity

Unity Manual: Understanding Automatic Memory Management

Gamasutra: C# Memory Management for Unity Developers by Wendelin Reich

Gamasutra: C# memory and performance tips for Unity by Robert Zubek

Gamasutra: Reducing memory allocations to avoid Garbage Collection on Unity by Grhyll JDD

Gamasutra: Unity Garbage Collection Tips and Tricks by Megan Hughes

Boxing

MSDN: Boxing and Unboxing (C# Programming Guide)

Object pooling

Unity Learn: Object Pooling Tutorial

Wikipedia: Object Pool Pattern

Strings

Best Practices for Using Strings in the .NET Framework

猜你喜欢

转载自blog.csdn.net/fztfztfzt/article/details/122785763