TActorIterator 를 사용하다 보니 많이 무겁지 않을까 싶어서 어떻게 돌아가나 한 번 찍먹해보았음.

아마 추가적인 구현이 더 있겠지만, 찍먹 수준에서 개념만 살펴보았으니 설명하지 않은 부분이 있을 것임.

TActorIterator 의 구조는 TActorIteratorBase 를 상속받아 구현되어있음.

TActorIterator

/**
 * Actor iterator
 * Note that when Playing In Editor, this will find actors only in CurrentWorld
 */
class FActorIterator : public TActorIteratorBase<FActorIterator>
{
    friend class TActorIteratorBase<FActorIterator>;
    typedef TActorIteratorBase<FActorIterator> Super;
 
public:
    ....
}

TActorIteratorBase 에서 State 라는 것을 볼 수 있는데 TOptional<FActorIteratorState> 로 선언된 멤버 변수임.

TActorIteratorBase

/**
 * Template class used to filter actors by certain characteristics
 */
template <typename Derived>
class TActorIteratorBase
{
public:
    ...
 
private:
    TOptional<FActorIteratorState> State;
 
    ...
}

TActorIteratorState에는 ObjectArray 라는 TArray<UObject*> 배열이 존재.

이 배열은 생성자 호출시 GetObjectOfClass 호출되며 초기화됨.

FActorIteratorState

/**
 * Abstract base class for actor iteration. Implements all operators and relies on IsActorSuitable
 * to be overridden by derived class.
 * Note that when Playing In Editor, this will find actors only in CurrentWorld.
 */
class FActorIteratorState
{
public:
    /** Current world we are iterating upon                     */
    UWorld* CurrentWorld;
    /** Results from the GetObjectsOfClass query                */
    TArray<UObject*> ObjectArray;
    /** index of the current element in the object array        */
    int32 Index;
    /** Whether we already reached the end                      */
    bool    ReachedEnd;
    /** Number of actors that have been considered thus far     */
    int32       ConsideredCount;
    /** Current actor pointed to by actor iterator              */
    AActor* CurrentActor;
    /** Contains any actors spawned during iteration            */
    TArray<AActor*> SpawnedActorArray;
    /** The class type we are iterating, kept for filtering     */
    UClass* DesiredClass;
    /** Handle to the registered OnActorSpawned delegate        */
    FDelegateHandle ActorSpawnedDelegateHandle;
 
    /**
     * Default ctor, inits everything
     */
    FActorIteratorState( UWorld* InWorld, TSubclassOf<AActor> InClass ) :
        CurrentWorld( InWorld ),
        Index( -1 ),
        ReachedEnd( false ),
        ConsideredCount( 0 ),
        CurrentActor( NULL ),
        DesiredClass(InClass)
    {
        check(IsInGameThread());
        check(CurrentWorld);
        EObjectFlags ExcludeFlags = RF_ClassDefaultObject;
        GetObjectsOfClass(InClass, ObjectArray, true, ExcludeFlags, EInternalObjectFlags::PendingKill);
 
        auto ActorSpawnedDelegate = FOnActorSpawned::FDelegate::CreateRaw(this, &FActorIteratorState::OnActorSpawned);
        ActorSpawnedDelegateHandle = CurrentWorld->AddOnActorSpawnedHandler(ActorSpawnedDelegate);
    }
 
    ~FActorIteratorState()
    {
        CurrentWorld->RemoveOnActorSpawnedHandler(ActorSpawnedDelegateHandle);
    }
 
    ...
}

GetObjectOfClass 함수는 GetObjectsOfClassThreadSafe 함수를 호출하며 값을 채움.

GetObjectOfClass

void GetObjectsOfClass(UClass* ClassToLookFor, TArray<UObject *>& Results, bool bIncludeDerivedClasses, EObjectFlags ExclusionFlags, EInternalObjectFlags ExclusionInternalFlags)
{
    SCOPE_CYCLE_COUNTER( STAT_Hash_GetObjectsOfClass );
     
    // We don't want to return any objects that are currently being background loaded unless we're using the object iterator during async loading.
    ExclusionInternalFlags |= EInternalObjectFlags::Unreachable;
    if (!IsInAsyncLoadingThread())
    {
        ExclusionInternalFlags |= EInternalObjectFlags::AsyncLoading;
    }
 
    TSet<UClass*> ClassesToSearch;
    ClassesToSearch.Add( ClassToLookFor );
    if( bIncludeDerivedClasses )
    {
        auto& ThreadHash = FUObjectHashTables::Get();
        FHashTableLock HashLock( ThreadHash );
        RecursivelyPopulateDerivedClasses( ThreadHash, ClassToLookFor, ClassesToSearch );
    }
 
    GetObjectsOfClassThreadSafe(FUObjectHashTables::Get(), ClassesToSearch, Results, ExclusionFlags, ExclusionInternalFlags);
 
    check( Results.Num() <= GUObjectArray.GetObjectArrayNum() ); // otherwise we have a cycle in the outer chain, which should not be possible
}

그런데 여기서 다른 스레드에 간섭을 받지 않도록 FHashTableLock 로 FUObjectHasTables 에 대해 락을 걸어둠

FUObjectHashTable에서 ClassToObjectListMap 이 존재함

이건 UClass를 Key로 TSet<UObjectBase> 를 Value 로 하는 맵데이터를 UObjectHash 로 만들어둔 것임.

클래스와 관련된 객체들만 조회하기 때문에 빠름.

GetObjectOfClassThreadSafe

static void GetObjectsOfClassThreadSafe(FUObjectHashTables& ThreadHash, TSet<UClass*>& ClassesToSearch, TArray<UObject *>& Results, EObjectFlags ExclusionFlags, EInternalObjectFlags ExclusionInternalFlags)
{
    ExclusionInternalFlags |= EInternalObjectFlags::Unreachable;
    FHashTableLock HashLock(ThreadHash);
     
    for (auto ClassIt = ClassesToSearch.CreateConstIterator(); ClassIt; ++ClassIt)
    {
        TSet<UObjectBase*> const* List = ThreadHash.ClassToObjectListMap.Find(*ClassIt);
        if (List)
        {
            for (auto ObjectIt = List->CreateConstIterator(); ObjectIt; ++ObjectIt)
            {
                UObject *Object = static_cast<UObject *>(*ObjectIt);
                if (!Object->HasAnyFlags(ExclusionFlags) && !Object->HasAnyInternalFlags(ExclusionInternalFlags))
                {
                    Results.Add(Object);
                }
            }
        }
    }

TSet도 HashTable 로 작동하기 때문에, 특정 UClass 에 대한 UObject 를 가져오는 것도 빠를 것임.

FUObjectHashTable의 ClassToObjectListMap은 Map 구조라서 Linear Search 는. 그런데 이것도 UClass 에 UObjectBase 대응이기때문에 클래스별로 각각 존재.

GetObjectOfClass 에서 Results 로 받아온 값들은 담긴 순서대로 검색된 클래스의 액터들이겠지만, 이것은 포인터일뿐임.

Pointer Dereference 를 하면 결국 캐시 미스. 어쩔수가 없음.

포인터 변수 자체의 주소값만 TArray 가 제공하는 배열속에 Linear 하게 저장되어 있다고 할 수 있음.

그래서 전체 월드의 엑터를 TActorIterator 로 조회한다면, TSet(HashTable)으로 관리되는 액터들을 조회하는 것은 빠를 것다고 볼 수 있을 것 같음.

결론적으로 TActorIterator는 전체 월드를 순회하는 것이 아닌, ClassToObjectListMap 으로 관리되는 <UClass, TSet<UObjectBase>> 에서 전달된 템플릿 인자로 맵을 조회해 관련 액터가 들어있는 TSet <>( HashTable) 을 조회하는 것이기 때문에 빠름.

전체 월드에 각각 조회한다는 것은 내 착각이었던게 맞는듯.

코드에 기반한 뇌피셜임.