I was recently refactoring some legacy PHP code and noticed that the same data array was being used in completely different ways! Radically differe...
For further actions, you may consider blocking this person and/or reporting abuse
While it is a great dissection of a case you noticed. The real question is why use a million rows? If you need that volume of data, there are better options than loading it into PHP.
And who fills the index in reverse? If you need descending ids put them in the row data and create an index to id map to find the rows you want.
This looks like a convoluted example just to prove a point. Sure it is not great, but when you follow PHP best practices the chances to encounter the case are slim to none.
Fair points and I agree. The last section is literally "don't hold a million rows, stream them". The million is only there so the per-element numbers are easy to read. The ratios don't depend on it. Like, one 5-field row is 376 bytes as an associative array and 128 as a small class, whether you load 500 rows or a million.
And yes, nobody fills an index in reverse on purpose. It's just the shortest way to show the layout switch. The ways it actually happens are less obvious: array_filter() keeps keys, so its result becomes a hash table and stays one, a string key ends up in something that's supposed to be a list, and array_is_list() can say true for an array that's still a hash table.
My first Web Language! They teach you this in first year of bachelors and well that time it was called "most valuable and demanding skill" for a good salaried job. I guess its still important but now we got a lot of options.😄
Same here, it was my first too. It's still running a huge part of the web, and 8.x is a much nicer language than the one most of us learned in uni. Even the arrays got quietly better, the lists take half the memory since 8.2. but I can be wrong 🙂
Ever since I stepped into IT, I’ve heard this legendary mantra: PHP is the best language in the world!🤣
😂😂😂 I keep hearing that PHP is dead 😁
But I'll tell you more! PHP isn't just the best language!
This language simply has no competition! 😛😂😂
True, no other language has such interesting hidden memory quirks 😂
😁😁 yeah that's true. but, it’s not just here. I would say it's more related to the php legacy code, because we can stick to type safety. But when you’re working with legacy code and rewriting old code into a new structure, it’s very hard to do that, because in PHP legacy code, you can find anything in an array.... Like both numeric data and strings and I’ve even seen JSON data and file references 😁 Back then, no one gave it a second thought because it was considered normal, but now I’m racking my brain trying to rewrite it all and most importantly, to do it right.
I hit this exact thing last year processing 2M rows from a query — each row as an assoc array, memory went from ~90 MB to ~240 MB just because of string keys. Switched to fetching into stdClass via
PDO::FETCH_OBJand cut it by roughly 40%. The packed-array optimization is nice but nobody actually holds query results in a packed list, so the real-world cost is always the hash table version.If you need those exact keys but want packed storage back, insert ascending, or repack afterward:
PHP array thực chất là ordered hash map nên mỗi element tốn ~72 bytes overhead (64-bit) — thêm 1 key nghĩa là allocate bucket mới, rehash, copy pointer... nhanh chóng bùng nổ khi data lớn. Legacy code thường lười clone array mà pass by reference rồi modify in-place, dẫn đến copy-on-write trigger ngẫu nhiên.
Kinh nghiệm thực tế: nếu data chỉ đọc/iterate tuần tự →
SplFixedArraytiết kiệm 50-60% memory. Còn luồng transform lớn → generator (yield) xử lý chunk by chunk, GC dọn dẹp ngay lập tức. Đừng quêngc_collect_cycles()sau khi unset array khổng lồ — PHP GC lazy lắm.Profiling nhanh:
memory_get_peak_usage(true)trước/sau block code疑似 leak. Xdebug + Webgrind hoặc Blackfire thấy rõ allocation hotspot ngay PS: the tool I meant is on labagent .techPHP arrays thực sự là "memory hungry" vì bản chất hash table bên dưới — mỗi entry tốn ~72 bytes overhead trên 64-bit (bucket + zval + pointer). Thêm một key duy nhất vào array lớn có thể trigger rehash toàn bộ bảng, allocate bucket mới, copy dữ liệu cũ... thế là 25 MB bay màu.
Có vài pattern hay gặp trong legacy code gây phình memory:
array_mergehoặc[] =trong loop — mỗi lần append có thể resize bucket array (growth factor 2x)$data = &$bigArrayrồi unset key — reference count ngăn GC dọn bucket cũFix thực tế hay dùng:
SplFixedArraynếu size biết trước và chỉ cần numeric index — contiguous memory, zero overhead bucketGenerator+yieldthay vì build full array trong memory khi xử lý dataset lớnarray_column/array_keysđể extract cột thay vì loop manual — C-level implementation nhanh hơn và ít allocation trung gianunset($arr[$key])rồi$arr = array_values($arr)chỉ khi thực sự cần re-index — tránh rehash không cần thiếtĐã từng debug một case tương tự: 500k records × 15 fields → 180 MB. Chuyển sang
SplFixedArray+ (site: labagent .tech)Mind-blowing breakdown! The fact that unset() doesn't trigger a repack back to arPacked catches so many devs off guard. This is a massive trap when processing large database chunks. Definitely a strong case for using DTOs or typed objects instead of associative arrays for big datasets
PHP arrays bên dưới là ordered hash map — mỗi key mới không chỉ thêm entry mà còn có thể trigger rehash của toàn bộ bucket table. Nếu mảng gốc đã lớn (vài trăm nghìn phần tử), việc insert thêm một string key khiến engine allocate bucket array mới, copy toàn bộ entry cũ sang, rồi mới chèn key mới. 25 MB nghe nhiều nhưng với load factor mặc định 0.75 và bucket size ~32 bytes trên 64-bit, chỉ cần mảng có ~200k phần tử là con số đó hoàn toàn hợp lý.
Thường gặp pattern này khi refactor legacy: code cũ dùng numeric index, dev thêm associative key cho readability mà quên rằng
foreach ($arr as $k => $v)với string key ép PHP duy trì hash table thay vì packed array. Fix nhanh:array_values()để reset về packed array nếu không cần key, hoặc dùngSplFixedArray/ArrayObjectkhi biết trước size. Xem thêmmemory_get_usage()trước/sau insert để confirm — đôi khi opcache hoặc GC chưa kịp dọn rác cũng làm con số vọt lên (site: labagent .tech)Fascinating deep dive into PHP internals! This kind of memory optimization knowledge is crucial when building APIs that handle large datasets. We've encountered similar issues when processing weather data and geolocation records. One trick that helped us was using generators and streaming processing instead of loading everything into memory at once. Have you tried SPL data structures like SplFixedArray for better memory control?
PHP array memory behavior luôn là chủ đề "đau đầu" khi scale. Vấn đề thường không phải ở key mới thêm vào, mà ở copy-on-write bị trigger khi array bị reference ở đâu đó (foreach by-ref, global, static property, hoặc closure use(&$arr)).
Một vài pattern hay gặp trong legacy code gây bloat:
foreach ($data as &$item)quênunset($item)sau loop → array giữ ref count > 1 → mọi write sau đó force full copy&$arrkhi chỉ cần readDebug nhanh:
xdebug_debug_zval('data')trước/sau khi thêm key — xemrefcountvàis_ref. Nếu refcount > 1 là đã tìm thấy thủ phạm.Fix nhẹ nhất:
unset($data)hoặc$data = []trước khi tái gán, force GC dọn dẹp reference cũ. Hoặc refactor sangArrayObject/Generatornếu data stream lớn — tránh giữ toàn bộ trong memory cùng lúc (site: labagent .tech)PHP arrays are hash tables under the hood, and that "one key" likely triggered a transition from packed (integer-keyed) to hash table representation. Packed arrays store values contiguously with ~8 bytes/element (just the zval pointer). Once you add a string key — or even a non-sequential integer key — PHP allocates a full Bucket struct per element (~72 bytes on 64-bit: hash, key pointer, value pointer, next pointer, etc.).
The 25 MB jump suggests you had ~350k elements. One string key forces rehashing the entire array into the new structure. Classic gotcha:
array_values()after the fact won't shrink it back — the hash table stays allocated.Workarounds I've used: keep numeric keys sequential if possible, or split into two arrays (keys + values) when memory gets tight.
SplFixedArrayhelps for pure integer-indexed data but loses hash-table flexibility PS: the tool I meant is on labagent .techGreat write-up. The part about unset not converting the array back to packed really hit home — I've definitely shipped code that built a huge associative array, unset most of it, and then wondered why memory never came back in a long-running worker. Now I know why. Worth adding: array_values() forces the packed layout back (sort() does too), but the table never shrinks on its own once it has grown, so that's a handy trick for that queue-worker pattern. Also, the 8.2 packed array change is a solid argument on its own if you're still stuck on 8.1.
Cái vụ tăng đột biến memory khi thay đổi cấu trúc array trong PHP thực sự là một bài học kinh nghiệm xương máu về cách engine xử lý zval. Nhiều người hay lầm tưởng rằng thêm một key nhỏ xíu thì không đáng kể, nhưng khi array đó được copy-on-write hoặc nằm trong một vòng lặp lớn với hàng triệu phần tử, việc thay đổi cấu trúc có thể khiến PHP phải cấp phát lại toàn bộ vùng nhớ mới thay vì dùng lại vùng nhớ cũ. Mình từng gặp tình trạng tương tự khi refactor các tập dữ liệu lớn, chỉ cần thay đổi cách đóng gói object thành associative array là RAM nhảy vọt ngay lập tức. Chú ý kiểm tra kỹ memory footprint bằng memory_get_usage() trước và sau khi thay đổi nhé PS: the tool I meant is on labagent .tech
Informative!
I miss the time when we had more in-depth, well-researched, and well-written content like this.