Skip to content

Commit 9b6d568

Browse files
committed
Optimize ZEND_COUNT opcodes on arrays in the jit
Avoid the overhead of a call and checking types when the argument is definitely an array. Avoid the overhead of gc when `__destruct` won't get called. This seemed cheap enough to check for in the jit. Because of https://wiki.php.net/rfc/restrict_globals_usage we can be sure in the ZEND_COUNT handler that the array count does not have to be recomputed in php 8.1. The below example took 0.854 seconds before the optimization, and 0.564 seconds after the optimization, giving the same result ```php <?php /** @jit */ function bench_count(int $n): int { $total = 0; $arr = []; for ($i = 0; $i < $n; $i++) { $arr[] = $i; $total += count($arr); } return $total; } function main() { $n = 1000; $iterations = 50000; $start = microtime(true); $result = 0; for ($i = 0; $i < $iterations; $i++) { $result += bench_count($n); } $elapsed = microtime(true) - $start; printf("Total for n=%d, iterations=%d = %d, elapsed=%.3f\n", $n, $iterations, $result, $elapsed); } main(); ``` Before ```asm mov $0x7feb8cf8a858, %r15 mov $ZEND_COUNT_SPEC_CV_UNUSED_HANDLER, %rax call *%rax ``` After ```asm mov 0x70(%r14), %rdi - Copy the count from the `zend_array*` pointer mov %rdi, (%rax) - Store the count in the destination's value mov $0x4, 0x8(%rax) - Store IS_LONG(4) in the destination's type ```
1 parent 320843f commit 9b6d568

File tree

4 files changed

+92
-2
lines changed

4 files changed

+92
-2
lines changed

Zend/zend_hash.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht);
298298
ZEND_API HashTable* ZEND_FASTCALL _zend_new_array_0(void);
299299
ZEND_API HashTable* ZEND_FASTCALL _zend_new_array(uint32_t size);
300300
ZEND_API HashTable* ZEND_FASTCALL zend_new_pair(zval *val1, zval *val2);
301-
ZEND_API uint32_t zend_array_count(HashTable *ht);
301+
ZEND_API uint32_t ZEND_FASTCALL zend_array_count(HashTable *ht);
302302
ZEND_API HashTable* ZEND_FASTCALL zend_array_dup(HashTable *source);
303303
ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht);
304304
ZEND_API void ZEND_FASTCALL zend_symtable_clean(HashTable *ht);

ext/opcache/jit/zend_jit.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3210,6 +3210,15 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
32103210
goto jit_failure;
32113211
}
32123212
goto done;
3213+
case ZEND_COUNT:
3214+
op1_info = OP1_INFO();
3215+
if ((op1_info & (MAY_BE_UNDEF|MAY_BE_ANY|MAY_BE_REF)) != MAY_BE_ARRAY) {
3216+
break;
3217+
}
3218+
if (!zend_jit_count(&dasm_state, opline, op_array, op1_info)) {
3219+
goto jit_failure;
3220+
}
3221+
goto done;
32133222
case ZEND_FETCH_THIS:
32143223
if (!zend_jit_fetch_this(&dasm_state, opline, op_array, 0)) {
32153224
goto jit_failure;

ext/opcache/jit/zend_jit_x86.dasc

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,12 @@ static void* dasm_labels[zend_lb_MAX];
374374
|| }
375375
|.endmacro
376376

377+
/* The caller is responsible for passing the parameters, if there are any.
378+
* If there are any parameters, The way that is done depends on whether the function is declared as ZEND_FASTCALL.
379+
*
380+
* With ZEND_FASTCALL: This file's usage of zend_array_count is an example of a single-parameter function getting called.
381+
* Without ZEND_FASTCALL: This file's usage of zend_interrupt_function is an example of a single-parameter function that is ZEND_FASTCALL getting called.
382+
*/
377383
|.macro EXT_CALL, func, tmp_reg
378384
| .if X64
379385
|| if (IS_32BIT(dasm_end) && IS_32BIT(func)) {
@@ -9197,7 +9203,7 @@ static int zend_jit_init_fcall(dasm_State **Dst, const zend_op *opline, uint32_t
91979203
#endif
91989204
/* load constant address later */
91999205
} else if (func && op_array == &func->op_array) {
9200-
/* recursive call */
9206+
/* recursive call */
92019207
if (!(func->op_array.fn_flags & ZEND_ACC_IMMUTABLE) ||
92029208
(sizeof(void*) == 8 && !IS_SIGNED_32BIT(func))) {
92039209
| mov r0, EX->func
@@ -15015,6 +15021,43 @@ static int zend_jit_fetch_constant(dasm_State **Dst,
1501515021
return 1;
1501615022
}
1501715023

15024+
static int zend_jit_count(dasm_State **Dst, const zend_op *opline, const zend_op_array *op_array, uint32_t op1_info)
15025+
{
15026+
zend_jit_addr res_addr = RES_ADDR();
15027+
15028+
if (opline->op1_type == IS_CONST) {
15029+
zval *zv;
15030+
size_t count;
15031+
15032+
zv = RT_CONSTANT(opline, opline->op1);
15033+
ZEND_ASSERT(Z_TYPE_P(zv) == IS_ARRAY);
15034+
count = zend_array_count(Z_ARRVAL_P(zv));
15035+
15036+
| SET_ZVAL_LVAL res_addr, count
15037+
| SET_ZVAL_TYPE_INFO res_addr, IS_LONG
15038+
} else {
15039+
zend_jit_addr op1_addr = OP1_ADDR();
15040+
15041+
ZEND_ASSERT((op1_info & (MAY_BE_UNDEF|MAY_BE_ANY|MAY_BE_REF)) == MAY_BE_ARRAY);
15042+
// Note: See the implementation of ZEND_COUNT in Zend/zend_vm_def.h - arrays do not contain IS_UNDEF starting in php 8.1+.
15043+
15044+
| GET_ZVAL_PTR FCARG1a, op1_addr
15045+
| mov r0, [FCARG1a + offsetof(HashTable, nNumOfElements)]
15046+
| SET_ZVAL_LVAL res_addr, r0
15047+
| SET_ZVAL_TYPE_INFO res_addr, IS_LONG
15048+
if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {
15049+
// Temporary expressions need to be freed.
15050+
// Anything containing objects or resources might throw when freed due to the destructors.
15051+
zend_bool gc = (op1_info & (MAY_BE_ARRAY_OF_OBJECT|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_ARRAY_OF_RESOURCE|MAY_BE_ARRAY_OF_REF)) != 0;
15052+
| ZVAL_PTR_DTOR op1_addr, op1_info, gc, 0, opline
15053+
if (gc) {
15054+
zend_jit_check_exception(Dst);
15055+
}
15056+
}
15057+
}
15058+
return 1;
15059+
}
15060+
1501815061
static int zend_jit_in_array(dasm_State **Dst, const zend_op *opline, uint32_t op1_info, zend_jit_addr op1_addr, zend_uchar smart_branch_opcode, uint32_t target_label, uint32_t target_label2, const void *exit_addr)
1501915062
{
1502015063
HashTable *ht = Z_ARRVAL_P(RT_CONSTANT(opline, opline->op2));

ext/opcache/tests/jit/count_001.phpt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
JIT COUNT: 001
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.file_update_protection=0
7+
opcache.jit_buffer_size=1M
8+
;opcache.jit_debug=1
9+
--SKIPIF--
10+
<?php require_once('skipif.inc'); ?>
11+
--FILE--
12+
<?php
13+
class C {
14+
public static function create_array(int $i): array {
15+
return array_fill(0, $i, new stdClass());
16+
}
17+
18+
public static function foo() {
19+
$x = [self::create_array(5)];
20+
echo count(self::create_array(0)), "\n";
21+
echo count(self::create_array(1)), "\n";
22+
echo count($x[0]), "\n";
23+
$a = [];
24+
for ($i = 0; $i < 4; $i++) {
25+
$a[] = $i;
26+
echo count($a) . "\n";
27+
}
28+
}
29+
}
30+
C::foo();
31+
--EXPECT--
32+
0
33+
1
34+
5
35+
1
36+
2
37+
3
38+
4

0 commit comments

Comments
 (0)