Allocation in a loop, and the profiler that shows it

A CPU profile shows where time is spent and not why, and a loop constructing an object per iteration frequently spends its time in the allocator rather than in anything named.

// 400,000 DateTimeImmutable objects, one per row
foreach ($rows as $row) {
    $d = new DateTimeImmutable($row['placed_at']);
    $buckets[$d->format('Y-m')][] = $row;
}

// the same grouping, with no objects at all
foreach ($rows as $row) {
    $buckets[substr($row['placed_at'], 0, 7)][] = $row;
}

Sixty percent of that loop was object construction, which appears in a profile as time in an internal function rather than in the loop — so the profile points at DateTimeImmutable::__construct and the fix is not to call it. memory_get_peak_usage alongside the timing is what makes allocation visible at all. The general shape is that any per-row object in a large loop is worth questioning before anything else is optimised.