Skip to content

Commit

Permalink
Optimize array_key_exists/in_array for empty array
Browse files Browse the repository at this point in the history
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)`
  • Loading branch information
TysonAndre committed Dec 28, 2021
1 parent 85af420 commit 0989f65
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 24 deletions.
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

0 comments on commit 0989f65

Please sign in to comment.