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.
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.
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
BTW I was honestly a bit surprised that EAllowShrinking::Yes is default in UE. It’s the opposite of std.