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 Nov 17, 2019
1 parent f826bbd commit 9f410c2
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 17 deletions.
31 changes: 21 additions & 10 deletions ext/opcache/Optimizer/dfa_pass.c
Original file line number Diff line number Diff line change
Expand Up @@ -448,16 +448,27 @@ int zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
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 (zend_hash_num_elements(src) == 0) {
/* 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 {
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
17 changes: 17 additions & 0 deletions ext/opcache/Optimizer/sccp.c
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,10 @@ static inline int ct_eval_in_array(zval *result, uint32_t extended_value, zval *
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 @@ -1437,6 +1441,19 @@ 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;
}
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
8 changes: 1 addition & 7 deletions ext/opcache/Optimizer/zend_inference.c
Original file line number Diff line number Diff line change
Expand Up @@ -2480,14 +2480,8 @@ static int zend_update_type_info(const zend_op_array *op_array,
case ZEND_ISSET_ISEMPTY_STATIC_PROP:
case ZEND_ASSERT_CHECK:
case ZEND_IN_ARRAY:
UPDATE_SSA_TYPE(MAY_BE_FALSE|MAY_BE_TRUE, ssa_ops[i].result_def);
break;
case ZEND_ARRAY_KEY_EXISTS:
tmp = MAY_BE_FALSE|MAY_BE_TRUE;
if (t2 & ((MAY_BE_ANY|MAY_BE_UNDEF) - (MAY_BE_ARRAY|MAY_BE_OBJECT))) {
tmp |= MAY_BE_NULL;
}
UPDATE_SSA_TYPE(tmp, ssa_ops[i].result_def);
UPDATE_SSA_TYPE(MAY_BE_FALSE|MAY_BE_TRUE, ssa_ops[i].result_def);
break;
case ZEND_CAST:
if (ssa_ops[i].op1_def >= 0) {
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
--SKIPIF--
<?php require_once('skipif.inc'); ?>
--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
--SKIPIF--
<?php require_once('skipif.inc'); ?>
--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 9f410c2

Please sign in to comment.