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.