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) 을 조회하는 것이기 때문에 빠름.
전체 월드에 각각 조회한다는 것은 내 착각이었던게 맞는듯.
코드에 기반한 뇌피셜임.
혹시 UE5 관련해서 본문이랑은 관계 없는 질문을 해도 될까요?
Cpp 을 모르고 블루프린트로만 취미로 하는 사람이라서요
bbagwang@gmail.com 으로 메일 보내주세요~