KoreanFoodie's Study

언리얼 스마트 포인터(Unreal Smart Pointer) 정리 2 : 구현 세부사항과 팁 본문

Game Dev/Unreal C++ : Study

언리얼 스마트 포인터(Unreal Smart Pointer) 정리 2 : 구현 세부사항과 팁

GoldGiver 2023. 3. 22. 11:44

언리얼 스마트 포인터 구현 세부사항과 팁

핵심 :

1. 스마트 포인터를 사용할지 고려할 때는 항상 퍼포먼스에 대해 생각해야 한다. 스마트 포인터는 자원 관리에는 적합하지만 일부 스마트 포인터 타입은 C++ 기본 포인터보다 더 느리기 때문이다.
2. TSharedPtr 는 비침범형(non-intrusive) 로, 오브젝트가 스마트 포인터의 소유 하에 있는지 알 수 없다. 오브젝트를 TSharedRef 또는 TSharedPtr 로 접근하면, 오브젝트 클래스를 TSharedFromThis 에서 파생시켜야 한다.
3. 스마트 포인터는 일반적으로 싱글 스레드에서 안전하게 사용할 수 있다. 멀티 스레드에서 사용해야 한다면, 스레드 세이프 버전을 사용하자.

스마트 포인터 구현 세부사항

언리얼 스마트 포인터 라이브러리의 모든 스마트 포인터는 기능성 및 효율성 측면에서 일반적인 특징을 공유한다.

 

속도

스마트 포인터를 사용할지 고려할 때는 항상 퍼포먼스에 대해서 생각해야 한다. 스마트 포인터는 특정 하이 레벨 시스템이나 자원 관리 또는 툴 프로그램에 매우 적합하지만 일부 스마트 포인터 타입은 C++ 기본 포인터보다 더 느리며, 이런 오버헤드로 인해 렌더링과 같은 로우 레벨 엔진 코드에는 덜 유용하다.

스마트 포인터의 일반적인 퍼포먼스 이점:

  • 모든 연산이 고정비(constant-time)
  • 빌드를 출시할 때, 대부분의 스마트 포인터들을 참조 해제하는 속도가 C++ 기본 포인터만큼 빠름
  • 스마트 포인터들을 복사해도 절대 메모리가 할당되지 않음
  • 스레드 세이프(Thread-safe) 스마트 포인터는 잠금 없는(lockless) 구조

스마트 포인터의 퍼포먼스 문제점에는 다음이 포함되어 있음 :

  • 스마트 포인터의 생성 및 복사는 C++ 기본 포인터의 생성 및 복사보다 더 많은 오버헤드가 발생
  • 참조 카운트를 유지하면 기본 연산에 주기가 추가
  • 일부 스마트 포인터는 C++ 기본 포인터보다 메모리 사용량이 더 높음
  • 레퍼런스 컨트롤러에는 두 번의 힙 할당량이 있다. MakeShareable 대신에 MakeShared를 사용하면 두 번째 할당을 피할 수 있으며, 퍼포먼스를 개선할 수 있다. 자세한 내용은 이 글 참고.

 

침범형 접근자(Intrusive Accessors)

쉐어드 포인터는 비침범형(non-intrusive)으로, 오브젝트가 스마트 포인터의 소유 하에 있는지 알 수 없다. 이런 속성은 일반적으로 문제가 없지만, 오브젝트를 쉐어드 레퍼런스 또는 쉐어드 포인터로서 접근하려는 경우가 있을 수도 있다. 이러한 경우에는, 오브젝트의 클래스를 템블릿 매개변수로 사용하여 TSharedFromThis 에서 오브젝트의 클래스를 파생시켜야 한다. TSharedFromThis 는 두 가지 함수 AsShared 및 SharedThis 를 제공하며, 두 함수로 오브젝트를 쉐어드 레퍼런스로 변환하고, 쉐어드 레퍼런스를 또 쉐어드 포인터로 변환할 수 있다. 이는 항상 쉐어드 레퍼런스를 반환하는 클래스 팩토리나 쉐어드 레퍼런스 또는 쉐어드 포인터를 요구하는 시스템에 오브젝트를 넣을 때 특히나 유용하다. AsShared 는 호출되는 오브젝트의 페어런트 타입일 수 있는 TSharedFromThis 에 템플릿 매개변수로서 전달된 본래 타입의 클래스를 반환하는 동시에 ‘SharedThis'는 ‘this'에서 타입을 직접 파생키시고 해당 타입의 오브젝트를 참조하는 스마트 포인터를 반환한다. 다음은 두 함수의 사용 방법을 보여주는 예제 코드이다 :

class FRegistryObject;
class FMyBaseClass: public TSharedFromThis<FMyBaseClass>
{
    virtual void RegisterAsBaseClass(FRegistryObject* RegistryObject)
    {
        // 'this'의 쉐어드 레퍼런스에 접근
        // <TSharedFromThis>로부터 직접 상속되어 AsShared()와 SharedThis(this)는 동일한 타입을 반환
        TSharedRef<FMyBaseClass> ThisAsSharedRef = AsShared();
        // RegistryObject는 TSharedRef<FMyBaseClass> 또는 TSharedPtr<FMyBaseClass>를 요구한다. TsharedRef는 묵시적으로 TsharedPtr로 변환될 수 있다.
        RegistryObject->Register(ThisAsSharedRef);
    }
};
class FMyDerivedClass : public FMyBaseClass
{
    virtual void Register(FRegistryObject* RegistryObject) override
    {
        // TSharedFromThis<>로부터 직접 상속되지 않아서 AsShared()와 SharedThis(this)는 각기 다른 타입을 반환한다.
        // AsShared()는 해당 예제 내 TSharedFromThis<> - TSharedRef<FMyBaseClass>에서 정의된 본래 타입을 반환한다.
        // SharedThis(this)는 해당 예제 내 'this' - TSharedRef<FMyDerivedClass>의 타입과 함께 TsharedRef를 반환한다.
        // SharedThis() 함수는 ‘this' 포인터와 동일한 범위 내에서만 가능하다.
        TSharedRef<FMyDerivedClass> AsSharedRef = SharedThis(this);
        // FmyDerivedClass는 FmyBaseClass 타입의 일종이기 때문에 RegistryObject가 TSharedRef<FMyDerivedClass>를 허용한다.
        RegistryObject->Register(ThisAsSharedRef);
    }
};
class FRegistryObject
{
    // 이 함수는 FmyBaseClass나 그 자녀 클래스에 TsharedRef나 TsharedPtr를 허용한다
    void Register(TSharedRef<FMyBaseClass>);
};

AsShared 나 SharedThis 를 생성자로 호출하지 말자. 이때 쉐어드 레퍼런스가 선언되지 않은 상태이기 때문에 충돌이나 어서트가 발생하게 된다!

 

형변환

쉐어드 포인터는 (쉐어드 레퍼런스도) 언리얼 스마트 포인터 라이브러리에 포함되어 있는 여러 가지 지원 함수를 통해 형변환할 수 있다. 올림변환(Up-casting)는 C++ 포인터와 마찬가지로 묵시적이다. ConstCastSharedPtr 함수로 const cast 연산자를 사용할 수 있으며, StaticCastSharedPtr 로 static cast (주로 파생된 클래스 포인터로 내림변환(downcast)하기 위해) 연산자를 사용할 수 있다. 런타임 타입 정보(RTTI, run-type type information)가 없기 때문에 동적 형변환은 지원되지 않는다. 그 대신에 다음 코드와 같이 정적 형변환을 사용할 것을 권장한다 :

// FdragDropOperation가 FAssetDragDropOp이라는 점을 다른 수단을 통해 유효성이 확인되었다고 가정하고 있다.
TSharedPtr<FDragDropOperation> Operation = DragDropEvent.GetOperation();
// 이제 StaticCastSharedPtr로 형변환할 수 있다.
TSharedPtr<FAssetDragDropOp> DragDropOp = StaticCastSharedPtr<FAssetDragDropOp>(Operation);

 

스레드 안정성

기본적으로 스마트 포인터는 싱글 스레드가 접근하는 것이 안전하다. 멀티 스레드가 접근해야 한다면 스마트 포인터 클래스의 스레드 세이프 버전을 사용하자 :

  • TSharedPtr<T, ESPMode::ThreadSafe>
  • TSharedRef<T, ESPMode::ThreadSafe>
  • TWeakPtr<T, ESPMode::ThreadSafe>
  • TSharedFromThis<T, ESPMode::ThreadSafe>

이러한 스레드 세이프 버전은 원자적(atomic) 참조 카운팅으로 인해 디폴트보다 다소 느리지만 그 비헤이비어는 일반 C++ 포인터와 같다:

  • 읽기와 복사본은 항상 스레드 세이프함
  • 안전성을 위해 쓰기와 초기화는 반드시 동기화되어야 함
하나 이상의 스레드가 포인터에 접근하지 않는다는 것이 확실하다면, 스레드 세이프 버전을 사용하지 않음으로써 퍼포먼스를 향상시킬 수 있다!

 

 

팁 및 제한사항

  • 가급적이면 함수에 데이터를 TSharedRef 또는 TSharedPtr 매개변수로 넣지 않는 것을 권장한다. 이러한 데이터의 해제와 참조 카운팅으로 인해 오버헤드가 발생한다. 그 대안으로, 레퍼런스된 오브젝트를 ‘const &'로 넣자.
  • 쉐어드 포인터를 불완전한 타입/형식으로 미리 선언할 수 있다.
  • 쉐어드 포인터는 언리얼 오브젝트(UObject 와 이로부터 파생된 클래스)와 호환되지 않는다. 언리얼 엔진은 ‘UObject' 관리를 위한 별도의 메모리 관리 시스템이 있으며 (언리얼 오브젝트 처리 문서를 참고) 두 시스템은 완전히 다른 시스템이다!

 

참고 : 언리얼 공식 문서
Comments