The code below contains no new and allocates ten million objects. It does
contain two (int) casts, and they are worth looking at closely, because they
are unboxing conversions rather than the thing doing the allocating — the boxing
itself is written nowhere.
long Sum(int[] values)
{
object total = 0;
for (int i = 0; i < values.Length; i++)
{
total = (int)total + values[i];
}
return (int)total;
}
for (int i = 0; i < 10_000_000; i++)
{
logger.LogDebug("processed {Index} of {Total}", i, total);
}
if (status.Equals(OrderStatus.Placed)) { }
Method Allocated
---------------- -----------
Sum, boxed 240,000,024 B ~10M boxes at 24 bytes each
Sum, int total 0 B
Two hundred and forty megabytes of generation 0 garbage to add up an array. The
collector handles it — that is what generation 0 is for — but the allocation
itself is not free, the collections are not free, and the cache behaviour is
ruinous because every logical integer is a pointer chase to a separate object.
The fixes are all the same shape: keep the value in a type the compiler knows
statically. Use long total rather than object. Where you are comparing or
sorting through an interface, put a generic constraint on it — where T : IComparable<T> — so the call resolves to the value type's own implementation
without boxing, which is the same fix in a different costume. Check
logger.IsEnabled before a hot debug call, or use a source-generated logging
method, which avoids both the boxing and the format parse. And use == on enums
rather than Equals.
The detail candidates miss is that boxing shows up in a memory profile and never
in a CPU profile of your own methods — the time is spread across the collector,
which is somebody else's stack frame.