理解自动内存管理外文翻译资料

 2022-12-10 16:23:23

Understanding Automatic Memory Management

When an object, string or array is created, the memory required to store it is allocated from a central pool called the heap. When the item is no longer in use, the memory it once occupied can be reclaimed and used for something else. In the past, it was typically up to the programmer to allocate and release these blocks of heap memory explicitly with the appropriate function calls. Nowadays, runtime systems like Unityrsquo;s Mono engine manage memory for you automatically. Automatic memory management requires less coding effort than explicit allocation/release and greatly reduces the potential for memory leakage (the situation where memory is allocated but never subsequently released).

Value and Reference Types

When a function is called, the values of its parameters are copied to an area of memory reserved for that specific call. Data types that occupy only a few bytes can be copied very quickly and easily. However, it is common for objects, strings and arrays to be much larger and it would be very inefficient if these types of data were copied on a regular basis. Fortunately, this is not necessary; the actual storage space for a large item is allocated from the heap and a small “pointer” value is used to remember its location. From then on, only the pointer need be copied during parameter passing. As long as the runtime system can locate the item identified by the pointer, a single copy of the data can be used as often as necessary.

Types that are stored directly and copied during parameter passing are called value types. These include integers, floats, booleans and Unityrsquo;s struct types (eg, Color and Vector3). Types that are allocated on the heap and then accessed via a pointer are called reference types, since the value stored in the variable merely “refers” to the real data. Examples of reference types include objects, strings and arrays.

Allocation and Garbage Collection

The memory manager keeps track of areas in the heap that it knows to be unused. When a new block of memory is requested (say when an object is instantiated), the manager chooses an unused area from which to allocate the block and then removes the allocated memory from the known unused space. Subsequent requests are handled the same way until there is no free area large enough to allocate the required block size. It is highly unlikely at this point that all the memory allocated from the heap is still in use. A reference item on the heap can only be accessed as long as there are still reference variables that can locate it. If all references to a memory block are gone (ie, the reference variables have been reassigned or they are local variables that are now out of scope) then the memory it occupies can safely be reallocated.

To determine which heap blocks are no longer in use, the memory manager searches through all currently active reference variables and marks the blocks they refer to as “live”. At the end of the search, any space between the live blocks is considered empty by the memory manager and can be used for subsequent allocations. For obvious reasons, the process of locating and freeing up unused memory is known as garbage collection (or GC for short).

Optimization

Garbage collection is automatic and invisible to the programmer but the collection process actually requires significant CPU time behind the scenes. When used correctly, automatic memory management will generally equal or beat manual allocation for overall performance. However, it is important for the programmer to avoid mistakes that will trigger the collector more often than necessary and introduce pauses in execution.

There are some infamous algorithms that can be GC nightmares even though they seem innocent at first sight. Repeated string concatenation is a classic example:

function ConcatExample(intArray: int[]) {

var line = intArray[0].ToString();

for (i = 1; i lt; intArray.Length; i ) {

line = ', ' intArray[i].ToString();

}

return line;

}

The key detail here is that the new pieces donrsquo;t get added to the string in place, one by one. What actually happens is that each time around the loop, the previous contents of the line variable become dead - a whole new string is allocated to contain the original piece plus the new part at the end. Since the string gets longer with increasing values of i, the amount of heap space being consumed also increases and so it is easy to use up hundreds of bytes of free heap space each time this function is called. If you need to concatenate many strings together then a much better option is the Mono libraryrsquo;sSystem.Text.StringBuilder class.

However, even repeated concatenation wonrsquo;t cause too much trouble unless it is called frequently, and in Unity that usually implies the frame update. Something like:

var scoreBoard: GUIText;

var score: int;

function Update() {

var scoreText: String = 'Score: ' score.ToString();

scoreBoard.text = scoreText;

}

hellip;will allocate new strings each time Update is called and generate a constant trickle of new garbage. Most of that can be saved by updating the text only when the score changes:

var scoreBoard: GUIText;

var scoreText: String;

var score: int;

var oldScore: int;

function Update() {

if (score != oldScore) {

scoreText = 'Score: '

剩余内容已隐藏,支付完成后下载完整资料


理解自动内存管理

当一个对象、字符串或数组被创建时,存储所需的内存就从一个中央池中分配,称为堆。当项目不再使用时,内存就可以被回收并用于别的项目。在过去,通常是由程序员用适当的函数调用来明确地分配和释放这些堆中的内存块。现在,像Unity的Mono引擎这样的运行时系统自动为你管理内存。自动内存管理比显式内存分配/释放需要更少的编码工作,而且潜在的内存泄漏(内存被分配后没有释放掉)隐患也大大降低了。

值类型和引用类型

当一个函数被调用时,其参数的·值被复制到该特定调用的内存区域中。占用只有几个字节类型的数据,可以很快很容易地复制。然而,对象、字符串和数组通常都占有较大内存,如果这些类型的数据被定期的复制,这样效率是非常低的。幸运的是,这是没有必要的;实际上占用较大内存的项目其存储空间是从堆上分配的,一个小的“指针”值是用来记住它的位置。因此,在参数传递过程中只需要复制指针就行了。只要运行时系统可以找到由指针确定的项目,可能被使用数据的简单重复复制就可以应需求而定了。

在参数传递过程中被直接存储和复制的数据的数据类型被称为值类型。这些包括整数、浮点数、布尔值和Unity的结构类型(如Color和Vector3)。在堆上分配内存而且能够通过指针访问的类型被称为引用类型,因为在变量中存储的值仅仅是“指”向真实数据。引用类型的例子包括对象、字符串和数组。

内存分配和垃圾回收

内存管理器跟踪堆中的确定不再使用的内存区域。当有一个新的内存块的要求(比如当一个对象被实例化)时,内存管理器从堆中选择一个未使用的区域去分配内存块,然后把堆中没有使用空间内已分配的内存删除掉。随后的请求以同样的方式处理,直到没有足够大的内存区域来分配所需的块大小。所有在堆上分配的内存一直在使用的情况是非常不可能的。堆中的引用类型项目,只有在引用变量可以定位到它时才能够访问。如果一个内存块的引用都消失了(即参考变量被重新分配,或是现在的范围局部变量)那么它所占用的内存就可以被安全地重新分配。

要确定哪些堆块不再在使用中,内存管理器要搜索当前所有活动的引用变量,并标记它们所指的块为“活”。在搜索结束时,内存管理器指定所有在活的块之间的内存区域为空,可用于后续的分配。由于明显的原因,将定位和释放未使用的内存的过程称为垃圾收集(或简称为GC)。

优化

垃圾收集是自动的,对程序员是不可见的,但收集过程中实际需要花费场景后相当的处理器时间。当正确使用时,自动内存管理一般会等于或超过手动分配的整体性能。然而,对程序员而言,避免触发不必要的垃圾收集及其所引发的程序执行中暂停是非常重要的。

有一些垃圾回收算法第一眼看上去很平凡,但其实是臭名昭著并能引发垃圾回收的噩梦。重复的字符串是一个经典的例子:

function ConcatExample(intArray: int[]) {

var line = intArray[0].ToString();

for (i = 1; i lt; intArray.Length; i ) {

line = ', ' intArray[i].ToString();

}

return line;

}

这里的关键细节是,新的字符串片段不会一个一个地添加到字符串中。实际发生的是,每一次循环中,之前line变量的内容都会变死,内存中新分配了一个字符串来包含原来的字符串再加上新的字符串部分。随着i值得增加字符串变得越来越长,我的堆空间的消耗量也随之增加,因此每次调用这个函数时很容易用上百个字节的自由堆空间。如果你需要连接很多字符串,一个更好的选择是Mono 库中的system.text.stringbuilder类。

然而,即使重复串联不会造成太大的麻烦,除非重复被频繁调用,在Unity中通常意味帧更新。像:

var scoreBoard: GUIText;

var score: int;

function Update() {

var scoreText: String = 'Score: ' score.ToString();

scoreBoard.text = scoreText;

}

hellip;hellip;每次Update被调用的时候都会重新分配一个新的字符串,产生一个个持续不断的新的垃圾。只有当score改变时,可以通过更新文本来保存这些:

var scoreBoard: GUIText;

var scoreText: String;

var score: int;

var oldScore: int;

function Update() {

if (score != oldScore) {

scoreText = 'Score: ' score.ToString();

scoreBoard.text = scoreText;

oldScore = score;

}

}

当函数返回数组值时,会出现另一个潜在问题:

function RandomList(numElements: int) {

var result = new float[numElements];

for (i = 0; i lt; numElements; i ) {

result[i] = Random.value;

}

return result;

}

当创建一个新的数组并赋值时,这种函数是非常优雅和方便的。但是,如果它被重复调用,那么每次都将分配新的内存。由于数组可能非常大,自由的堆空间便会很快用光,导致频繁的垃圾收集。避免这个问题,就要利用数组是一个引用类型的事实。一个数组传递到一个函数,作为参数可以在该函数中进行修改,并将在函数返回后保留结果。类似上面的一个函数可以被替换为类似的:

function RandomList(arrayToFill: float[]) {

for (i = 0; i lt; arrayToFill.Length; i ) {

arrayToFill[i] = Random.value;

}

}

这仅仅是用新值替换数组中已有的内容。虽然这需要在调用当前代码前数组就已经被分配内存并初始化了(这似乎有些不雅),当该函数被调用时,它将不会产生任何新的垃圾。

请求垃圾收集

如上所述,最好尽可能避免分配。然而,鉴于他们不能完全消除,有2个主要的策略,你可以使用,以减少他们的发生:

小堆,快速而频繁的垃圾收集

在一个以平滑的帧率作为主要关注点的长时间的游戏中,这种策略往往是最好的。像这样的游戏通常会经常分配小的块,但这些块只会短暂的被使用。堆的大小,在iOS上使用时,这种策略是大约200KB,iPhone 3G需要花费5毫秒进行垃圾收集。如果堆增加到1MB,收集大约需要7毫秒。因此可以请求一个垃圾收集在一个固定的帧间隔内。这通常会使垃圾收集更经常发生,而不是严格必要的,但它们将被迅速处理,并对游戏的影响最小:

if (Time.frameCount % 30 == 0)

{

System.GC.Collect]();

}

然而,你应该谨慎使用这种技术,并检查分析器的统计数据,以确保它确实减少了游戏的收集时间。

大堆,缓慢但极少的垃圾收集

对于分配次数比较少,可以在暂停处处理的游戏,这一策略的效果最好。只要不会被系统杀死,堆得大小可以尽可能的大。然而,Mono的运行时避免了所有可能的堆自动扩展。你可以在游戏启动时预分配一些占位符空间来扩展堆得大小(即你实例化一个只对内存管理器分配内存有影响的“无用”的对象):

function Start() {

var tmp = new System.Object[1024];

// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks

for (var i : int = 0; i lt; 1024; i )

tmp[i] = new byte[1024];

// release reference

tmp = null;

}

一个足够大的堆不应该完全去填补那些在游戏的暂停当中占据一个集合大小的数据。当这样的停顿发生时,您可以明确地请求一个垃圾回收:

System.GC.Collect();

再次,在使用这一策略时,你应该小心并注意分析器的统计,而不是只看是不是有希望的效果。

可重用的对象池

有很多情况下,您可以通过减少创建和销毁的对象的数目,以避免产生垃圾。游戏中有某些类型的物件,如射弹,可能会被反复遇到,即使只有一次只有一小部分会在游戏中被打过。在这种情况下,尽可能的去重用这个对象,并不是摧毁并用新的替换之。

更多信息

内存管理是一个微妙的和复杂的主题,大量的学术努力一直致力于。如果你有兴趣学习更多关于它memorymanagement.org是极好的资源,有许多出版物和网上的文章。关于对象池的更多信息可以在维基百科的页面也在sourcemaking.com。

剩余内容已隐藏,支付完成后下载完整资料


资料编号:[31118],资料为PDF文档或Word文档,PDF文档可免费转换为Word

您需要先支付 30元 才能查看全部内容!立即支付

发小红书推广免费获取该资料资格。点击链接进入获取推广文案即可: Ai一键组稿 | 降AI率 | 降重复率 | 论文一键排版