Skip to content
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

Improve performance of array_find() etc #18157

Merged
merged 2 commits into from
Mar 27, 2025
Merged

Conversation

nielsdos
Copy link
Member

This avoids destruction logic for the common case, avoids some copy, and adds an optimization hint.

On an intel i7 1185G7:

Benchmark 1: ./sapi/cli/php x.php
  Time (mean ± σ): 543.7 ms ± 3.8 ms [User: 538.9 ms, System: 4.4 ms]
  Range (min … max): 538.4 ms … 552.9 ms 10 runs

Benchmark 2: ./sapi/cli/php_old x.php
  Time (mean ± σ): 583.0 ms ± 4.2 ms [User: 578.4 ms, System: 3.4 ms]
  Range (min … max): 579.3 ms … 593.9 ms 10 runs

Summary
  ./sapi/cli/php x.php ran
    1.07 ± 0.01 times faster than ./sapi/cli/php_old x.php

On an intel i7 4790:

Benchmark 1: ./sapi/cli/php x.php
  Time (mean ± σ):     828.6 ms ±   4.8 ms    [User: 824.4 ms, System: 1.6 ms]
  Range (min … max):   822.8 ms … 839.0 ms    10 runs

Benchmark 2: ./sapi/cli/php_old x.php
  Time (mean ± σ):     940.1 ms ±  26.4 ms    [User: 934.4 ms, System: 2.5 ms]
  Range (min … max):   918.0 ms … 981.1 ms    10 runs

Summary
  ./sapi/cli/php x.php ran
    1.13 ± 0.03 times faster than ./sapi/cli/php_old x.php

This avoids destruction logic for the common case, avoids some copy, and
adds an optimization hint.

On an intel i7 1185G7:
```
Benchmark 1: ./sapi/cli/php x.php
  Time (mean ± σ): 543.7 ms ± 3.8 ms [User: 538.9 ms, System: 4.4 ms]
  Range (min … max): 538.4 ms … 552.9 ms 10 runs

Benchmark 2: ./sapi/cli/php_old x.php
  Time (mean ± σ): 583.0 ms ± 4.2 ms [User: 578.4 ms, System: 3.4 ms]
  Range (min … max): 579.3 ms … 593.9 ms 10 runs

Summary
  ./sapi/cli/php x.php ran
    1.07 ± 0.01 times faster than ./sapi/cli/php_old x.php
```

On an intel i7 4790:
```
Benchmark 1: ./sapi/cli/php x.php
  Time (mean ± σ):     828.6 ms ±   4.8 ms    [User: 824.4 ms, System: 1.6 ms]
  Range (min … max):   822.8 ms … 839.0 ms    10 runs

Benchmark 2: ./sapi/cli/php_old x.php
  Time (mean ± σ):     940.1 ms ±  26.4 ms    [User: 934.4 ms, System: 2.5 ms]
  Range (min … max):   918.0 ms … 981.1 ms    10 runs

Summary
  ./sapi/cli/php x.php ran
    1.13 ± 0.03 times faster than ./sapi/cli/php_old x.php
```
@nielsdos nielsdos marked this pull request as ready for review March 26, 2025 22:51
@nielsdos nielsdos requested a review from bukka as a code owner March 26, 2025 22:51
@nielsdos nielsdos requested review from TimWolla and iluuu1994 and removed request for bukka March 26, 2025 22:51
Copy link
Member

@TimWolla TimWolla left a comment

Choose a reason for hiding this comment

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

LGTM, except for the two remarks.

$ grep -m1 'model name' /proc/cpuinfo
model name	: 13th Gen Intel(R) Core(TM) i7-1365U
$ cat test.php
<?php
$array = range(1, 10000);

$result = 0;
for ($i = 0; $i < 5000; $i++) {
    $result += array_find($array, static function ($item) {
            return $item === 5000;
    });
}
var_dump($result);
$ taskset -c 0 hyperfine -L php php-before,php-after '/tmp/bench/{php} test.php'
Benchmark 1: /tmp/bench/php-before test.php
  Time (mean ± σ):     597.8 ms ±   4.6 ms    [User: 594.0 ms, System: 2.8 ms]
  Range (min … max):   592.4 ms … 606.0 ms    10 runs
 
Benchmark 2: /tmp/bench/php-after test.php
  Time (mean ± σ):     568.2 ms ±   9.5 ms    [User: 564.4 ms, System: 2.7 ms]
  Range (min … max):   560.9 ms … 591.3 ms    10 runs
 
Summary
  /tmp/bench/php-after test.php ran
    1.05 ± 0.02 times faster than /tmp/bench/php-before test.php

Comment on lines 6636 to 6648
bool retval_true;
switch (Z_TYPE(retval)) {
case IS_TRUE:
retval_true = !negate_condition;
break;
case IS_FALSE:
retval_true = negate_condition;
break;
default:
retval_true = zend_is_true(&retval) ^ negate_condition;
zval_ptr_dtor(&retval);
break;
}
Copy link
Member

Choose a reason for hiding this comment

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

This can probably also apply to array_filter(). Perhaps this logic can be moved into some force-inlined:

	bool is_true(zval *retval, bool negate_condition) {
		switch (Z_TYPE(retval)) {
			case IS_TRUE:
				return !negate_condition;
			case IS_FALSE:
				return negate_condition;
			default:
				bool retval_true = zend_is_true(&retval) ^ negate_condition;
				zval_ptr_dtor(&retval);
				return retval_true;
		}
	}

that is then used for array_find and array_filter.

@@ -6620,23 +6620,32 @@ static enum php_array_find_result php_array_find(const HashTable *array, zend_fc
if (!str_key) {
ZVAL_LONG(&args[1], num_key);
} else {
ZEND_ASSUME(!HT_IS_PACKED(array));
Copy link
Member

Choose a reason for hiding this comment

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

This could benefit from a short explanatory comment.

Copy link
Member

Choose a reason for hiding this comment

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

Ack. It's also not clear to me how this improves performance.

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.

LGTM in general. I'm also wondering whether Z_PARAM_FUNC() may benefit from optimizations for \Closure. zend_is_callable_ex() also finds the first user call frame, even though it's never used in zend_get_callable_name_ex() in the IS_OBJECT path. But this is ofc something that can be considered separately.

I was also wondering if it may be possible to store zend_fcall_info_cache statically for each function, and allow it to be re-used between calls through a polymorphic cache. This should work for immutable values living in shm, at least, given that they are guaranteed not to relocate. However, most callables are likely passed as \Closures nowadays, so this may not matter much anymore.

@@ -6620,23 +6620,32 @@ static enum php_array_find_result php_array_find(const HashTable *array, zend_fc
if (!str_key) {
ZVAL_LONG(&args[1], num_key);
} else {
ZEND_ASSUME(!HT_IS_PACKED(array));
Copy link
Member

Choose a reason for hiding this comment

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

Ack. It's also not clear to me how this improves performance.

@TimWolla
Copy link
Member

I'm also wondering whether Z_PARAM_FUNC() may benefit from optimizations for \Closure.

It probably does. See PR description in: symfony/symfony#60000.

I planned to have a look at the performance differences between Closure and callable as parameter types already.

@nielsdos nielsdos merged commit 0943b8b into php:master Mar 27, 2025
9 checks passed
@TimWolla
Copy link
Member

For reference, performance numbers after the latest changes with the extra function:

Benchmark 1: /tmp/bench/php-before test.php
  Time (mean ± σ):     667.0 ms ±   3.7 ms    [User: 662.7 ms, System: 2.8 ms]
  Range (min … max):   663.1 ms … 674.3 ms    10 runs
 
Benchmark 2: /tmp/bench/php-after test.php
  Time (mean ± σ):     650.5 ms ±   3.3 ms    [User: 646.9 ms, System: 2.0 ms]
  Range (min … max):   647.1 ms … 657.7 ms    10 runs
 
Summary
  /tmp/bench/php-after test.php ran
    1.03 ± 0.01 times faster than /tmp/bench/php-before test.php

No idea why there even is a 70ms difference in the new baseline …

@nielsdos
Copy link
Member Author

@TimWolla And if you add zend_always_inline to php_is_true ?

@TimWolla
Copy link
Member

@nielsdos Doesn't make a difference. And also note how the php-before has gotten slower (with the commit being 370f242 for the newer test). So it's probably my CPU being cursed again.

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

Successfully merging this pull request may close these issues.

3 participants