Skip to content

zend_vm: Add OPcode specialization for === [] #18571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

TimWolla
Copy link
Member

Checking whether an array is empty with a strict comparison against the empty array is a common pattern in PHP. A GitHub search for "=== []" language:PHP reveals 44k hits. From the set of !$a, count($a) === 0, empty($a) and $a === [] it however is also the slowest option.

A test script:

<?php

$variable = array_fill(0, 10, random_int(1, 2));

$f = true;
for ($i = 0; $i < 50_000_000; $i++) {
	$isEmpty = $variable === [];
	$f = $f && $isEmpty;
}

var_dump($f);

with the $isEmpty = …; statement appropriately replaced results in:

Benchmark 1: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
  Time (mean ± σ):     467.6 ms ±   2.3 ms    [User: 463.3 ms, System: 3.4 ms]
  Range (min … max):   464.6 ms … 473.4 ms    10 runs

Benchmark 2: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
  Time (mean ± σ):     305.3 ms ±   0.3 ms    [User: 302.0 ms, System: 3.1 ms]
  Range (min … max):   304.9 ms … 305.7 ms    10 runs

Benchmark 3: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php
  Time (mean ± σ):     630.3 ms ±   3.9 ms    [User: 624.8 ms, System: 3.8 ms]
  Range (min … max):   627.4 ms … 637.6 ms    10 runs

Benchmark 4: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
  Time (mean ± σ):     311.8 ms ±   3.4 ms    [User: 307.9 ms, System: 3.6 ms]
  Range (min … max):   308.7 ms … 320.7 ms    10 runs

Summary
  sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php ran
    1.02 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
    1.53 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
    2.06 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php

This patch adds another OPcode specialization for ZEND_IS_IDENTICAL that specifically matches a comparison against the empty array. With this specialization the === [] check becomes the fastest of them all, which is not surprising given how specific it is:

Benchmark 1: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
  Time (mean ± σ):     384.1 ms ±   2.3 ms    [User: 379.3 ms, System: 3.8 ms]
  Range (min … max):   382.2 ms … 389.8 ms    10 runs

Benchmark 2: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
  Time (mean ± σ):     305.8 ms ±   3.2 ms    [User: 301.7 ms, System: 3.8 ms]
  Range (min … max):   304.4 ms … 314.9 ms    10 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Benchmark 3: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php
  Time (mean ± σ):     293.9 ms ±   2.9 ms    [User: 289.7 ms, System: 3.3 ms]
  Range (min … max):   291.5 ms … 299.4 ms    10 runs

Benchmark 4: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
  Time (mean ± σ):     306.8 ms ±   0.4 ms    [User: 303.8 ms, System: 2.7 ms]
  Range (min … max):   306.3 ms … 307.3 ms    10 runs

Summary
  sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php ran
    1.04 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
    1.04 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
    1.31 ± 0.02 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php

As a follow-up optimization it might be possible to transform the other emptiness checks, such as count($arr) === 0 into $arr === [] if $arr is known to be MAY_BE_ARRAY only.

@TimWolla
Copy link
Member Author

Will follow up with !== [] if this one is good.

@@ -10062,6 +10062,19 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_IS_NOT_EQUAL|ZEND_IS_NOT_IDENTICAL, (op1_info
ZEND_VM_SMART_BRANCH(result, 0);
}

ZEND_VM_TYPE_SPEC_HANDLER(ZEND_IS_IDENTICAL, op->op2_type == IS_CONST && (Z_TYPE_P(RT_CONSTANT(op, op->op2)) == IS_ARRAY && zend_hash_num_elements(Z_ARR_P(RT_CONSTANT(op, op->op2))) == 0), ZEND_IS_IDENTICAL_EMPTY_ARRAY, TMPVARCV, CONST, SPEC(SMART_BRANCH,COMMUTATIVE))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do I manually need to check op->op2_type == IS_CONST here? Shouldn't that be the job of the CONST in ZEND_IS_IDENTICAL_EMPTY_ARRAY, TMPVARCV, CONST?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More generally speaking, I'm not at all sure if the “flags” I specified here are correct, since I clearly don't understand the implications. So please carefully check this 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can look at the generated zend_vm_set_opcode_handler_ex(). It assumes op types are correct. If it works only for some op types, checking the op type should be the right approach. The signature looks correct to me.

Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I benchmarked this against Symfony Demo and saw varying but consistent improvements of ~0.1% on average. Given the 3 additional handlers are quite small, I would personally support this.

TimWolla added 2 commits May 16, 2025 13:50
Checking whether an array is empty with a strict comparison against the empty
array is a common pattern in PHP. A GitHub search for `"=== []" language:PHP`
reveals 44k hits. From the set of `!$a`, `count($a) === 0`, `empty($a)` and
`$a === []` it however is also the slowest option.

A test script:

    <?php

    $variable = array_fill(0, 10, random_int(1, 2));

    $f = true;
    for ($i = 0; $i < 50_000_000; $i++) {
    	$isEmpty = $variable === [];
    	$f = $f && $isEmpty;
    }

    var_dump($f);

with the `$isEmpty = …;` statement appropriately replaced results in:

    Benchmark 1: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
      Time (mean ± σ):     467.6 ms ±   2.3 ms    [User: 463.3 ms, System: 3.4 ms]
      Range (min … max):   464.6 ms … 473.4 ms    10 runs

    Benchmark 2: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
      Time (mean ± σ):     305.3 ms ±   0.3 ms    [User: 302.0 ms, System: 3.1 ms]
      Range (min … max):   304.9 ms … 305.7 ms    10 runs

    Benchmark 3: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php
      Time (mean ± σ):     630.3 ms ±   3.9 ms    [User: 624.8 ms, System: 3.8 ms]
      Range (min … max):   627.4 ms … 637.6 ms    10 runs

    Benchmark 4: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
      Time (mean ± σ):     311.8 ms ±   3.4 ms    [User: 307.9 ms, System: 3.6 ms]
      Range (min … max):   308.7 ms … 320.7 ms    10 runs

    Summary
      sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php ran
        1.02 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
        1.53 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
        2.06 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php

This patch adds another OPcode specialization for `ZEND_IS_IDENTICAL` that
specifically matches a comparison against the empty array. With this
specialization the `=== []` check becomes the fastest of them all, which is not
surprising given how specific it is:

    Benchmark 1: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php
      Time (mean ± σ):     384.1 ms ±   2.3 ms    [User: 379.3 ms, System: 3.8 ms]
      Range (min … max):   382.2 ms … 389.8 ms    10 runs

    Benchmark 2: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
      Time (mean ± σ):     305.8 ms ±   3.2 ms    [User: 301.7 ms, System: 3.8 ms]
      Range (min … max):   304.4 ms … 314.9 ms    10 runs

      Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

    Benchmark 3: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php
      Time (mean ± σ):     293.9 ms ±   2.9 ms    [User: 289.7 ms, System: 3.3 ms]
      Range (min … max):   291.5 ms … 299.4 ms    10 runs

    Benchmark 4: sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
      Time (mean ± σ):     306.8 ms ±   0.4 ms    [User: 303.8 ms, System: 2.7 ms]
      Range (min … max):   306.3 ms … 307.3 ms    10 runs

    Summary
      sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 identical.php ran
        1.04 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 empty.php
        1.04 ± 0.01 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 not.php
        1.31 ± 0.02 times faster than sapi/cli/php -d zend_extension=modules/opcache.so -d opcache.enable_cli=1 count.php

As a follow-up optimization it might be possible to transform the other
emptiness checks, such as `count($arr) === 0` into `$arr === []` if `$arr` is
known to be `MAY_BE_ARRAY` only.
@TimWolla TimWolla force-pushed the identical-to-empty-array branch from 0527e89 to 3597d4b Compare May 16, 2025 11:50
@TimWolla
Copy link
Member Author

TimWolla commented May 16, 2025

Given the 3 additional handlers are quite small,

Without the SMART_BRANCH it's only one handler. I don't know what exactly that does and whether or not it provides a value-add in this case.

I've also just added the corresponding !== [] handler, since you considered the existing one to be good. So we're at additional 6 handlers total.

@iluuu1994
Copy link
Member

The smart branch is used when the comparison is immediately followed by a conditional branch. Instead of advancing to the opcode to branch, it branches directly.

@iluuu1994
Copy link
Member

I can't measure more improvements for this change, but Symfony+vendors only has ~20 uses of [] !== or !== [], so not a big improvement is expected.

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