Repository navigation
Conversation
|
Haven't given much thought to this, but have you considered having it only do a full GC run when e.g. the collector didn't manage to collect some minimum number of cycles? Though I suppose this would still penalise applications which create very few cycles... In my particular case it'd be preferable to avoid full GC entirely. Realistically I'd still end up doing manual GC triggers anyway (so my application would have control over when to do a full vs incremental GC) but it's probably worth thinking about what conditions should trigger a full automatic GC. |
|
I tried this branch at 3e57c1e, merged onto master 4f3d950, with This reproduces it every time. It's the workload of the #25243 test, and without the env variable it's clean: <?php
class Node { public $owner; public $data; }
$c = new stdClass;
for ($i = 0; $i < 200000; $i++) { $n = new Node; $n->owner = $c; $c->nodes[] = $n; }
for ($i = 0; $i < 500000; $i++) {
$a = new Node; $b = new Node; $a->data = $b; $b->data = $a; unset($a, $b);
$n = $c->nodes[$i % 200000]; unset($n);
}
echo "ok\n";Without ASan, doctrine/orm's test suite segfaults on this branch, and garbage rings of 100k objects end with |
This is a PoC of making the cycle collector generational.
In some applications, there is an object that transitively references everything (e.g. the DI container or the UnitOfWork), and many objects transitively reference this object back. As a result, most GC runs in these applications will end up scanning the entire application. This defeats the premise of the cycle collector.
Based on that, a generational cycle collector should improve throughput to some degree.
Unlike a tracing GC, we don't need to keep track of created inter-generational references, so this turns out relatively simple.
This is enabled by setting the environment variable
FULL_GC_FREQto a non-zero value (e.g. 10 will run a full GC every 10 runs).I've used the following script to test this PoC: https://gist.github.com/arnaud-lb/6bfb493f361e056979571941f1b85990. This simulates a typical Doctrine or Symfony application with a very large DI container or UnitOfWork; a worse-case scenario for the GC.
With
FULL_GC_FREQ=10, the benchmark runs 10-20% faster when the root object (the Tree class) is large, or it creates garbage often (keeping the threshold low). The benchmark runs slower when the root object is relatively small and it does not create garbage often, as in comparison the non-gen GC will keep the threshold high in this case. This could be mitigated by improving how the threshold is adjusted in the gen GC. In the first case the non-gen GC is slow because of the low threshold, so we may achieve similar results in the non-gen GC with better adjustment of the threshold, at the cost of higher peak mm usage than the gen GC.Of course this is only one benchmark. This needs to be tested on real applications.
This should improve throughput and average pause time, but not maximum pause time.
Related: #17131
Implementation details:
GC_OLD. This flag is not part ofGC_INFO_MASK, so it's ignored byGC_INFO_MASK()/GC_REMOVE_FROM_BUFFER()/GC_REF_SET_INFO()and persists across GC runs. Caveat: Had to reclaim a bit fromGC_ADDRESS.gc_mark_roots()either ignores OLD nodes (in partial runs), or removes the OLD flag (in full runs)gc_scan_roots()always ignoresOLDnodesgc_collect_roots()movesOLDroots to the old roots buffer (unless this is a full run, as in this case we have proven these roots are not part of a garbage cycle).