Skip to content

Optimize array_key_exists/in_array for empty array #4925

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 43 additions & 24 deletions Zend/Optimizer/dfa_pass.c
Original file line number Diff line number Diff line change
Expand Up @@ -446,31 +446,50 @@ int zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
uint32_t op_num = send_needly - op_array->opcodes;
zend_ssa_op *ssa_op = ssa->ops + op_num;

if (ssa_op->op1_use >= 0) {
/* Reconstruct SSA */
int var_num = ssa_op->op1_use;
zend_ssa_var *var = ssa->vars + var_num;

ZEND_ASSERT(ssa_op->op1_def < 0);
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
ssa_op->op1_use = -1;
ssa_op->op1_use_chain = -1;
op_num = call_info->caller_call_opline - op_array->opcodes;
ssa_op = ssa->ops + op_num;
ssa_op->op1_use = var_num;
ssa_op->op1_use_chain = var->use_chain;
var->use_chain = op_num;
}

ZVAL_ARR(&tmp, dst);
if (zend_hash_num_elements(src) == 0 &&
!(ssa->var_info[ssa_op->op1_use].type & MAY_BE_UNDEF)) {
if (ssa_op->op1_use >= 0) {
/* Reconstruct SSA - the needle is no longer used by any part of the call */
ZEND_ASSERT(ssa_op->op1_def < 0);
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
ssa_op->op1_use = -1;
ssa_op->op1_use_chain = -1;
}
/* TODO remove needle from the uses of ssa graph? */
ZVAL_FALSE(&tmp);
zend_array_destroy(dst);

call_info->caller_call_opline->opcode = ZEND_QM_ASSIGN;
call_info->caller_call_opline->extended_value = 0;
call_info->caller_call_opline->op1_type = IS_CONST;
call_info->caller_call_opline->op1.constant = zend_optimizer_add_literal(op_array, &tmp);
call_info->caller_call_opline->op2_type = IS_UNUSED;
} else {
if (ssa_op->op1_use >= 0) {
/* Reconstruct SSA - the needle is now used by the ZEND_IN_ARRAY opline */
int var_num = ssa_op->op1_use;
zend_ssa_var *var = ssa->vars + var_num;

/* Update opcode */
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
call_info->caller_call_opline->extended_value = strict;
call_info->caller_call_opline->op1_type = send_needly->op1_type;
call_info->caller_call_opline->op1.num = send_needly->op1.num;
call_info->caller_call_opline->op2_type = IS_CONST;
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
ZEND_ASSERT(ssa_op->op1_def < 0);
zend_ssa_unlink_use_chain(ssa, op_num, ssa_op->op1_use);
ssa_op->op1_use = -1;
ssa_op->op1_use_chain = -1;
op_num = call_info->caller_call_opline - op_array->opcodes;
ssa_op = ssa->ops + op_num;
ssa_op->op1_use = var_num;
ssa_op->op1_use_chain = var->use_chain;
var->use_chain = op_num;
}
ZVAL_ARR(&tmp, dst);

/* Update opcode */
call_info->caller_call_opline->opcode = ZEND_IN_ARRAY;
call_info->caller_call_opline->extended_value = strict;
call_info->caller_call_opline->op1_type = send_needly->op1_type;
call_info->caller_call_opline->op1.num = send_needly->op1.num;
call_info->caller_call_opline->op2_type = IS_CONST;
call_info->caller_call_opline->op2.constant = zend_optimizer_add_literal(op_array, &tmp);
}
if (call_info->caller_init_opline->extended_value == 3) {
MAKE_NOP(call_info->caller_call_opline - 1);
}
Expand Down
18 changes: 18 additions & 0 deletions Zend/Optimizer/sccp.c
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,10 @@ static inline zend_result ct_eval_in_array(zval *result, uint32_t extended_value
return FAILURE;
}
ht = Z_ARRVAL_P(op2);
if (zend_hash_num_elements(ht) == 0) {
ZVAL_FALSE(result);
return SUCCESS;
}
if (EXPECTED(Z_TYPE_P(op1) == IS_STRING)) {
res = zend_hash_exists(ht, Z_STR_P(op1));
} else if (extended_value) {
Expand Down Expand Up @@ -1211,6 +1215,20 @@ static void sccp_visit_instr(scdf_ctx *scdf, zend_op *opline, zend_ssa_op *ssa_o
ssa_op++;
SET_RESULT_BOT(op1);
break;
case ZEND_ARRAY_KEY_EXISTS:
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)) {
/* Skip needles that could cause TypeError in array_key_exists */
break;
}
ZEND_FALLTHROUGH;
case ZEND_IN_ARRAY:
SKIP_IF_TOP(op2);
if (Z_TYPE_P(op2) == IS_ARRAY && zend_hash_num_elements(Z_ARRVAL_P(op2)) == 0) {
ZVAL_FALSE(&zv);
SET_RESULT(result, &zv);
return;
}
break;
}

if ((op1 && IS_BOT(op1)) || (op2 && IS_BOT(op2))) {
Expand Down
46 changes: 46 additions & 0 deletions ext/opcache/tests/array_key_exists_empty.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
array_key_exists() on known empty array
--EXTENSIONS--
opcache
--FILE--
<?php
error_reporting(E_ALL);
function helper(&$var) {
var_dump($var);
}
class ExampleArrayKeyExists {
const EMPTY_ARRAY = [];
public static function test(int $x, array $arr) {
$y = array_key_exists($x, self::EMPTY_ARRAY);
$v2 = array_key_exists($undef, self::EMPTY_ARRAY);
$z = array_key_exists($x, []);
$z1 = array_key_exists($x, [1 => true]);
$z2 = array_key_exists($x, [2 => true]);
$w = array_key_exists('literal', self::EMPTY_ARRAY);
echo helper($y);
echo helper($z);
echo helper($w);
echo helper($z1);
echo helper($z2);
$unusedVar = array_key_exists('unused', $arr);
if (array_key_exists(printf("Should get called\n"), self::EMPTY_ARRAY)) {
echo "Impossible\n";
}
$v = array_key_exists($arr, self::EMPTY_ARRAY);
}
}
try {
ExampleArrayKeyExists::test(1,[2]);
} catch (TypeError $e) {
printf("%s at line %d\n", $e->getMessage(), $e->getLine());
}
?>
--EXPECTF--
Warning: Undefined variable $undef in %s on line 10
bool(false)
bool(false)
bool(false)
bool(true)
bool(false)
Should get called
Illegal offset type at line 24
51 changes: 51 additions & 0 deletions ext/opcache/tests/in_array_empty.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
--TEST--
in_array() on known empty array
--EXTENSIONS--
opcache
--FILE--
<?php
error_reporting(E_ALL);
function helper(&$var) {
var_dump($var);
}
class ExampleInArray {
const EMPTY_ARRAY = [];
public static function test(int $x, array $arr) {
$y = in_array($x, self::EMPTY_ARRAY);
$y2 = in_array($x, self::EMPTY_ARRAY, true);
$v2 = in_array($undef, self::EMPTY_ARRAY);
$z = in_array($x, []);
$w = in_array('literal', self::EMPTY_ARRAY);
$z1 = in_array($x, [1]);
$z2 = in_array($x, [2]);
$z3 = in_array($x, [1], true);
$z4 = in_array($x, [2], true);
echo helper($y);
echo helper($y2);
echo helper($z);
echo helper($w);
echo "Results for non-empty arrays\n";
echo helper($z1);
echo helper($z2);
echo helper($z3);
echo helper($z4);
$unusedVar = in_array('unused', $arr);
if (in_array(printf("Should get called\n"), self::EMPTY_ARRAY)) {
echo "Impossible\n";
}
}
}
ExampleInArray::test(1,[2]);
?>
--EXPECTF--
Warning: Undefined variable $undef in %s on line 11
bool(false)
bool(false)
bool(false)
bool(false)
Results for non-empty arrays
bool(true)
bool(false)
bool(true)
bool(false)
Should get called