Skip to content

Performance penalty for builtin/extension functions due to interrupt checks #19829

Open

Description

@dktapps

Description

I was profiling an extension that's intended to act as a faster replacement for the pack() and unpack() functions. The functions in the extension are very fast - just a few instructions each.

During profiling I discovered, much to my surprise, that the overhead of the DO_FCALL opcode far exceeded the time actually spent in my function, and that 2/3rds of the overhead was coming from ZEND_VM_FCALL_INTERRUPT_CHECK.

Doing some digging I found that this interrupt check runs after every single internal function call. This means that my extension's functions are being overshadowed by interrupt checks on every call which are >5x slower than the actual function itself.

The net result overall is less visible (a 5-10% performance degradation in my synthetic test script), but I have to ask if there's a better way to do this. Why is PHP checking for interrupts after every internal function call? I get it for stuff like network and I/O functions which could take a long time, but not every builtin function is like that. For the fast ones, this is an unavoidable and (probably) unnecessary performance penalty.

I don't know much about how the interrupt system works, but my feeling is that there has to be a better way to implement it than having every internal function call pay such a penalty. Even if there was a way to avoid this interrupt check with some ZEND_ACC flag that would tell the engine a function is "fast" and not to do the interrupt check?

Activity

  1. ndossche commented on Sep 13, 2025

    @ndossche
    Member

    This interupt check was added without much dicussion way back (before my time) IIRC
    See also Dmitry's comment on a related pr #14627 (comment)

  2. mvorisek commented on Sep 14, 2025

    @mvorisek
    Contributor

    With all interrupt checks removed, what is the performance improvement on the performance benchmarks?

  3. dktapps commented on Sep 14, 2025

    @dktapps
    ContributorAuthor

    Zend/bench.php doesn't show a significant difference in most tests, but I think largely because it doesn't really do much internal function calling. I didn't run the other benchmarks because my env doesn't have the right extensions.

    FWIW I don't think killing the interrupt checks completely is the right move, but I do think it could probably be polled less often.

  4. mvorisek commented on Sep 14, 2025

    @mvorisek
    Contributor

    I mean to remove them temporary to verify the impact on realworld userland app. If you know how to remove them, then you can push the php-src code to GH and GHA will output the benchmark numbers.

  5. arnaud-lb commented on Sep 15, 2025

    @arnaud-lb
    Member

    Interrupts are checked quite often, including before entering userland function (via ZEND_VM_ENTER_EX()), after returning from internal functions (via explicit check), in smart branches, loops, and some jumps (via ZEND_VM_SET_OPCODE()), etc. Most of these (including internal calls) were added in 5c68836.

    As @mvorisek has suggested, it would be interesting to verify the impact of interrupts by disabling them temporarily. If they are indeed the source of the overhead, a possible workaround would be to declare the function as frameless, as frameless calls do not check interrupts. This may be beneficial on its own, too.

  6. dktapps commented on Sep 15, 2025

    @dktapps
    ContributorAuthor

    If they are indeed the source of the overhead, a possible workaround would be to declare the function as frameless, as frameless calls do not check interrupts. This may be beneficial on its own, too.

    I had looked into this, but the documentation on frameless functions is extremely sparse. Also, as far as I know, they have to be declared differently from regular functions, which would be a pain in my case as I still need to support PHP <8.4.

    Benchmark results are as follows. The results look a bit underwhelming, but I will say that I don't think the instruction counts paint a full picture though.

    PHP 8.4 base

    https://github.com/php/php-src/actions/runs/17728778760/job/50375221322

    {
        "branch": "PHP-8.4",
        "Zend\/bench.php": {
            "instructions": "2238547261"
        },
        "Zend\/bench.php JIT": {
            "instructions": "602045601"
        },
        "Symfony Demo 2.2.3": {
            "instructions": "39825708"
        },
        "Symfony Demo 2.2.3 JIT": {
            "instructions": "34312908"
        },
        "Wordpress 6.2": {
            "instructions": "122750078"
        },
        "Wordpress 6.2 JIT": {
            "instructions": "93605841"
        }
    }
    
    Disabled internal function interrupts

    https://github.com/dktapps/php-src/actions/runs/17739651420/job/50410223738

    {
        "branch": "2\/merge",
        "Zend\/bench.php": {
            "instructions": "2235291018"
        },
        "Zend\/bench.php JIT": {
            "instructions": "602044206"
        },
        "Symfony Demo 2.2.3": {
            "instructions": "39759966"
        },
        "Symfony Demo 2.2.3 JIT": {
            "instructions": "34414535"
        },
        "Wordpress 6.2": {
            "instructions": "122607185"
        },
        "Wordpress 6.2 JIT": {
            "instructions": "93576566"
        }
    }
    
    Disabled loop interrupts:

    https://github.com/dktapps/php-src/actions/runs/17739667715/job/50410276410

    {
        "branch": "3\/merge",
        "Zend\/bench.php": {
            "instructions": "2238546893"
        },
        "Zend\/bench.php JIT": {
            "instructions": "602044524"
        },
        "Symfony Demo 2.2.3": {
            "instructions": "39818052"
        },
        "Symfony Demo 2.2.3 JIT": {
            "instructions": "34420746"
        },
        "Wordpress 6.2": {
            "instructions": "122749574"
        },
        "Wordpress 6.2 JIT": {
            "instructions": "93611056"
        }
    }
    
    Disabled `ZEND_VM_INTERRUPT_CHECK()`

    https://github.com/dktapps/php-src/actions/runs/17739683417/job/50410330496

     {
        "branch": "4\/merge",
        "Zend\/bench.php": {
            "instructions": "2166770913"
        },
        "Zend\/bench.php JIT": {
            "instructions": "602038150"
        },
        "Symfony Demo 2.2.3": {
            "instructions": "39618324"
        },
        "Symfony Demo 2.2.3 JIT": {
            "instructions": "34253136"
        },
        "Wordpress 6.2": {
            "instructions": "121592519"
        },
        "Wordpress 6.2 JIT": {
            "instructions": "93386286"
        }
    }
    
  7. arnaud-lb commented on Sep 16, 2025

    @arnaud-lb
    Member

    Thank you! I've computed the differences:

    Disabled internal function interrupts vs baseline:

    Zend/bench.php          -0.15%
    Zend/bench.php JIT      -0.00%
    Symfony Demo 2.2.3      -0.17%
    Symfony Demo 2.2.3 JIT  +0.30%
    Wordpress 6.2           -0.12%
    Wordpress 6.2 JIT       -0.03%
    

    Disabled ZEND_VM_INTERRUPT_CHECK() vs baseline:

    Zend/bench.php          -3.21%
    Zend/bench.php JIT      -0.00%
    Symfony Demo 2.2.3      -0.52%
    Symfony Demo 2.2.3 JIT  -0.17%
    Wordpress 6.2           -0.94%
    Wordpress 6.2 JIT       -0.23%
    

    These are not negligible. This is worth investigating.

  8. morrisonlevi commented on Dec 30, 2025

    @morrisonlevi
    Contributor

    All the use-cases of interrupts need to be accounted for if you intend to remove it. The main cases I know about are:

    1. The PHP execution timer was previously done in a signal handler and this caused crashes because most code is not async-signal-safe. This is likely the primary reason vm interrupts were added.
    2. Sampling profilers, probably other observability tools as well. Possibly debuggers, I'd definitely ask @derickr and @bwoebi.
    3. Theoretically, task schedulers. The interrupt points are generally designed so that you can jump away, but not for internal functions since PHP 8.4 I think? I made this change, I just don't remember which version. Dmitry has mentioned this many times when I was working on this space, but I am not personally aware of any extensions which actually use it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions