ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] FUObjectArray (GUObjectArray)
    UE4 & UE5/Core 2021. 6. 20. 23:36

    [UE5] FUObjectArray(GUObjectArray)


    최초 작성 : 2021-06-20
    마지막 수정 : 2021-06-20
    최재호

    1. 환경

    Unreal Engine 5 Early access

    UE4.26.2 코드와 거의 유사하므로 UE4에 익숙하시면 보시는데 문제가 없을 것 같습니다.
    일부 코드는 줄 바꿈 되거나 접은 상태로 중요한 코드를 위주로 설명하니 보실 때 참고해주세요.
    또한 코드를 직접 붙여 넣어서 진행했기 때문에 작아서 잘 보이지 않는 코드는 클릭하여 봐주세요.

    2. 목표

    언리얼엔진의 UObject가 생성되면 어디에 저장될까요? 바로 GUObjectArray(Type : FUObjectArray)라는 전역 변수에 저장됩니다. UObject가 어떻게 관리되고 있는지 이해해봅시다.

    * 코드에는 설명에 대응하는 1, 2, 3 등의 번호가 매겨져 있으니 참고해주세요.
    * 개인적인 공부 용도로 분석한 내용이라 틀린 내용이 있을 수 있습니다. 그런 부분은 지적 부탁드립니다.

    3. 내용

    먼저 FUObjectArray로 가봅시다. FUObjectArray는 TUObjectArray(== FChunkedFixedUObjectArray)를 가지고 있네요. 그리고 실제로 모든 UObject들은 TUObjectArray 내부에 있다는 것을 알 수 있습니다. 그렇다면 FUObjectArray는 UObject들을 위한 추가 기능이 있을 것 같네요. 일단 UObject가 실제로 어디에 저장되는지 따라 들어가 봅시다.

    그림1.

    FChunkedFixedUObjectArray(== TUObjectArray)는 FUObjectItem이란 클래스들을 관리하는 것을 알 수 있습니다.

    먼저 FUObjectItem을 알아보는 것이 좋을 것 같습니다.
    FUObjectItem에는 아래 4가지 변수가 있습니다.
    1. class UObjectBase* Object : 생성된 UObject
    2. int32 Flags : Flags는 현재 UObject가 어떤 상태에 있는지를 기록해둡니다. AsyncLoading 되는 중인가? Async 로딩된 UObject인가? GC에서 RootSet인가 등의 정보를 담고 있습니다. UObject의 상태 파악에 필요한 Flags로 기억해두고 문제가 있을 때 한 번씩 조회해보면 좋을 것입니다.
    3. int32 ClusterRootIndex : GC를 위한 Flag입니다. 자신의 ClusteRoot의 FChunckedFixedUObjectArray에서 Index를 가리킵니다. 언리얼의 GC는 Mark and Sweep 방식으로, 모든 UObject들에 대해서 RootSet으로부터 Reachability를 체크하고 Reachable 하지 않는다면 소멸됩니다. UObject의 수가 많으면 당연히 시간이 더 길어지기 때문에 Reachability 체크를 할 대표 UObject를 ClusterRoot로 하여 그것만 Reachable 하면 나머지 Cluster 하위에 있는 UObject는 Reachable 하다고 가정하도록 해주는 것이 Cluster 기능입니다.
    4. int32 SerialNumber : 이 기능은 언리얼의 FWeakObjectPtr을 위한 것입니다. 언리얼의 UObject를 위해 특별한 WeakPtr 방식을 사용했는데요. 재미있는 방식입니다. 이글의 마지막에서 알아봅시다.

    그림2.
    그림3.

    다시 FChunckedFixedUObjectArray으로 돌아갑시다.
    1. 이 배열은 FUObjectItem을 순회할 때 캐시 효율을 올리기 위해서 64 * 1024 개 단위로 FUObjectItem 배열을 확장합니다.
    2. 만약에 배열을 확장해야 한다면 보는 것과 같이 새로운 64 * 1024 개의 FUObjectItem 배열을 만들어서 붙여줍니다.
    3. 매번 필요할 때마다 배열의 크기를 확장할 수도 있지만 더 캐시 효율을 향상하려면 애초에 큰 연속된 메모리 공간을 할당해도 되겠죠? 그래서 PreAllocate 함수에서 그것이 가능합니다. bPreAllocateChunks = true 이면, 연속된 배열 공간을 할당한 다음 그것을 FUObjectItem** Objects; 에 마치 Chunk 단위로 할당된 것처럼 만들어줍니다.
    4. 마지막으로 FChunkedFixedUObjectArray 내에서의 Index를 알고 있는 경우 [] 연산자를 사용하거나 GetObjectPtr를 사용하여 FUObjectItem을 얻어올 수 있습니다.

    그림4.


    이제 다시 우리의 목표 FUObjectArray 로 가봅시다. FUObjectArray는 아래와 같은 기능을 추가로 가지고 있습니다.
    [1]. UObject가 생성되거나 소멸되는 경우 메시지를 받을 수 있도록 Listener를 등록할 수 있습니다.
    [2]. GC를 하지 않는 인덱스 범위와 GC를 하는 인덱스 범위를 구분해줄 수 있습니다.
    [3]. FUObjectItem가 소멸이 일어나면서 FChunkedFixedUObjectArray내의 할당된 Chunk들 사이가 비어있는 경우 이 빈 곳을 찾아서 먼저 사용하도록 유도해줍니다. 이렇게 함으로써 불필요한 배열 확장을 막아주고, 캐시 효율도 증가시켜줍니다.
    [4]. MasterSerialNumber 를 사용하여 FWeakObjectPtr의 작동을 도와줍니다. MasterSerialNumber는 FUObjectItem의 SerialNumber값을 채워줍니다.

    이제 위의 내용들을 차례로 살펴봅시다.

    먼저 [1], [2], [3] 에 대한 내용을 확인해봅시다.
    엔진이 초기화되면 UObject가 여럿 생성될 것입니다. 그리고 엔진 초기화 중 생성된 UObject들은 엔진이 종료될 때까지 반드시 메모리에 올라가야 하는 것들이 존재할 것입니다. 이런 것들을 지워지지 말아야 한다는 것을 알고 있으므로 GC 되지 않다는 것이 확실하죠. 그래서 엔진이 초기화되고 일정 부분까지는 NonGC 영역으로 처리합니다.

    그림5.

    1. OpenForDisregardForGC 플래그 여부로 현재 NonGC UObject를 생성중인 것을 나타냅니다. 이 변수가 false로 변경되는 그때부터는 GC 처리할 UObject가 생성됩니다. OpenForDisregardForGC false로 변경되고 나면 ObjFirstGCIndex 가 확정되며 이 Index를 기반으로 GC가 처리됩니다.
    2. 이 부분은 NonGC UObject를 생성하는 부분입니다. 만약 FChunkedFixedUObjectArray에 추가 FUObjectItem을 생성해야 한다면 추가 생성하는 부분을 볼 수 있습니다.
    3. 만약 GC를 해야 되는 Object를 만들기 시작한다면 여기로 들어갈 것입니다. 생성되어있다가 소멸되어서 비어있는 Index들의 정보가 있습니다. 이 Index는 당연히 FChunkedFixedUObjectArray의 FUObjectItem의 요소일 것입니다.
    4. 3번 과정에서 비어있는 Index가 없다면 새로 FUObjectItem을 할당받습니다.
    5. 이번에 새로 생성한 FUObjectItem을 가져와서 기본 데이터를 초기화해주고, UObject의 InternalIndex를 FChunkedFixedUObjectArray에서의 인덱스로 설정해줍니다. 이제 우리는 UObject가 있으면 InternalIndex를 사용하여 언제든지 GUObjectArray에서 필요한 FUObjectItem을 얻어 올 수 있습니다.
    6. 등록되어있는 UObject Listener에 Notify를 보내줍니다.

    그림6. 


    [4]에 대한 것을 알아봅시다. 이 부분은 FWeakObjectPtr과 연관되어있습니다. 아래와 같이 FUObjectArray는 AllocateSerialNumber에 Index를 넘겨주면 해당 FUObjectItem의 SerialNumber를 리턴해줍니다. 이때 SerialNumber가 없다면 MasterSerialNumber에서 새로운 번호를 발급받아 설정하고 리턴합니다. 그래서 SerialNumber는 MasterSerialNumber가 오버플로가 일어난 경우가 아니면 웬만하면 유일하다는 것을 알 수 있습니다.

    그림7. 

    std의 WeakPtr의 경우 Reference Count를 가리키는 포인터 하나와 실제 값을 가리키는 포인터 하나를 갖게 됩니다. 하지만 언리얼 FWeakObjectPtr 의 구현은 조금 다릅니다.
    1. FWeakObjectPtr은 ObjectIndex 와 ObjectSerialNumber를 멤버로 가집니다.
    2. 만약 UObject로 FWeakObjectPtr를 만든다면 ObjectIndex와 ObjectSerialNumber를 설정해줍니다.

    그림8.

    위에서 FUObjectArray를 봤을 때 우리는 ObjectIndex가 있다면 해당 UObject를 얻어올 수 있다는 것을 알 수 있었습니다. 그러면 SerialNumber는 왜 필요할까요?
    아래는 그림9는 FUObjectArray에 있는 FUObjectItem들이라고 생각해봅시다. 초록색 영역이 UObject가 할당되어있다는 의미입니다.

    아래와 같은 상황이라고 합시다.
    1 -> 2 기존의 UObject가 소멸됨
    2 -> 3 새로운 UObject가 생성함

    만약 SerialNumber 없이 ObjectIndex만 있다면 UObject가 소멸되고 새로 생성되었을 때, 해당 UObject가 내가 FWeakObjectPtr에 저장했던 시점의 UObject인지 확인할 수 없을 것입니다. 그렇기 때문에 SerialNumber를 추가로 사용합니다. 오브젝트가 소멸되고 재할당되면 SerialNumber가 0으로 초기화되었다가 FWeakObjectPtr을 사용할 때 MasterSerialNumber로 새로 할당받게 됩니다. 그렇다면 1 과정에서 만들어두었던 FWeakObjectPtr의 ObjectSerialNumber는 새로 생성된(ObjectIndex는 같지만) UObject의 SerialNumber와 서로 다를 것입니다. 이때 우리는 FWeakObjectPtr의 Stale or Invalid 생태인 것을 확인할 수 있습니다.

    그림9.

    4. 레퍼런스

    1. https://github.com/EpicGames/UnrealEngine/tree/ue5-early-access

    'UE4 & UE5 > Core' 카테고리의 다른 글

    [UE5] Garbage Collection  (3) 2023.07.05

    댓글

Designed by Tistory & scahp.