Optimizing TWeakObjectPtr usage

In this post I want to share some examples of inefficient usage of TWeakObjectPtr that I’ve come across in my time developing in Unreal Engine, and how these can be improved on.

These tips are pretty straightforward if you’re familiar with TWeakObjectPtr, but they’re useful to keep at the back of your mind any time you’re profiling or optimizing code that makes use of TWeakObjectPtr.

Why use TWeakObjectPtr

TWeakObjectPtr acts as a wrapper to a UObject. It provides a safe way to conditionally access an object that might have been destroyed. Unlike a UPROPERTY reference, it doesn’t prevent garbage collection.

I find it useful largely because it makes the non-owning relationship to the underlying object obvious, and provides clear semantics for getting at the object, which I’ve sometimes seen confusion about when referencing actors or components (will entries in this array get garbage collected? is a null check sufficient or do I need an IsValid check? etc).

It also provides a robust way to check for validity of an object which may have been destroyed, which is not safe using a raw pointer even with an IsValid check – the pointer may now be pointing at a different, yet valid, object! This may seem obvious when talking about cached pointers as class members, but can come up in unexpected places like lambda captures.

And of course a TWeakObjectPtr can be used anywhere, whereas UPROPERTY can only be used inside a USTRUCT or UCLASS.

Generally speaking I’ve noticed that systems often tend towards adopting TWeakObjectPtr over time as rare crashes crop up, and so I find I often just write code using TWeakObjectPtr to begin with.

Dereferencing a TWeakObjectPtr

We can dereference a TWeakObjectPtr using .Get(), which returns either a valid object or nullptr.

To understand how this works you have to remember that a TWeakObjectPtr simply stores the ObjectIndex and SerialNumber of the UObject it’s constructed from. The ObjectIndex is just an array index into the global GUObjectArray, the container of all UObjects, while the SerialNumber is a unique number assigned to each UObject. To get at the object, we look up the object in the GUObjectArray for our stored ObjectIndex (which can fail if our object was destroyed and the index is no longer valid), and then check that the SerialNumber of this object matches our stored SerialNumber (which can fail if our object was destroyed and a different object now exists in this slot).

// adapted from WeakObjectPtr.h

UObject* Internal_Get(bool bEvenIfPendingKill) const
{
    FUObjectItem* const ObjectItem = Internal_GetObjectItem();
    return ((ObjectItem != nullptr) && GUObjectArray.IsValid(ObjectItem, bEvenIfPendingKill)) ? (UObject*)ObjectItem->Object : nullptr;
}
    
FUObjectItem* Internal_GetObjectItem() const
{
    if (ObjectSerialNumber == 0)
    {
        return nullptr;
    }
    if (ObjectIndex < 0)
    {
        return nullptr;
    }
    FUObjectItem* const ObjectItem = GUObjectArray.IndexToObject(ObjectIndex);
    if (!ObjectItem)
    {
        return nullptr;
    }
    if (!SerialNumbersMatch(ObjectItem))
    {
        return nullptr;
    }
    return ObjectItem;
}

There’s obviously a performance cost to this, not just the logic that needs to check the object is valid, but the lookup in the GUObjectArray itself. We need to index into an arbitrary chunk of memory, which is likely going to be a cache miss.

Something to note is that .IsValid() has almost exactly the same implementation as .Get(), just returning true or false instead of the object or nullptr.

Tip 1: only check validity once

I sometimes come across code which seems to forget that TWeakObjectPtr checks validity when you use .Get(). The below example checks validity 3 times, which is wasteful:

if (ObjectWeakPtr.IsValid())
{
   UObject* Object = ObjectWeakPtr.Get();
    
    if (IsValid(Object))
    {
        ...

We can do this more efficiently and just as safely:

if (UObject* Object = ObjectWeakPtr.Get())
{
    ...

As a rule, you shouldn’t be using .IsValid() if you’re going to use .Get(). And remember that a simple null check of the pointer returned by .Get() is enough to know you have a valid object.

Tip 2: only dereference once

Similarly, I sometimes see code which ends up dereferencing the same TWeakObjectPtr multiple times in a row. This usually happens when the code is using operator-> as a syntactical shortcut.

The following code effectively dereferences ObjectWeakPtr 3 times.

if (ObjectWeakPtr.IsValid())
{
    ObjectWeakPtr->Foo();
    ObjectWeakPtr->Bar();
}

We can improve on this with the same pattern as the previous tip: using .Get() once upfront.

if (UObject* Object = ObjectWeakPtr.Get())
{
    Object->Foo();
    Object->Bar();
}

Generally I’d suggest avoiding using operator-> when working with TWeakObjectPtr, since I feel it obscures the underlying dereference that’s taking place.

Tip 3: avoid constructing a TWeakObjectPtr multiple times

TWeakObjectPtr can be implicitly constructed from an object in a few ways. Whilst this is usually a convenience, it’s not always transparent that this is what’s happening when reading through code, and so it can be easy to accidentally end up in a situation where you are repeatedly constructing a TWeakObjectPtr from the same object eg. in a loop body. There’s a cost to constructing TWeakObjectPtr, so we’ll want to avoid doing so unnecessarily.

In the example below, the implementation of operator== constructs a TWeakObjectPtr from OtherObject in each iteration.

UObject* OtherObject = ...

for (TWeakObjectPtr<UObject>& ObjectWeakPtr : ObjectWeakPtrArray)
{
    if (ObjectWeakPtr == OtherObject)
    {
        ...

We can improve this by explicitly constructing a TWeakObjectPtr outside the loop.

UObject* OtherObject = ...
TWeakObjectPtr<UObject> OtherObjectWeakPtr = OtherObject;

for (TWeakObjectPtr<UObject>& ObjectWeakPtr : ObjectWeakPtrArray)
{
    if (ObjectWeakPtr == OtherObjectWeakPtr)
    {
        ...

Tip 4: avoid operator== completely if you can

If you look at the implementation of operator== you’ll notice a quirk: it returns true if the stored object data matches OR both are invalid.

// adapted from WeakObjectPtr.h

bool operator==(const FWeakObjectPtr& Other) const
{
    return (ObjectIndex == Other.ObjectIndex && ObjectSerialNumber == Other.ObjectSerialNumber)
    || (!IsValid() && !Other.IsValid());
}

Whilst this might be desirable semantics in some cases, it comes at a clear performance cost, since every comparison between two TWeakObjectPtrs that reference different objects must also check .IsValid().

This cost adds up if you’re eg. searching an array of TWeakObjectPtrs for some object. Additionally, in my experience, when you’re doing a lookup like this you usually already know that your object is valid! This makes every one of the .IsValid() checks in operator== unnecessary. For example:

TWeakObjectPtr<UObject> OtherObjectWeakPtr = ... // known to be valid

for (TWeakObjectPtr<UObject>& ObjectWeakPtr : ObjectWeakPtrArray)
{
    if (ObjectWeakPtr == OtherObjectWeakPtr)
    {
        ...


Fortunately TWeakObjectPtr has a function .HasSameIndexAndSerialNumber() which lets us compare the ObjectIndex and SerialNumber of two TWeakObjectPtrs without the .IsValid() check. We can use this to write the previous example more efficiently:

TWeakObjectPtr<UObject> OtherObjectWeakPtr = ... // known to be valid

for (TWeakObjectPtr<UObject>& ObjectWeakPtr : ObjectWeakPtrArray)
{
    if (ObjectWeakPtr.HasSameIndexAndSerialNumber(OtherObjectWeakPtr))
    {
        ...

While the example above is quite explicit, there are more subtle cases where operator== is being used like TArray .Find() or .AddUnique().

This tip might be more situational in terms of where it can be useful, however I’ve used this approach in the past to significantly improve the performance of some hot loop code.

This entry was posted in Game Development and tagged , , , . Bookmark the permalink.