Nested TArrays in structs and memory

I have a question regarding memory usage of nested TArrays in structs, or just how memory works in general.

The problem i have is that a TArray of a struct that contains a TArray seems to be using much more memory than it should.

Here’s a picture to illustrate the problem:

Why is the third example using so much more memory than the other two? Is it just because the TArray has to be reallocated so many times?

A lot depends on the chosen allocator.
Adding an element to an array can cause memory allocation. Since allocation is a fairly slow process, the allocator usually allocates more memory than is needed at the moment to amortize this cost. Hence the difference between capacity and size. If you allocate and add 100 elements to an array, and then remove 25, the array will still take up memory for 100 elements.

You can check how it behaves:

TArray<int32> Arr;
int32 Capacity = Arr.Max();
for (int32 i = 0; i < 65536; ++i) {
  Arr.Add(i);
  if (Arr.Max() != Capacity) {
    Capacity = Arr.Max();
    UE_LOG(LogTemp, Warning, TEXT("Capacity changed %d"), Capacity);
  }
}

BTW instead of counting bytes in a loop you can use the TArray::GetAllocatedSize() function.

1 Like

As Emaer said, memory allocation is relatively slow on the heap, so we reserve multiple elements at a time. This is pretty standard with dynamically resizing arrays- ie in the standard library, gcc implements vectors with a 1.5x allocation (so 3->4, 6->9, 10->15, 1000->1500) iirc.
Bringing us back to your third array, the reason it’s so much bigger is because it allocates for 4 elements.

By default (ignoring some optimizations), whenever the default sized allocator runs out of space, it will allocate according to this calculation:

const SIZE_T FirstGrow = 4;
const SIZE_T ConstantGrow = 16;
	
SIZE_T Grow = FirstGrow; // this is the amount for the first alloc

if (!bArrayIsEmpty)
{
	Grow = SIZE_T(NumElements) + 3 * SIZE_T(NumElements) / 8 + ConstantGrow;
}

Since this only happens on a resize, you can avoid this by explicitly reserving exactly how much you need, which is a good practice I see you already know of given the Reserve.


I do want to point out that this is actually up to the individual developer in most cases, and the default behavior for most of the remove functions is to shrink the array so it doesn’t hold onto that memory. Sometimes this is a bool, but most of the time it’s an EAllowShrinking.

1 Like

Did not know it would allocate for 4 or more at a time by default - thanks for the detailed explanation.

1 Like

Of course. Generally if you add and remove elements from the array quite often, then setting shrinking=false would be a better solution. Otherwise, hmm, who cares :slight_smile:

BTW I was honestly a bit surprised that EAllowShrinking::Yes is default in UE. It’s the opposite of std.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.