개요


언리얼에서 특정 메모리에 접근할 때, 혹시 몰라서 IsValid 와 IsValidLowLevel 둘 다 같이 작성했던 경험이 있다. 이번에 두 함수의 차이점에 대해 알아보려고 한다.

IsValid()


체크하는 것들:

  • ✅ PendingKill 상태
  • ✅ BeginDestroy 호출 여부
  • ✅ GC 마킹 상태
  • ✅ 실제 사용 가능한 상태인지

따라서

  • 접근하려는 메모리가 안전한지 확인하기 위해서는 IsValid 를 사용하자.


IsValid 의 내부를 보면 다음과 같다.

  • 해당 UObject 가 Null Ptr 인지 검사한다.
  • IsValid 는 Object.h 에 선언된 전역함수이다.
  • 해당 UObject가 현재 Garbage 플래그, 즉 GC 대상 오브젝트인지 검사한다.
    • IsPendingKillPending 함수도 CheckObjectValidBasedOnItsFlags 함수를 호출한다.
FORCEINLINE bool IsValid(const UObject *Test)
{
    return Test && FInternalUObjectBaseUtilityIsValidFlagsChecker::CheckObjectValidBasedOnItsFlags(Test);
}
FORCEINLINE static bool CheckObjectValidBasedOnItsFlags(const UObject* Test)
{
    // Here we don't really check if the flags match but if the end result is the same
    checkSlow(GUObjectArray.IndexToObject(Test->InternalIndex)->HasAnyFlags(EInternalObjectFlags::Garbage) == Test->HasAnyFlags(RF_MirroredGarbage));
    return !Test->HasAnyFlags(RF_MirroredGarbage);
}

IsValidLowLevel()


체크하는 것들:

  • ✅ nullptr인지
  • ✅ 메모리상 존재하는지
  • ✅ 전역 오브젝트 시스템에 등록되어 있는지

체크하지 않는 것들:

  • ❌ PendingKill 상태
  • ❌ BeginDestroy 호출 여부
  • ❌ GC 마킹 상태
  • ❌ 실제 사용 가능한 상태인지

따라서

  • 성능이 중요한 저수준 코드에서만 사용 하는 것이 좋다.


IsValidLowLevel 의 내부를 보면 다음과 같다.
bool UObjectBase::IsValidLowLevel() const
{
    return IsValidLowLevelForDestruction() && GUObjectArray.IsValid(this);
}
  • 전역 오브젝트 배열에서 이 오브젝트가 유효한 상태인지 확인
    • 이 때의 IsValid 는 FObjectArray의 멤버함수로 GC 대상인지 확인하지 않는다.
bool UObjectBase::IsValidLowLevelForDestruction() const
{
    PRAGMA_DISABLE_DEPRECATION_WARNINGS
    if (!IsThisNotNull(this, "UObjectBase::IsValidLowLevelForDestruction"))
    PRAGMA_ENABLE_DEPRECATION_WARNINGS
    {
       UE_LOG(LogUObjectBase, Warning, TEXT("NULL object"));
       return false;
    }
    if (!ClassPrivate.GetNoResolve())
    {
       UE_LOG(LogUObjectBase, Warning, TEXT("Object is not registered"));
       return false;
    }
    return true;
}
  • 해당 오브젝트가 Null 인지 확인
  • GetNoResolve 함수 안에서는 계속 파고파고 들어가다보면
    • 단순히 오브젝트 핸들이 문제가 없으면, 포인터를 반환한다.
    • 메모리 관련된 확인은 진행하지 않는다.
GetNoResolve() 
    ↓
NoAccessTrackingGetNoResolve()
    ↓  
NoResolveObjectHandleNoRead()
    ↓
ReadObjectHandlePointerNoCheck()


IsValid 와 IsValidLowLevel 비교


  • 결론 : IsValid 를 사용하는 것이 안전하다.
void ADelegateTestActor::Test()
{
    TargetObj = GetWorld()->SpawnActor<ATargetObject>();

    TargetObj->Destroy();
    UE_LOG(LogTemp, Warning, TEXT("TargetObj -> Destroy()"));
    UE_LOG(LogTemp, Warning, TEXT("TargetObj -> IsPendingKill : %d"), TargetObj->IsPendingKillPending());
    UE_LOG(LogTemp, Warning, TEXT("TargetObj -> IsValidLowLevel : %d"), TargetObj->IsValidLowLevel());
    UE_LOG(LogTemp, Warning, TEXT("TargetObj -> IsValid : %d"), IsValid(TargetObj));
  • IsValid 와 IsPendingKill 은 TargetObj 가 GC 대상이기 떄문에 0과 1을 출력한다.
  • 하지만 IsValidLowLevel 은 GC가 되기 전이기 때문에 1 을 출력한다.
결과: 
LogTemp: Warning: TargetObj -> Destroy()
LogTemp: Warning: TargetObj -> IsPendingKill : 1
LogTemp: Warning: TargetObj -> IsValidLowLevel : 1
LogTemp: Warning: TargetObj -> IsValid : 0
  • 언제 GC 될 지 모르기 때문에, IsValidLowLevel 은 1 을 출력할 가능성이 있다.