PHP / Advanced: reliable models and runtime evidence / Lesson 16
Performance, allocation, and runtime evidence
Improve a correct PHP workload using measured hypotheses and bounded data. Distinguish algorithm choices, request lifecycle, OPcache, JIT, and database cost without universal tuning prescriptions.
On this page
Lesson goals
- Measure a representative end-to-end operation.
- Explain memory retention and bounded caching.
- Separate runtime capabilities from proven speedups.
Start with a specific slow outcome
A performance question should name the user or batch outcome, data size, environment, and target. Separate PHP computation, database waiting, serialization, filesystem work, and response delivery before attributing a delay to the language. A faster helper may not improve the complete request if the database dominates. Record runtime, extensions, cache state, warm-up, and repeated observations; report variability instead of one attractive number. Keep correctness checks active so skipping records or validation does not count as a successful optimization. This chapter uses operation counts and local deterministic outputs, not fabricated production benchmark results.
An index trades memory for repeated lookup work
Repeated scans can perform unnecessary work when many lookups target the same dataset. Building an identifier map once may reduce comparisons but requires memory and an update/invalidation policy. The example counts the linear comparisons for three searches and confirms a map returns the same identifiers. It does not prove a specific engineโs timing complexity or a universal speedup. For small lists, index construction may cost more than it saves. Consider lookup frequency, data size, key uniqueness, and mutation patterns before selecting a representation. Reject duplicate identifiers or define which record wins deliberately.
<?php
declare(strict_types=1);
$rows = [["id" => 1], ["id" => 2], ["id" => 3]];
$wanted = [3, 1, 3]; $comparisons = 0; $found = [];
foreach ($wanted as $id) {
foreach ($rows as $row) {
$comparisons++;
if ($row["id"] === $id) { $found[] = $row["id"]; break; }
}
}
$index = array_column($rows, null, "id");
$mapped = array_map(fn(int $id): int => $index[$id]["id"], $wanted);
echo $comparisons, PHP_EOL;
echo $found === $mapped ? "same
" : "different
";
7
same
Scans of three, one, and three produce seven comparisons. The indexed result preserves the same requested order.
Retention depends on reachable owners and process lifetime
Copy-on-write can avoid an immediate physical copy for arrays, but later mutation, nested objects, caches, and serialization still have costs. A long-lived worker can retain global or static state across jobs in ways a short-request mental model misses. Bound cache size and lifetime, clear per-job data at owned boundaries, and avoid closures retaining large payloads unnecessarily. Garbage collection is not a policy for evicting useful-looking cached values. Test repeated operations and inspect growth with representative fixtures. A single high memory reading does not by itself establish a leak; find a retaining path or reproducible growth trend.
Name the cache policy you actually implement
A cache needs a key contract, size bound, invalidation rule, and clear ownership of mutable values. The example evicts the oldest inserted key when a third distinct entry exceeds capacity two. It is not LRU because reading does not refresh order. A real data cache may also need user/organization scope and authorization-aware invalidation so a performance feature cannot disclose one actorโs data to another. Cache misses and failed loads should remain distinct from legitimate empty values. Measure hit patterns before adding complexity and keep a simple uncached correctness path available for comparison in controlled tests.
<?php
declare(strict_types=1);
$cache = [];
foreach (["A" => 1, "B" => 2, "C" => 3] as $key => $value) {
$cache[$key] = $value;
if (count($cache) > 2) { unset($cache[array_key_first($cache)]); }
}
echo implode(",", array_keys($cache)), " ", count($cache), PHP_EOL;
B,C 2
The output confirms the size policy only; no hit-rate or elapsed-time benefit is asserted.
OPcache and JIT are capabilities to evaluate
OPcache stores compiled script bytecode to reduce repeated compilation under its configured lifecycle. JIT can optimize particular execution patterns, but enabling it does not guarantee faster database-heavy requests. CLI settings and web-server settings can differ, so php -v alone is not evidence of web OPcache behavior. Changes require controlled measurement, resource review, and correct invalidation or release procedures. Keep runtime and application upgrades separate enough to diagnose regressions. For a database bottleneck, inspect query count, plans, result size, and transaction duration on an authorized isolated workload rather than applying a generic PHP memory limit or worker-count recipe.
Common errors and fixes
JIT is promised to speed every request
- Why it happens
- Runtime capability is confused with measured workload benefit.
- How to fix it
- Measure representative correct end-to-end work.
- Prevent it next time
- Record configuration and compare controlled baselines.
A cache leaks data across actors
- Why it happens
- Key scope and authorization invalidation are incomplete.
- How to fix it
- Include required scope and invalidate on relevant changes.
- Prevent it next time
- Test different synthetic users and stale-permission cases.
One memory sample is called a leak
- Why it happens
- Allocation timing is mistaken for persistent growth.
- How to fix it
- Reproduce repeated growth and locate the retaining owner.
- Prevent it next time
- Test lifecycle cleanup across repeated jobs.
Check your understanding
1. What does the seven-comparison example establish?
Show worked solution
It establishes the number of tested linear comparisons and equivalent ordered identifiers from the map. It does not establish elapsed-time speedup on another dataset or runtime. Measure the complete workload including index construction and memory when deciding whether to change production design.
2. Why is the cache not LRU?
Show worked solution
It tracks insertion order and never refreshes an entry on read. A true recency policy needs explicit read-update behavior and tests for reads, overwrites, and eviction. Name the simpler policy honestly instead of copying an algorithm label from a different implementation.
Key takeaways
- Performance evidence must preserve correctness and report conditions.
- Algorithm, cache, database, and runtime costs require different diagnoses.