【UE】Unreal Engine のガベージコレクションについて

Unreal Engine

はじめに

ガベージコレクションとは

突然ですが、ガベージコレクションとは何でしょうか?
我らの Wikipedia には以下のように説明されています。

ガベージコレクション[注釈 1](英: garbage collection、GC)とは、コンピュータプログラムが動的に確保したメモリ領域のうち、不要になった領域を自動的に解放する機能である。

ガベージコレクション – Wikipedia

少し分かりにくいので、なんJ民を用いて説明します。
(「なんJ」と「なんJ民」についてはピクシブ百科事典をご覧ください)

2004年に「なんでも実況J(ジュピター)」として設立。姉妹板に「なんU(なんでも実況ウラヌス)」がある。
略称は「なんでも」と「J」からとられている(なんでも実況の略ではない)。板の住民は「なんJ民」、または単にナンジェイとも呼ばれる。

なんJ (なんじぇい)とは【ピクシブ百科事典】

Unreal Engine で作ったゲームを実行すると、NewObject() や UWorld::SpawnActor() などで生成した UObject はそのデバイスのメモリに格納されます。
これを「メモリ領域の確保」といいます。

しかし、生成はされたものの初めから一度も使用されなかったり、途中から使用されなくなった UObject がメモリに残っていることが多々あります。
(ここでいう「使用されなくなった UObject」とは参照が無くなり、孤立した UObject のことです)

その UObject の分だけ使えるメモリ領域が減ってしまっているので、その UObject は邪魔になってしまっています。
このような状態を「メモリリーク」といいます。

メモリリーク(英: memory leak)とは、コンピュータで実行中のプログラムが確保したメモリ領域のうち、不要になったものを解放するのを忘れたまま放置してしまい、利用可能な空き領域が失われる現象[1]

メモリリーク – Wikipedia

この状態はあまりよろしくないので、使用されていない UObject を自動で破棄してくれる機能が Unreal Engine には備わっていて、それをガベージコレクションといいます。

また、ガベージコレクションを行うものをガベージコレクタといいます。
(なんJ民のイラストの例だとマッマのことですね)

そして、Unreal Engine のガベージコレクションは基本的にエンジンが適切なタイミングで自動的に実行するのですが、UEngine::ForceGarbageCollection() を呼び出したり、コマンドを実行したりすることによって任意のタイミングで実行させることもできます!

意図的に UObject を破棄するには

ガベージコレクションを使用することなく、delete などによってプログラマーが意図的に UObject を即座に破棄することは基本的にはできません。

プログラマーが意図的に UObject を破棄するには UObjectBaseUtility::MarkPendingKill() を呼び出して、UObject をガベージコレクションの回収対象としてマークする必要があります。

しかし、UObjectBaseUtility::MarkPendingKill() を直接呼び出すことはほとんど無く、AActor クラスなら AActor::Destroy()、UActorComponent クラスなら UActorComponent::DestroyComponent() といった関数を呼び出して間接的にガベージコレクションの回収対象にしています。

UPROPERTY() の役割

Unreal C++ でメンバ変数を宣言するときに UPROPERTY() を付けることがありますが、ガベージコレクションの観点から見るとどのような意味があるのでしょうか?

UPROPERTY()
TObjectPtr<AActor> ActorPtr;

これには主に2つ意味があります。

ダングリングポインタを防ぐ

まず1つ目はダングリングポインタを防いでくれるということです。

語弊を恐れずにいうと、ポインタとはそのオブジェクトが格納されているメモリの住所のようなものですが、そのオブジェクトが無くなったにも関わらず、その住所にそのオブジェクトが住み続けていると勘違いしている状態のことをダングリングポインタといいます。

プログラムの実行中にポインタが不正なメモリ領域を指している状態。ポインタが宙ぶらりんな状態(dangling)であることから、ダングリング・ポインタと呼ばれる。

[HotFix Report] セキュリティ用語-ダングリング・ポインタ(dangling pointer)

具体的には UObject* 型の変数を宣言するときに UPROPERTY() を付けていないと、UObject がガベージコレクションによってメモリから解放されてもその変数は nullptr にならずに別のオブジェクトのメモリ領域を指し続けることになります。

このままではそのポインタ変数を通して無効なメモリ領域にアクセスされる可能性がありますが、これをダングリングポインタといいます。

Unreal C++ では UObjectBase::IsValidLowLevel() を使用することで、そのポインタがダングリングポインタの可能性があるかどうかを判断することができますが、そのポインタが指しているメモリ領域に別の UObject が格納されるなどした場合は正確に判断できないので、UObjectBase::IsValidLowLevel() は確実にダングリングポインタかどうかを判断できるわけではありません。

そこで、UObject* 型の変数を宣言するときに UPROPERTY() を付けると、その UObject がガベージコレクションによって破棄されたときにその変数に自動で nullptr を代入してくれるようになります!

つまり、そのポインタ変数が指していた無効なメモリ領域にアクセスされることを事前に防ぐことができるのです!
これがダングリングポインタの防止というやつです。

参照関係をガベージコレクタに伝える

UPROPERTY() には他にも役割があります。
それは参照関係をガベージコレクタに伝えるということです。

例えば、UObject* 型の変数を宣言するときに UPROPERTY() を付けていないと、その UObject がどこからも参照されていないことになり、ガベージコレクトのタイミングでその UObject も破棄されてしまいます。

このとき、UObject は PendingKill という状態を経由することなくメモリから解放されます。
PendingKill の状態とはメモリ上には存在しているけど、ガベージコレクションの回収対象としてマークされている状態のことをいいます。

しかし、UObject* 型の変数を宣言するときに UPROPERTY() を付けると、その UObject は参照されているという情報をガベージコレクタに伝えることができるので、ガベージコレクタによる意図しないオブジェクトの破棄を事前に防ぐことができます。

そして、その変数が宣言されていたクラスが破棄されるなどして、その UObject がどこからも参照されなくなると、その UObject はガベージコレクションによって破棄されます。

IsValid()

UObject に安全にアクセスするためには、その UObject のポインタが nullptr ではないことと PendingKill の状態ではないことの2つを同時に確認する必要があります。

このときに使用するのが IsValid() です。
IsValid() ではそれら2つのことを一度に確認できるので、基本的に UObject の有効性の確認には IsValid() を用いるといいでしょう。

void ASampleActor::Hoge(UObject* Object)
{
    if(IsValid(Object))
    {
        // Object が有効だったときの処理
    }
}

UPROPERTY() の活用例

では具体的に UPROPERTY() の活用例を見てみましょう。

ダングリングポインタを防ぐ

まずは先ほど説明した、ダングリングポインタを防いでくれるということから見ていきます。

このコードでは AActor* 型の ActorPtr 変数が ASampleActor クラスのメンバ変数として宣言されていますが、UPROPERTY() が付いていません。

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    TObjectPtr<AActor> ActorPtr;// UPROPERTY() が無い
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

このクラスでは BeginPlay() で AActor を生成し、その生成したオブジェクトのポインタを ActorPtr 変数に代入後、すぐにそのオブジェクトを Destroy() して PendingKill の状態にしています。
その後は ActorPtr 変数の状態を Tick() で毎フレーム確認しています。

ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ASampleActor::BeginPlay()
{
    Super::BeginPlay();

    ActorPtr = GetWorld()->SpawnActor<AActor>();

    ActorPtr->Destroy();
}

void ASampleActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    if (IsValid(ActorPtr))
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid"));
    }

    if (ActorPtr == nullptr)
    {
        UKismetSystemLibrary::PrintString(this, TEXT("nullptr"));

        return;
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not nullptr"));
    }

    if (ActorPtr->IsPendingKillPending())
    {
        UKismetSystemLibrary::PrintString(this, TEXT("pending kill"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not pending kill"));
    }

    if (ActorPtr->IsValidLowLevel())
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid low level"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid low level"));
    }
}

ゲームを実行してみるとガベージコレクト前は「not valid」「not nullptr」「pending kill」「valid low level」と表示されます。
メモリには存在しているけど、ガベージコレクションの対象としてマークされているということですね。

ガベージコレクト前(UPROPERTY 無し)

ガベージコレクト後は「not valid」「not nullptr」「pending kill」は同じですが、その次は「not valid low level」と表示されています。

ガベージコレクト後(UPROPERTY 無し)

つまり、ActorPtr 変数が指していたオブジェクトは破棄されているにも関わらず、ActorPtr 変数には nullptr が代入されていないので、ActorPtr 変数は本来のオブジェクトを指すポインタではなくなり、別のオブジェクトのメモリ領域を指す無効なポインタになってしまっています。
これがダングリングポインタというやつですね。

そこで、このコードのように ActorPtr 変数を宣言するときに UPROPERTY() を付けてみます。

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    UPROPERTY() // UPROPERTY() を追加
    TObjectPtr<AActor> ActorPtr;
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

ガベージコレクト前は先ほどと同様に「not valid」「not nullptr」「pending kill」「valid low level」と表示されますが、

ガベージコレクト前(UPROPERTY あり)

ガベージコレクトのタイミングで ActorPtr 変数に nullptr が自動的に代入されてくれるので、ガベージコレクト後は「not valid」「nullptr」と表示されます。

ガベージコレクト後(UPROPERTY あり)

つまり、ActorPtr 変数が別のオブジェクトのメモリ領域を指すことが無くなり、ダングリングポインタを事前に防いでくれるようになっています。

ガベージコレクションによって破棄されないようにする

次は UPROPERTY() のもう1つの役割である、参照関係をガベージコレクタに伝えてオブジェクトが勝手に破棄されないようにするというのを見てみます。

このコードでは UObject* 型の ObjectPtr 変数が ASampleActor クラスのメンバ変数として宣言されていますが、UPROPERTY() が付いていません。

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    TObjectPtr<UObject> ObjectPtr;// UPROPERTY() が無い
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

このクラスでは BeginPlay() で UObject を生成し、その生成したオブジェクトのポインタを ObjectPtr 変数に代入した後、ObjectPtr 変数の状態を Tick() で毎フレーム確認しています。

ASampleActor::ASampleActor()
{
    PrimaryActorTick.bCanEverTick = true;
}

void ASampleActor::BeginPlay()
{
    Super::BeginPlay();

    ObjectPtr = NewObject<UObject>();
}

void ASampleActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
	
    if (IsValid(ObjectPtr))
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid"));
    }

    if (ObjectPtr == nullptr)
    {
        UKismetSystemLibrary::PrintString(this, TEXT("nullptr"));

        return;
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not nullptr"));
    }

    if (ObjectPtr->IsValidLowLevel())
    {
        UKismetSystemLibrary::PrintString(this, TEXT("valid low level"));
    }
    else
    {
        UKismetSystemLibrary::PrintString(this, TEXT("not valid low level"));
    }
}

このコードでゲームを実行すると、ガベージコレクト前は「valid」「not nullptr」「valid low level」と表示されて、オブジェクトが有効な状態であることがわかります。

ガベージコレクト前(UPROPERTY 無し)

しかし、ガベージコレクト後は「valid」「not nullptr」「not valid low level」と表示されます。

ガベージコレクト後(UPROPERTY 無し)

つまり、ObjectPtr 変数の指しているオブジェクトは誰かから参照されているという情報がガベージコレクタに伝わらず、ObjectPtr 変数の指しているオブジェクトがガベージコレクトのタイミングで破棄されてしまっています。

そして、ObjectPtr 変数の指していたオブジェクトは破棄されているにも関わらず、ObjectPtr 変数には nullptr が代入されていないので、ObjectPtr 変数は本来のオブジェクトを指すポインタではなくなり、別のオブジェクトのメモリ領域を指す無効なポインタになってしまっています。
ダングリングポインタというやつですね。

そこで、このコードのように ObjectPtr 変数を宣言するときに UPROPERTY() を付けてみます。

UCLASS()
class SAMPLE_API ASampleActor : public AActor
{
    GENERATED_BODY()

private:
    UPROPERTY() // UPROPERTY() を追加
    TObjectPtr<UObject> ObjectPtr;
	
public:
    ASampleActor();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

ゲームを実行してみると「valid」「not nullptr」「valid low level」と表示されます。

ガベージコレクト前後(UPROPERTY あり)

つまり、ObjectPtr 変数の指しているオブジェクトは誰かから参照されているという情報がガベージコレクタに伝わり、ObjectPtr 変数の指しているオブジェクトがガベージコレクタによって破棄されないようになりました!

最後に

参考記事

お問い合わせ

    タイトルとURLをコピーしました