Skip to content

Commit 7f47104

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. phpGH-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 85af420 commit 7f47104

File tree

4 files changed

+157
-24
lines changed

4 files changed

+157
-24
lines changed

Zend/Optimizer/dfa_pass.c

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

449-
if (ssa_op->op1_use >= 0) {
450-
/* Reconstruct SSA */
451-
int var_num = ssa_op->op1_use;
452-
zend_ssa_var *var = ssa->vars + var_num;
453-
454-
ZEND_ASSERT(ssa_op->op1_def < 0);
455-
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
456-
ssa_op->op1_use = -1;
457-
ssa_op->op1_use_chain = -1;
458-
op_num = call_info->caller_call_opline - op_array->opcodes;
459-
ssa_op = ssa->ops + op_num;
460-
ssa_op->op1_use = var_num;
461-
ssa_op->op1_use_chain = var->use_chain;
462-
var->use_chain = op_num;
463-
}
464-
465-
ZVAL_ARR(&tmp, dst);
449+
if (zend_hash_num_elements(src) == 0 &&
450+
!(ssa->var_info[ssa_op->op1_use].type & MAY_BE_UNDEF)) {
451+
if (ssa_op->op1_use >= 0) {
452+
/* Reconstruct SSA - the needle is no longer used by any part of the call */
453+
ZEND_ASSERT(ssa_op->op1_def < 0);
454+
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
455+
ssa_op->op1_use = -1;
456+
ssa_op->op1_use_chain = -1;
457+
}
458+
/* TODO remove needle from the uses of ssa graph? */
459+
ZVAL_FALSE(&tmp);
460+
zend_array_destroy(dst);
461+
462+
call_info->caller_call_opline->opcode = ZEND_QM_ASSIGN;
463+
call_info->caller_call_opline->extended_value = 0;
464+
call_info->caller_call_opline->op1_type = IS_CONST;
465+
call_info->caller_call_opline->op1.constant = zend_optimizer_add_literal(op_array, &tmp);
466+
call_info->caller_call_opline->op2_type = IS_UNUSED;
467+
} else {
468+
if (ssa_op->op1_use >= 0) {
469+
/* Reconstruct SSA - the needle is now used by the ZEND_IN_ARRAY opline */
470+
int var_num = ssa_op->op1_use;
471+
zend_ssa_var *var = ssa->vars + var_num;
466472

467-
/* Update opcode */
468-
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
469-
call_info->caller_call_opline->extended_value = strict;
470-
call_info->caller_call_opline->op1_type = send_needly->op1_type;
471-
call_info->caller_call_opline->op1.num = send_needly->op1.num;
472-
call_info->caller_call_opline->op2_type = IS_CONST;
473-
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
473+
ZEND_ASSERT(ssa_op->op1_def < 0);
474+
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
475+
ssa_op->op1_use = -1;
476+
ssa_op->op1_use_chain = -1;
477+
op_num = call_info->caller_call_opline - op_array->opcodes;
478+
ssa_op = ssa->ops + op_num;
479+
ssa_op->op1_use = var_num;
480+
ssa_op->op1_use_chain = var->use_chain;
481+
var->use_chain = op_num;
482+
}
483+
ZVAL_ARR(&tmp, dst);
484+
485+
/* Update opcode */
486+
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
487+
call_info->caller_call_opline->extended_value = strict;
488+
call_info->caller_call_opline->op1_type = send_needly->op1_type;
489+
call_info->caller_call_opline->op1.num = send_needly->op1.num;
490+
call_info->caller_call_opline->op2_type = IS_CONST;
491+
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
492+
}
474493
if (call_info->caller_init_opline->extended_value == 3) {
475494
MAKE_NOP(call_info->caller_call_opline - 1);
476495
}

Zend/Optimizer/sccp.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,10 @@ static inline zend_result ct_eval_in_array(zval *result, uint32_t extended_value
696696
return FAILURE;
697697
}
698698
ht = Z_ARRVAL_P(op2);
699+
if (zend_hash_num_elements(ht) == 0) {
700+
ZVAL_FALSE(result);
701+
return SUCCESS;
702+
}
699703
if (EXPECTED(Z_TYPE_P(op1) == IS_STRING)) {
700704
res = zend_hash_exists(ht, Z_STR_P(op1));
701705
} else if (extended_value) {
@@ -1211,6 +1215,19 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
12111215
ssa_op++;
12121216
SET_RESULT_BOT(op1);
12131217
break;
1218+
case ZEND_ARRAY_KEY_EXISTS:
1219+
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)) {
1220+
/* Skip needles that could cause TypeError in array_key_exists */
1221+
break;
1222+
}
1223+
case ZEND_IN_ARRAY:
1224+
SKIP_IF_TOP(op2);
1225+
if (Z_TYPE_P(op2) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(op2)) == 0) {
1226+
ZVAL_FALSE(&zv);
1227+
SET_RESULT(result, &zv);
1228+
return;
1229+
}
1230+
break;
12141231
}
12151232

12161233
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)