Skip to content

GC does not scale well with a lot of objects created in destructor #13670

Closed

Description

@mvorisek

Description

The following code:

repro: https://3v4l.org/L7Dok

When the $i <= 3_200 limit (on line 31) is changed to $i <= 3_400, then the GC is very slow.

When the $i <= 3_200 limit (on line 31) is changed to $i <= 3_500, then the GC is very slow and internally exhaust call stack.

It seems the repro hits some internal limitation of php-src GC.

Resulted in this output:

since some threshold the GC is triggered massively:

...
2500, peak: 8 MiB, GC runs: 2, time: 0 s
2600, peak: 8 MiB, GC runs: 2, time: 0.01 s
2700, peak: 8 MiB, GC runs: 2, time: 0.01 s
2800, peak: 8 MiB, GC runs: 4, time: 0.02 s
2900, peak: 8 MiB, GC runs: 2, time: 0.01 s
3000, peak: 8 MiB, GC runs: 2, time: 0.01 s
3100, peak: 8 MiB, GC runs: 2, time: 0.01 s
3200, peak: 8 MiB, GC runs: 4, time: 0.02 s
3300, peak: 10 MiB, GC runs: 2, time: 0.01 s
3400, peak: 10 MiB, GC runs: 398, time: 2.02 s

Stack trace:
#0 .../repro.php(66): Repro\CycleValue->__destruct()
#1 {main}

Next Error: Maximum call stack size of 67043328 bytes reached. Infinite recursion? in .../repro.php:23
Stack trace:
#0 .../repro.php(66): Repro\CycleValue->__destruct()
#1 {main}

Next Error: Maximum call stack size of 67043328 bytes reached. Infinite recursion? in .../repro.php:23
Stack trace:
#0 .../repro.php(66): Repro\CycleValue->__destruct()
#1 {main}

... (heavily nested exception)

But I expected this output instead:

linear scaleability as the created objects consist always of the same/2 cycle length

I would expected $i <= 100_000 to pass in reasonable time. [1]

PHP Version

any (tested 7.4, 8.2, 8.3, /w and /wo opcache)

Operating System

any (tested Windows, linux)

[1] when the objects are not created in destructors I get about these (expected) times:

...
100000, peak: 66 MiB, time: 0.43 s
110000, peak: 68 MiB, time: 0.49 s
120000, peak: 80 MiB, time: 0.55 s
130000, peak: 82 MiB, time: 0.59 s
140000, peak: 90 MiB, time: 0.67 s
150000, peak: 96 MiB, time: 0.71 s
160000, peak: 100 MiB, time: 0.77 s
170000, peak: 102 MiB, time: 0.85 s
180000, peak: 104 MiB, time: 0.92 s
190000, peak: 112 MiB, time: 0.97 s
200000, peak: 116 MiB, time: 1.11 s

Activity

  1. changed the title [-]Segfault when GC is put on limits[/-] [+]Segfault when GC/destructors are put on limits[/+] on Mar 11, 2024
  2. changed the title [-]Segfault when GC/destructors are put on limits[/-] [+]Segfault when GC/destructors are put on their limits[/+] on Mar 11, 2024
  3. changed the title [-]Segfault when GC/destructors are put on their limits[/-] [+]GC does not scale well[/+] on Mar 11, 2024
  4. changed the title [-]GC does not scale well[/-] [+]GC does not scale well with a lot of isolated cycles[/+] on Mar 11, 2024
  5. changed the title [-]GC does not scale well with a lot of isolated cycles[/-] [+]GC does not scale well with a lot of objects created in destructor[/+] on Mar 11, 2024
  6. self-assigned this
    on Mar 11, 2024
  7. arnaud-lb commented on Mar 11, 2024

    @arnaud-lb
    Member

    This is triggering some edge case where collection does not decrease the number of roots (because garbage is created in destructors), and the GC threshold is not updated because this does not match heuristics. As a result, num_roots is permanently greater than gc_threadshold, so collections are run every time we create potential garbage.

    I think we need to update the condition here to look at the decrease of num_roots rather than the number of collected roots:

    diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c
    index 572e3b66fd4..08634c29424 100644
    --- a/Zend/zend_gc.c
    +++ b/Zend/zend_gc.c
    @@ -604,11 +604,12 @@ static void gc_grow_root_buffer(void)
     static void gc_adjust_threshold(int count)
     {
            uint32_t new_threshold;
    +       uint32_t old_num_roots = GC_G(num_roots);
     
            /* TODO Very simple heuristic for dynamic GC buffer resizing:
             * If there are "too few" collections, increase the collection threshold
             * by a fixed step */
    -       if (count < GC_THRESHOLD_TRIGGER) {
    +       if (GC_G(num_roots) > old_num_roots || (old_num_roots - GC_G(num_roots)) < GC_THRESHOLD_TRIGGER) {
                    /* increase */
                    if (GC_G(gc_threshold) < GC_THRESHOLD_MAX) {
                            new_threshold = GC_G(gc_threshold) + GC_THRESHOLD_STEP;

    This fixes the issue, but I need to think more about it.

  8. arnaud-lb commented on Mar 25, 2024

    @arnaud-lb
    Member

    Fixed by #13758

  9. mvorisek commented on Mar 25, 2024

    @mvorisek
    ContributorAuthor

    Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions