Order of Execution for Event Functions
플랫폼별 컴파일

Understanding Automatic Memory Management

오브젝트나 문자열, 배열을 생성한 이후 저장하려면 메모리 공간이 필요합니다. 필요한 공간은 heap이라고 하는 중심 풀에서 할당됩니다. 메모리 공간을 할당받은 항목이 더 이상 사용되지 않게 되면 차지하던 메모리를 회수하여 다른 항목을 저장하는 데 사용할 수 있습니다. 이전에는 프로그래머가 적절한 함수 호출을 통해 명시적으로 힙 메모리 블록을 할당하고 회수해야 했습니다. 하지만 최근에는 Unity Mono 엔진과 같은 런타임 시스템이 자동으로 메모리 관리를 수행합니다. 자동으로 메모리를 관리하면 명시적으로 할당하고 회수하는 것보다 코딩이 덜 필요하며 메모리 누수 현상이 발생할 가능성을 낮춥니다(메모리 누수 현상이란, 메모리가 할당된 이후 회수되지 않는 경우를 의미합니다).

Value and Reference Types

함수가 호출되면 하위 파라미터의 값이 해당 함수 호출을 위해 지정된 메모리 구역에 복사됩니다. 몇 바이트만 차지하는 데이터 타입은 빠르고 쉽게 복사할 수 있지만, 보통 오브젝트, 문자열, 배열은 이보다 더 큰 경우가 많습니다. 따라서 이를 주기적으로 복사하는 것은 대단히 비효율적이지만, 다행히도 꼭 그렇게 할 필요는 없습니다. 큰 항목의 실제 저장 공간은 힙에서 할당되며, 저장되는 위치를 기억하기 위해 작은 “포인터” 값이 사용됩니다. 그 이후부터는 파라미터를 패스할 때 포인터만 복사하면 됩니다. 런타임 시스템이 포인터가 식별하는 항목을 찾을 수 있는 경우 데이터를 한 번만 복사하더라도 여러 번 사용할 수 있습니다.

값 타입은 파라미터가 전달되는 동안 사본이 직접 저장되는 타입입니다. 이 타입으로는 정수, 부동소수점, 부울, Unity의 구조체 타입(예를 들어, ColorVector3)이 포함됩니다. 힙에 할당하고 그 후 포인터를 통해 액세스하는 타입을 레퍼런스 타입이라고 하며 변수에 저장되는 값은 어디까지나 실제 데이터를 “참조”합니다. 참조 타입의 예로는 오브젝트, 문자열, 배열 등이 있습니다.

Allocation and Garbage Collection

메모리 관리자는 힙에서 사용되지 않는 영역을 트래킹합니다. 오브젝트가 인스턴스화되는 것과 같이 새로운 메모리 블록이 요청되는 경우, 관리자는 블록을 할당하기 위해 미사용 영역을 선택한 후 할당된 메모리를 제거합니다. 이 과정은 필요한 블록 크기를 할당할 수 없는 빈 공간이 없을 때까지 반복됩니다. 이 시점에서는 힙에서 할당된 모든 메모리가 사용 중일 가능성이 매우 낮습니다. 힙에 있는 참조 항목을 접근하려면 해당 항목을 찾을 수 있도록하는 참조 변수가 필요합니다. 참조 변수가 재할당되거나 로컬 변수로 변하는 경우와 같이 메모리 블록에 대한 모든 참조가 사라진 경우, 해당 메모리 블록을 안전하게 재할당할 수 있게 됩니다.

어떤 힙 블록이 더 이상 사용되지 않고 있는지를 확인하기 위해, 메모리 관리자는 현재 모든 액티브 참조 변수를 검색하고 이 변수가 참조하는 블록을 “살아있음(live)”이라고 표시합니다. 검색이 끝나면 메모리 관리자는 살아 있는 블록 사이의 모든 공간을 비어 있다고 간주하며 다음 할당 요청 시 사용할 수 있다고 간주합니다. 이러한 이유로, 미사용 메모리를 파악하고 해제하는 프로세스를 가비지 컬렉션(garbage collection, GC)이라 합니다.

Unity uses the Boehm–Demers–Weiser garbage collector, a stop-the-world garbage collector. Whenever Unity needs to perform garbage collection, it stops running your program code and only resumes normal execution when the garbage collector has finished all its work. This interruption can cause delays in the execution of your game that last anywhere from less than one millisecond to hundreds of milliseconds, depending on how much memory the garbage collector needs to process and on the platform the game is running on. For real-time applications like games, this can become quite a big issue, because you can’t sustain the consistent frame rate that smooth animation require when the garbage collector suspends a game’s execution. These interruptions are also known as GC spikes, because they show as spikes in the Profiler frame time graph. In the next sections you can learn more about how to write your code to avoid unnecessary garbage-collected memory allocations while running the game, so the garbage collector has less work to do.

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.

처음에는 아무 문제 없어 보일지라도 이후 GC의 악몽을 불러올 수 있는 악명 높은 알고리즘이 일부 존재합니다. 가장 잘 알려진 사례로 문자열 접합 반복이 있습니다.

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    void ConcatExample(int[] intArray) {
        string line = intArray[0].ToString();
        
        for (i = 1; i < intArray.Length; i++) {
            line += ", " + intArray[i].ToString();
        }
        
        return line;
    }
}

여기서 주목해야 하는 사실은 새로운 조각이 문자열이 한 개씩 추가되지는 않는다는 점입니다. 실제로는 루프가 실행될 때마다 라인 변수의 이전 내용이 삭제되며, 기존 조각 끝에 새로운 부분이 더해진 형태의 새로운 문자열이 할당됩니다. i의 값이 커질수록 문자열은 길어지므로, 사용되는 힙 공간 역시 증가합니다. 따라서 이 함수는 매번 호출될 때마다 순식간에 빈 힙 공간을 수백 바이트 사용합니다. 문자열을 동시에 여러 개 연결해야 하는 경우, Mono 라이브러리의 System.Text.StringBuilder 클래스를 사용하는 것이 좋습니다.

하지만 문자열 연결이 반복되어도 지나치게 자주 호출되지 않은 이상 큰 문제는 일어나지 않습니다. 예를 들어, 아래와 같이 Unity에서 프레임 업데이트마다 문자열 연결을 반복하지만 않으면 됩니다.

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public GUIText scoreBoard;
    public int score;
    
    void Update() {
        string scoreText = "Score: " + score.ToString();
        scoreBoard.text = scoreText;
    }
}

…위 코드는 Update가 호출될 때마다 새 문자열을 할당하며 지속적으로 새 가비지 메모리를 조금씩 생성합니다. 대부분의 메모리 누수는 score가 변경될 때에만 텍스트를 업데이트하여 줄일 수 있습니다.

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    public GUIText scoreBoard;
    public string scoreText;
    public int score;
    public int oldScore;
    
    void Update() {
        if (score != oldScore) {
            scoreText = "Score: " + score.ToString();
            scoreBoard.text = scoreText;
            oldScore = score;
        }
    }
}

발생할 수 있는 또 다른 문제로는 어떤 함수가 배열 값을 반환하는 경우가 있습니다.

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    float[] RandomList(int numElements) {
        var result = new float[numElements];
        
        for (int i = 0; i < numElements; i++) {
            result[i] = Random.value;
        }
        
        return result;
    }
}

이 함수 타입은 값으로 채워진 새로운 배열을 생성하는 데 사용하면 매우 편리합니다. 하지만 이 타입을 자주 호출하면 매번 새로운 메모리가 할당됩니다. 보통 배열을 크기가 상당히 크므로 빈 힙 공간이 빠르게 소모되어 자주 가비지 컬렉션을 해야합니다. 배열이 참조 타입이라는 점을 활용하면 이 문제를 피할 수 있습니다. 함수에 파라미터로 전달된 배열은 해당 함수에서 수정할 수 있으며 수정 결과는 함수를 리턴해도 유지됩니다. 이 점을 활용하면 위의 함수를 아래와 같이 수정할 수 있습니다.

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    void RandomList(float[] arrayToFill) {
        for (int i = 0; i < arrayToFill.Length; i++) {
            arrayToFill[i] = Random.value;
        }
    }
}

위 함수는 배열의 기존 내용을 새로운 값으로 간단하게 교체합니다. 코드를 호출하는 과정에서 배열을 초기에 할당해야 하지만(따라서 덜 깔끔하지만), 이 함수는 호출되더라도 새로운 가비지를 생성하지 않습니다.

Disabling garbage collection

If you are using the Mono or IL2CPP scripting backend, you can avoid CPU spikes during garbage collection by disabling garbage collection at run time. When you disable garbage collection, memory usage never decreases because the garbage collector does not collect objects that no longer have any references. In fact, memory usage can only ever increase when you disable garbage collection. To avoid increased memory usage over time, take care when managing memory. Ideally, allocate all memory before you disable the garbage collector and avoid additional allocations while it is disabled.

For more details on how to enable and disable garbage collection at run time, see the GarbageCollector Scripting API page.

You can also try an experimental, incremental garbage collection option.

Requesting a Collection

As mentioned above, it is best to avoid allocations as far as possible. However, given that they can’t be completely eliminated, there are two main strategies you can use to minimise their intrusion into gameplay.

Small heap with fast and frequent garbage collection

이 방법은 오래 플레이되는 게임에서 부드러운 프레임률을 유지하는 데 가장 적합합니다. 이러한 게임은 작은 블록을 자주 할당하게 되지만, 이들 블록은 짧은 기간 동안만 사용됩니다. 이 방법을 iOS에서 사용할 때 할당할 일반적인 힙 크기는 200KB이며, 가비지 콜렉션은 이 경우 iPhone 3G에서 대략 5ms 정도 걸리게 됩니다. 힙 크기가 1MB로 증가하면 가비지 컬렉션은 7ms 정도 걸리게 됩니다. 따라서 가비지 콜렉션을 일정 프레임 간격마다 주기적으로 요청하는 것이 좋습니다. 이렇게 하면 가비지 컬렉션이 실제로 필요한 만큼 이상으로 발생하게 되지만 더 빠르게 수행되어 게임플레이에 최소한의 지장을 주게 됩니다. 아래를 참조하십시오.

if (Time.frameCount % 30 == 0)
{
   System.GC.Collect();
}

그러나 이 기법은 주의해서 사용해야 하며 게임에서 실제로 가비지 컬렉션 시간을 감소시키는지 프로파일러 통계를 확인해야 합니다.

Large heap with slow but infrequent garbage collection

이 전략은 메모리 할당과 가비지 콜렉션이 비교적 자주 발생하지 않아 게임플레이 중간에 처리할 수 있는 게임에 적합합니다. 힙 용량은 최대한 큰 것이 좋습니다. 다만 너무 커서 운영체제가 시스템 메모리를 확보하려고 앱을 강제종료하는 경우는 피해야 합니다. Mono 런타임은 자동으로 힙을 확장하는 것을 최대한 피합니다. 따라서 수동으로 힙을 확장해야 하는데, 시작할 때 일부 플레이스홀더를 미리 할당하면 됩니다. 예를 들어, 메모리 관리자에서 메모리를 할당받기 위해 “무의미한” 오브젝트를 인스턴스 처리하면 됩니다.

//C# script example
using UnityEngine;
using System.Collections;

public class ExampleScript : MonoBehaviour {
    void 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 (int i = 0; i < 1024; i++)
            tmp[i] = new byte[1024];
        
        // release reference
        tmp = null;
    }
}

충분히 큰 힙을 확보하면 게임플레이 중 일시정지가 발생할 때까지 완전히 힙이 차버려서 가비지 컬렉션이 일어나는 일은 발생하지 않습니다. 일시정지가 발생하면 명시적으로 가비지 컬렉션을 요청할 수 있습니다.-

System.GC.Collect();

재차 강조하지만, 이 전략을 사용할 때에는 원하는 효과가 알아서 구현될 것이라 단정하지 말고 프로파일러 통계를 계속해서 참조하십시오.

Reusable Object Pools

새로 생성되고 제거되는 오브젝트의 수를 줄이는 것으로 간단하게 가비지 생성을 줄일 수 있습니다. 게임에서는 투사체와 같이 여러 번 반복되지만 한 번에는 몇 개만 사용되는 오브젝트 유형이 있습니다. 이런 경우 오브젝트를 새로 생성하여 기존 오브젝트를 대체하는 것보다 오브젝트를 재사용하는 편이 좋습니다.

Incremental Garbage Collection (Experimental)

Note: This is a preview feature and is subject to change. Any Projects that use this feature may need updating in a future release. Do not rely on this feature for full-scale production until it is officially released.

Incremental Garbage Collection spreads out the work performed to perform garbage collection over multiple frames.

With Incremental garbage collection, Unity still uses the Boehm–Demers–Weiser garbage collector, but runs it in an incremental mode. Instead of doing a full garbage collection each time it runs, Unity splits up the garbage collection workload over multiple frames. So instead of having a single, long interruption of your program’s execution to allow the garbage collector to do its work, you have multiple, much shorter interruptions. While this does not make garbage collection faster overall, it can significantly reduce the problem of garbage collection “spikes” breaking the smoothness of your game by distributing the workload over multiple frames.

The following screenshots from the Unity Profiler, without and with incremental garbage collection enabled, illustrate how incremental collection reduces frame rate hiccups. In these profile traces, the light blue parts of the frame show how much time is used by script operations, the yellow parts show the time remaining in the frame until Vsync (waiting for the next frame to begin), and the dark green parts show the time spent for garbage collection.

Nonincremental garbage collection profile
Nonincremental garbage collection profile

Without incremental GC (above), you can see a spike interrupting the otherwise smooth 60fps frame rate. This spike pushes the frame in which garbage collection occurs well over the 16 millisecond limit required to maintain 60FPS. (In fact, this example drops more than one frame because of garbage collection.)

Incremental garbage collection profile
Incremental garbage collection profile

With incremental garbage collection enabled (above), the same project keeps its consistent 60fps frame rate, as the garbage collection operation is broken up over several frames, using only a small time slice of each frame (the darker green fringe just above the yellow Vsync trace).

Incremental garbage collection using left over time in frame
Incremental garbage collection using left over time in frame

This screenshot shows the same project, also running with incremental garbage collection enabled, but this time with fewer scripting operations per frame. Again, the garbage collection operation is broken up over several frames. The difference is that this time, the garbage collection uses more time each frame, and requires fewer total frames to finish. This is because we adjust the time allotted to the garbage collection based on the remaining available frame time if Vsync or Application.targetFrameRate is being used. This way, we can run the garbage collection in time which would otherwise be spent waiting, and thus get garbage collection “for free”.

Enabling incremental garbage collection

Incremental garbage collection is currently supported on Mac, Windows and Linux Standalone Players and on iOS, Android and Windows UWP players. More supported platforms will be added in the future. Incremental garbage collection requires the new .NET 4.x Equivalent scripting runtime version.

On supported configurations, Unity provides Incremental garbage collection as an experimental option in the “Other settings” area of the Player settings window. Just enable the Use incremental GC (Experimental) checkbox.

Player Settings to enable incremental garbage collection
Player Settings to enable incremental garbage collection

In addition, if you set the VSync Count to anything other than Don’t Sync in your project Quality settings or with the Application.VSync property or you set the Application.targetFrameRate property, Unity automatically uses any idle time left at the end of a given frame for incremental garbage collection.

You can exercise more precise control over incremental garbage collection behavior using the Scripting.GarbageCollector class. For example, if you do not want to use VSync or a target frame rate, you could calculate the amount of time available before the end of a frame yourself and provide that time to the garbage collector to use.

Possible problems with incremental collection

In most cases, incremental garbage collection can mitigate the problem of garbage collection spikes. However, in some cases, incremental garbage collection may not prove beneficial in practice.

When incremental garbage collection breaks up its work, it breaks up the marking phase in which it scans all managed objects to determine which objects are still in use and which objects can be cleaned up. Dividing up the marking phase works well when most of the references between objects don’t change between slices of work. When an object reference does change, those objects must be scanned again in the next iteration. Thus, too many changes can overwhelm the incremental garbage collector and cause a situation where the marking pass never finishes because it always has more work to do – in this case, the garbage collection falls back to doing a full, non-incremental collection.

Also, when using incremental garbage collection, Unity needs to generate additional code (known as write barriers) to inform the garbage collection whenever a reference has changed (so the garbage collection will know if it needs to rescan an object). This adds some overhead when changing references which can have a measurable performance impact in some managed code.

Still, most typical Unity projects (if there is such a thing as a “typical” Unity project) can benefit from incremental garbage collection, especially if they suffer from garbage collection spikes.

Always use the Profiler to verify that your game or program performs as you expect.

Experimental status

Incremental garbage collection is included in Unity 2019.1 as an experimental preview feature. This has been done for a number of reasons: * It isn’t yet supported on all platforms. * As outlined in the “Possible problems with incremental collection” section above, we expect incremental garbage collection to be beneficial or at least not detrimental performance-wise for most Unity content, and this seems to have been true for various projects we have been testing with. But as Unity content is very diverse, we want to make sure that this assumption stays true across the greater Unity ecosystem, and we need your feedback on this. * The requirement for Unity code and scripting VM (mono, il2cpp) to add write barriers to inform the garbage collection whenever references in managed memory have changed introduces a potential source of bugs where we have missed adding such a write barrier, which could lead to objects being garbage collected when they are still needed. Now, we have done extensive testing (both manual and automated) and we aren’t aware of any such issues, and we believe that this feature is stable (otherwise, we would not ship it). But, once again, because of the diversity of Unity content and because such bugs might turn out to be hard to trigger in practice, we cannot completely rule out the possibility that there may be issues.

So, overall we believe that this feature works as expected and there are no known issues with it. But, because of the complexity of the Unity ecosystem, we need some time and exposure to get the confidence to drop the experimental label, which we will do based on the feedback we get.

Further Information

메모리 관리는 섬세하고 복잡한 과정으로, 상당한 노력과 학습이 수반됩니다. 자세한 내용을 알아보려면 memorymanagement.org를 참조하십시오. 다수의 문건과 온라인 문서를 제공합니다. 오브젝트 풀링에 대한 자세한 내용은 Wikipedia 페이지Sourcemaking.com를 참조하십시오.


  • 2019–01–17
  • Ability to disable garbage collection on Mono and IL2CPP scripting backends added in Unity 2018.3 NewIn20183
  • Added exerimental Incremental Garbage Collection feature added in Unity 2019.1 NewIn20191
Order of Execution for Event Functions
플랫폼별 컴파일