Repository navigation
Fix GH-9993: Bad performance with hard-coded GC_THRESHOLD_* values - #25243
kamil-tekiela wants to merge 2 commits into
Conversation
The GC raised its threshold only when a run collected fewer than 100 nodes. A run that traverses a large live graph but also frees some garbage kept the threshold at 10001, so the GC traversed the whole live graph again after every 10000 new roots. Test suites that keep large object graphs alive spent most of their time in the GC after phpGH-9266 fixed the count that the threshold rule uses. Count the nodes that a run traverses. After the run, set the threshold to at least (traversed - collected) / 8, so that the cost of a run is paid for by the roots added before the next run. The old step rule still sets the base threshold: * The threshold falls back to the base threshold as soon as a run stops traversing the live graph. * The threshold resets to the base threshold at request startup, as the live graph belongs to the request. * The root buffer grows to the new threshold with one reallocation. A higher threshold makes each run free a larger batch of garbage, which fits less well in the CPU cache and keeps garbage alive for longer. A ratio of 4 was faster for large live graphs, but raised peak memory by up to 43% and slowed down a 100k node live graph with large garbage cycles by 40%. With a ratio of 8 that case is 5-13% slower, and peak memory did not change in any measured case. Scripts with small live graphs keep the old threshold. With a live graph of 1M nodes that the script touches while it creates garbage cycles, the run time drops from 24.2s to 3.3s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This makes sense and I will probably merge this. It results in -50% wall time on some synthetic benchmark with 200k live nodes reachable from roots. A real application shows no regressions. I see no potential drawback, but I need to think more about it. Do you have a workload that matches #9993? This reminded me of this experiment: #19709. If you have a real workload, could you try it against that branch as well? (with |
I only had synthetic workloads. I tried it with PHPStan on Doctrine Dbal and your PR seems to provide similar or even better results, but it did crash so I don't know how accurate that is. Take it with a grain of salt. If you would like you can try to reproduce that yourself and you may be able to get more accurate results than me. |
Review material for php/php-src#25243: master vs the PR on GCC 15 and Clang 21, memory with destructors, a one-line fix for the rerun, doctrine/orm suite.
ArtUkrainskiy
left a comment
There was a problem hiding this comment.
One issue with destructors, see the line comment. Numbers below.
|
I measured this on master 4f3d950 with GCC 15 and Clang 21, on a Ryzen 5950X with the flags of the official Docker image. It does what it says. Nothing changes without a big live graph, and with 1M live objects the script takes 85% less time. The one issue is the rerun after destructors, see my line comment. Change in wall time against master (negative = faster):
@arnaud-lb, about real workloads. The GC takes 9% of doctrine/orm's test suite and 1% of Symfony Demo's, so the gain there is small. PHPStan and Composer call Details and scripts: https://github.com/ArtUkrainskiy/php-src-bench/tree/main/reports/pr-25243-gc-threshold |
Co-Authored-By: Artem Ukrainskiy <art.ukrainskiy@gmail.com>
9390291 to
cfbd52a
Compare
The GC raised its threshold only when a run collected fewer than 100
nodes. A run that traverses a large live graph but also frees some
garbage kept the threshold at 10001, so the GC traversed the whole live
graph again after every 10000 new roots. Test suites that keep large
object graphs alive spent most of their time in the GC after
GH-9266 fixed the count that the threshold rule uses.
Count the nodes that a run traverses. After the run, set the threshold
to at least (traversed - collected) / 8, so that the cost of a run is
paid for by the roots added before the next run. The old step rule
still sets the base threshold:
stops traversing the live graph.
the live graph belongs to the request.
A higher threshold makes each run free a larger batch of garbage, which
fits less well in the CPU cache and keeps garbage alive for longer. A
ratio of 4 was faster for large live graphs, but raised peak memory by
up to 43% and slowed down a 100k node live graph with large garbage
cycles by 40%. With a ratio of 8 that case is 5-13% slower, and peak
memory did not change in any measured case.
Scripts with small live graphs keep the old threshold. With a live
graph of 1M nodes that the script touches while it creates garbage
cycles, the run time drops from 24.2s to 3.3s.
Fixes GH-9993.