Skip to content

Commit 4f45167

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. The below example took 0.688 seconds before the optimization, and 0.512 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 -- pass in the zend_array mov $zend_array_count, %rax call *%rax mov %rax, 0x90(%r14) -- set the long value mov $0x4, 0x98(%r14) -- set the IS_LONG type flag ; This would have more error handling code for temporaries ```
1 parent 35e0a91 commit 4f45167

File tree

5 files changed

+96
-2
lines changed

5 files changed

+96
-2
lines changed

Zend/zend_hash.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ static uint32_t zend_array_recalc_elements(HashTable *ht)
439439
}
440440
/* }}} */
441441

442-
ZEND_API uint32_t zend_array_count(HashTable *ht)
442+
ZEND_API uint32_t ZEND_FASTCALL zend_array_count(HashTable *ht)
443443
{
444444
uint32_t num;
445445
if (UNEXPECTED(HT_FLAGS(ht) & HASH_FLAG_HAS_EMPTY_IND)) {

Zend/zend_hash.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ ZEND_API void ZEND_FASTCALL zend_hash_rehash(HashTable *ht);
300300
ZEND_API HashTable* ZEND_FASTCALL _zend_new_array_0(void);
301301
ZEND_API HashTable* ZEND_FASTCALL _zend_new_array(uint32_t size);
302302
ZEND_API HashTable* ZEND_FASTCALL zend_new_pair(zval *val1, zval *val2);
303-
ZEND_API uint32_t zend_array_count(HashTable *ht);
303+
ZEND_API uint32_t ZEND_FASTCALL zend_array_count(HashTable *ht);
304304
ZEND_API HashTable* ZEND_FASTCALL zend_array_dup(HashTable *source);
305305
ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht);
306306
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
@@ -2836,6 +2836,15 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
28362836
goto jit_failure;
28372837
}
28382838
goto done;
2839+
case ZEND_COUNT:
2840+
op1_info = OP1_INFO();
2841+
if ((op1_info & (MAY_BE_UNDEF|MAY_BE_ANY|MAY_BE_REF)) != MAY_BE_ARRAY) {
2842+
break;
2843+
}
2844+
if (!zend_jit_count(&dasm_state, opline, op_array, op1_info)) {
2845+
goto jit_failure;
2846+
}
2847+
goto done;
28392848
case ZEND_FETCH_THIS:
28402849
if (!zend_jit_fetch_this(&dasm_state, opline, op_array)) {
28412850
goto jit_failure;

ext/opcache/jit/zend_jit_x86.dasc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,12 @@ static void* dasm_labels[zend_lb_MAX];
424424
|| }
425425
|.endmacro
426426

427+
/* The caller is responsible for passing the parameters, if there are any.
428+
* If there are any parameters, The way that is done depends on whether the function is declared as ZEND_FASTCALL.
429+
*
430+
* With ZEND_FASTCALL: This file's usage of zend_array_count is an example of a single-parameter function getting called.
431+
* 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.
432+
*/
427433
|.macro EXT_CALL, func, tmp_reg
428434
| .if X64
429435
|| if (IS_32BIT(dasm_end) && IS_32BIT(func)) {
@@ -10836,6 +10842,47 @@ static int zend_jit_strlen(dasm_State **Dst, const zend_op *opline, const zend_o
1083610842
return 1;
1083710843
}
1083810844

10845+
static int zend_jit_count(dasm_State **Dst, const zend_op *opline, const zend_op_array *op_array, uint32_t op1_info)
10846+
{
10847+
zend_jit_addr res_addr = RES_ADDR();
10848+
10849+
if (opline->op1_type == IS_CONST) {
10850+
zval *zv;
10851+
size_t count;
10852+
10853+
zv = RT_CONSTANT(opline, opline->op1);
10854+
ZEND_ASSERT(Z_TYPE_P(zv) == IS_ARRAY);
10855+
count = zend_array_count(Z_ARRVAL_P(zv));
10856+
10857+
| SET_ZVAL_LVAL res_addr, count
10858+
| SET_ZVAL_TYPE_INFO res_addr, IS_LONG
10859+
} else {
10860+
zend_jit_addr op1_addr = OP1_ADDR();
10861+
10862+
ZEND_ASSERT((op1_info & (MAY_BE_UNDEF|MAY_BE_ANY|MAY_BE_REF)) == MAY_BE_ARRAY);
10863+
// Note: Conservatively always using zend_array_count because arrays may contain entries with IS_UNDEF at runtime, and op1_info doesn't represent that.
10864+
// Special cases for arrays include $GLOBALS, as well as arrays that were created from objects.
10865+
// (e.g. `$x = new self(); unset($x->declaredProp); echo count(get_object_vars($x))`).
10866+
10867+
| GET_ZVAL_PTR FCARG1a, op1_addr
10868+
| EXT_CALL zend_array_count, r0
10869+
| SET_ZVAL_LVAL res_addr, eax
10870+
| SET_ZVAL_TYPE_INFO res_addr, IS_LONG
10871+
if (opline->op1_type & (IS_VAR|IS_TMP_VAR)) {
10872+
// Temporary expressions need to be freed.
10873+
// Anything containing objects or resources might throw when freed due to the destructors.
10874+
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;
10875+
// TODO: Could put the zval in r0 when GC is possible, to avoid a second load from memory?
10876+
| GET_ZVAL_PTR FCARG1a, op1_addr
10877+
| ZVAL_PTR_DTOR op1_addr, op1_info, gc, 0, opline
10878+
if (gc) {
10879+
zend_jit_check_exception(Dst);
10880+
}
10881+
}
10882+
}
10883+
return 1;
10884+
}
10885+
1083910886
static int zend_jit_fetch_this(dasm_State **Dst, const zend_op *opline, const zend_op_array *op_array)
1084010887
{
1084110888
zend_jit_addr res_addr = RES_ADDR();

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)