Skip to content

Commit 3bf4457

Browse files
committed
Optimize array_key_exists/in_array for empty array
Make opcache replace the result with false if the array argument is known to be empty. This may be useful when a codebase has placeholders, e.g. `if (!in_array($method, self::ALLOWED_METHODS)) { return; }` In zend_inference.c: In php 8, array_key_exists will throw a TypeError instead of returning null. I didn't see any discussion of this optimization (for/against) after a quick search on github, e.g. GH-3360 Potential future optimizations: - convert `in_array($needle, ['only one element'], true)` to `===`? (or `==` for strict=false) - When the number of elements is less than 4, switch to looping instead of hash lookup. (exact threshold for better performance to be determined) Also support looping for `in_array($value, [false, 'str', 2.5], true/false)`
1 parent b85f0d1 commit 3bf4457

File tree

4 files changed

+157
-24
lines changed

4 files changed

+157
-24
lines changed

ext/opcache/Optimizer/dfa_pass.c

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -435,31 +435,50 @@ int zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
435435
uint32_t op_num = send_needly - op_array->opcodes;
436436
zend_ssa_op *ssa_op = ssa->ops + op_num;
437437

438-
if (ssa_op->op1_use >= 0) {
439-
/* Reconstruct SSA */
440-
int var_num = ssa_op->op1_use;
441-
zend_ssa_var *var = ssa->vars + var_num;
442-
443-
ZEND_ASSERT(ssa_op->op1_def < 0);
444-
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
445-
ssa_op->op1_use = -1;
446-
ssa_op->op1_use_chain = -1;
447-
op_num = call_info->caller_call_opline - op_array->opcodes;
448-
ssa_op = ssa->ops + op_num;
449-
ssa_op->op1_use = var_num;
450-
ssa_op->op1_use_chain = var->use_chain;
451-
var->use_chain = op_num;
452-
}
453-
454-
ZVAL_ARR(&tmp, dst);
438+
if (zend_hash_num_elements(src) == 0 &&
439+
!(ssa->var_info[ssa_op->op1_use].type & MAY_BE_UNDEF)) {
440+
if (ssa_op->op1_use >= 0) {
441+
/* Reconstruct SSA - the needle is no longer used by any part of the call */
442+
ZEND_ASSERT(ssa_op->op1_def < 0);
443+
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
444+
ssa_op->op1_use = -1;
445+
ssa_op->op1_use_chain = -1;
446+
}
447+
/* TODO remove needle from the uses of ssa graph? */
448+
ZVAL_FALSE(&tmp);
449+
zend_array_destroy(dst);
450+
451+
call_info->caller_call_opline->opcode = ZEND_QM_ASSIGN;
452+
call_info->caller_call_opline->extended_value = 0;
453+
call_info->caller_call_opline->op1_type = IS_CONST;
454+
call_info->caller_call_opline->op1.constant = zend_optimizer_add_literal(op_array, &tmp);
455+
call_info->caller_call_opline->op2_type = IS_UNUSED;
456+
} else {
457+
if (ssa_op->op1_use >= 0) {
458+
/* Reconstruct SSA - the needle is now used by the ZEND_IN_ARRAY opline */
459+
int var_num = ssa_op->op1_use;
460+
zend_ssa_var *var = ssa->vars + var_num;
455461

456-
/* Update opcode */
457-
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
458-
call_info->caller_call_opline->extended_value = strict;
459-
call_info->caller_call_opline->op1_type = send_needly->op1_type;
460-
call_info->caller_call_opline->op1.num = send_needly->op1.num;
461-
call_info->caller_call_opline->op2_type = IS_CONST;
462-
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
462+
ZEND_ASSERT(ssa_op->op1_def < 0);
463+
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
464+
ssa_op->op1_use = -1;
465+
ssa_op->op1_use_chain = -1;
466+
op_num = call_info->caller_call_opline - op_array->opcodes;
467+
ssa_op = ssa->ops + op_num;
468+
ssa_op->op1_use = var_num;
469+
ssa_op->op1_use_chain = var->use_chain;
470+
var->use_chain = op_num;
471+
}
472+
ZVAL_ARR(&tmp, dst);
473+
474+
/* Update opcode */
475+
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
476+
call_info->caller_call_opline->extended_value = strict;
477+
call_info->caller_call_opline->op1_type = send_needly->op1_type;
478+
call_info->caller_call_opline->op1.num = send_needly->op1.num;
479+
call_info->caller_call_opline->op2_type = IS_CONST;
480+
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
481+
}
463482
if (call_info->caller_init_opline->extended_value == 3) {
464483
MAKE_NOP(call_info->caller_call_opline - 1);
465484
}

ext/opcache/Optimizer/sccp.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,10 @@ static inline int ct_eval_in_array(zval *result, uint32_t extended_value, zval *
734734
return FAILURE;
735735
}
736736
ht = Z_ARRVAL_P(op2);
737+
if (zend_hash_num_elements(ht) == 0) {
738+
ZVAL_FALSE(result);
739+
return SUCCESS;
740+
}
737741
if (EXPECTED(Z_TYPE_P(op1) == IS_STRING)) {
738742
res = zend_hash_exists(ht, Z_STR_P(op1));
739743
} else if (extended_value) {
@@ -1372,6 +1376,19 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
13721376
ssa_op++;
13731377
SET_RESULT_BOT(op1);
13741378
break;
1379+
case ZEND_ARRAY_KEY_EXISTS:
1380+
if (ctx->scdf.ssa->var_info[ssa_op->op1_use].type & ~(MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING)) {
1381+
/* Skip needles that could cause TypeError in array_key_exists */
1382+
break;
1383+
}
1384+
case ZEND_IN_ARRAY:
1385+
SKIP_IF_TOP(op2);
1386+
if (Z_TYPE_P(op2) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(op2)) == 0) {
1387+
ZVAL_FALSE(&zv);
1388+
SET_RESULT(result, &zv);
1389+
return;
1390+
}
1391+
break;
13751392
}
13761393

13771394
if ((op1 && IS_BOT(op1)) || (op2 && IS_BOT(op2))) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
--TEST--
2+
array_key_exists() on known empty array
3+
--SKIPIF--
4+
<?php require_once('skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
error_reporting(E_ALL);
8+
function helper(&$var) {
9+
var_dump($var);
10+
}
11+
class ExampleArrayKeyExists {
12+
const EMPTY_ARRAY = [];
13+
public static function test(int $x, array $arr) {
14+
$y = array_key_exists($x, self::EMPTY_ARRAY);
15+
$v2 = array_key_exists($undef, self::EMPTY_ARRAY);
16+
$z = array_key_exists($x, []);
17+
$z1 = array_key_exists($x, [1 => true]);
18+
$z2 = array_key_exists($x, [2 => true]);
19+
$w = array_key_exists('literal', self::EMPTY_ARRAY);
20+
echo helper($y);
21+
echo helper($z);
22+
echo helper($w);
23+
echo helper($z1);
24+
echo helper($z2);
25+
$unusedVar = array_key_exists('unused', $arr);
26+
if (array_key_exists(printf("Should get called\n"), self::EMPTY_ARRAY)) {
27+
echo "Impossible\n";
28+
}
29+
$v = array_key_exists($arr, self::EMPTY_ARRAY);
30+
}
31+
}
32+
try {
33+
ExampleArrayKeyExists::test(1,[2]);
34+
} catch (TypeError $e) {
35+
printf("%s at line %d\n", $e->getMessage(), $e->getLine());
36+
}
37+
?>
38+
--EXPECTF--
39+
Warning: Undefined variable $undef in %s on line 10
40+
bool(false)
41+
bool(false)
42+
bool(false)
43+
bool(true)
44+
bool(false)
45+
Should get called
46+
Illegal offset type at line 24

ext/opcache/tests/in_array_empty.phpt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
--TEST--
2+
in_array() on known empty array
3+
--SKIPIF--
4+
<?php require_once('skipif.inc'); ?>
5+
--FILE--
6+
<?php
7+
error_reporting(E_ALL);
8+
function helper(&$var) {
9+
var_dump($var);
10+
}
11+
class ExampleInArray {
12+
const EMPTY_ARRAY = [];
13+
public static function test(int $x, array $arr) {
14+
$y = in_array($x, self::EMPTY_ARRAY);
15+
$y2 = in_array($x, self::EMPTY_ARRAY, true);
16+
$v2 = in_array($undef, self::EMPTY_ARRAY);
17+
$z = in_array($x, []);
18+
$w = in_array('literal', self::EMPTY_ARRAY);
19+
$z1 = in_array($x, [1]);
20+
$z2 = in_array($x, [2]);
21+
$z3 = in_array($x, [1], true);
22+
$z4 = in_array($x, [2], true);
23+
echo helper($y);
24+
echo helper($y2);
25+
echo helper($z);
26+
echo helper($w);
27+
echo "Results for non-empty arrays\n";
28+
echo helper($z1);
29+
echo helper($z2);
30+
echo helper($z3);
31+
echo helper($z4);
32+
$unusedVar = in_array('unused', $arr);
33+
if (in_array(printf("Should get called\n"), self::EMPTY_ARRAY)) {
34+
echo "Impossible\n";
35+
}
36+
}
37+
}
38+
ExampleInArray::test(1,[2]);
39+
?>
40+
--EXPECTF--
41+
Warning: Undefined variable $undef in %s on line 11
42+
bool(false)
43+
bool(false)
44+
bool(false)
45+
bool(false)
46+
Results for non-empty arrays
47+
bool(true)
48+
bool(false)
49+
bool(true)
50+
bool(false)
51+
Should get called

0 commit comments

Comments
 (0)