Skip to content

Commit 9f410c2

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 f826bbd commit 9f410c2

File tree

5 files changed

+136
-17
lines changed

5 files changed

+136
-17
lines changed

ext/opcache/Optimizer/dfa_pass.c

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -448,16 +448,27 @@ int zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
448448
ssa_op->op1_use_chain = var->use_chain;
449449
var->use_chain = op_num;
450450
}
451-
452-
ZVAL_ARR(&tmp, dst);
453-
454-
/* Update opcode */
455-
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
456-
call_info->caller_call_opline->extended_value = strict;
457-
call_info->caller_call_opline->op1_type = send_needly->op1_type;
458-
call_info->caller_call_opline->op1.num = send_needly->op1.num;
459-
call_info->caller_call_opline->op2_type = IS_CONST;
460-
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
451+
if (zend_hash_num_elements(src) == 0) {
452+
/* TODO remove needle from the uses of ssa graph? */
453+
ZVAL_FALSE(&tmp);
454+
zend_array_destroy(dst);
455+
456+
call_info->caller_call_opline->opcode = ZEND_QM_ASSIGN;
457+
call_info->caller_call_opline->extended_value = 0;
458+
call_info->caller_call_opline->op1_type = IS_CONST;
459+
call_info->caller_call_opline->op1.constant = zend_optimizer_add_literal(op_array, &tmp);
460+
call_info->caller_call_opline->op2_type = IS_UNUSED;
461+
} else {
462+
ZVAL_ARR(&tmp, dst);
463+
464+
/* Update opcode */
465+
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
466+
call_info->caller_call_opline->extended_value = strict;
467+
call_info->caller_call_opline->op1_type = send_needly->op1_type;
468+
call_info->caller_call_opline->op1.num = send_needly->op1.num;
469+
call_info->caller_call_opline->op2_type = IS_CONST;
470+
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
471+
}
461472
if (call_info->caller_init_opline->extended_value == 3) {
462473
MAKE_NOP(call_info->caller_call_opline - 1);
463474
}

ext/opcache/Optimizer/sccp.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,10 @@ static inline int ct_eval_in_array(zval *result, uint32_t extended_value, zval *
728728
return FAILURE;
729729
}
730730
ht = Z_ARRVAL_P(op2);
731+
if (zend_hash_num_elements(ht) == 0) {
732+
ZVAL_FALSE(result);
733+
return SUCCESS;
734+
}
731735
if (EXPECTED(Z_TYPE_P(op1) == IS_STRING)) {
732736
res = zend_hash_exists(ht, Z_STR_P(op1));
733737
} else if (extended_value) {
@@ -1437,6 +1441,19 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
14371441
ssa_op++;
14381442
SET_RESULT_BOT(op1);
14391443
break;
1444+
case ZEND_ARRAY_KEY_EXISTS:
1445+
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)) {
1446+
/* Skip needles that could cause TypeError in array_key_exists */
1447+
break;
1448+
}
1449+
case ZEND_IN_ARRAY:
1450+
SKIP_IF_TOP(op2);
1451+
if (Z_TYPE_P(op2) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(op2)) == 0) {
1452+
ZVAL_FALSE(&zv);
1453+
SET_RESULT(result, &zv);
1454+
return;
1455+
}
1456+
break;
14401457
}
14411458

14421459
if ((op1 && IS_BOT(op1)) || (op2 && IS_BOT(op2))) {

ext/opcache/Optimizer/zend_inference.c

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2480,14 +2480,8 @@ static int zend_update_type_info(const zend_op_array *op_array,
24802480
case ZEND_ISSET_ISEMPTY_STATIC_PROP:
24812481
case ZEND_ASSERT_CHECK:
24822482
case ZEND_IN_ARRAY:
2483-
UPDATE_SSA_TYPE(MAY_BE_FALSE|MAY_BE_TRUE, ssa_ops[i].result_def);
2484-
break;
24852483
case ZEND_ARRAY_KEY_EXISTS:
2486-
tmp = MAY_BE_FALSE|MAY_BE_TRUE;
2487-
if (t2 & ((MAY_BE_ANY|MAY_BE_UNDEF) - (MAY_BE_ARRAY|MAY_BE_OBJECT))) {
2488-
tmp |= MAY_BE_NULL;
2489-
}
2490-
UPDATE_SSA_TYPE(tmp, ssa_ops[i].result_def);
2484+
UPDATE_SSA_TYPE(MAY_BE_FALSE|MAY_BE_TRUE, ssa_ops[i].result_def);
24912485
break;
24922486
case ZEND_CAST:
24932487
if (ssa_ops[i].op1_def >= 0) {
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)