Skip to content
Closed
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
29 changes: 29 additions & 0 deletions Zend/tests/switch_on_numeric_strings.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
Switch on numeric strings
--FILE--
<?php

function test($value) {
switch ($value) {
case "01": return "01";
case "1": return "1";

case " 2": return " 2";
case "2": return "2";

case "10.0": return "10.0";
case "1e1": return "1e1";

default: return "default";
}
}

var_dump(test("1"));
var_dump(test("2"));
var_dump(test("1e1"));

?>
--EXPECT--
string(2) "01"
string(2) " 2"
string(4) "10.0"
100 changes: 99 additions & 1 deletion Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,8 @@ static void zend_check_live_ranges(zend_op *opline) /* {{{ */
} else if (opline->opcode == ZEND_FAST_RET) {
/* fast_calls don't have to be destroyed */
} else if (opline->opcode == ZEND_CASE ||
opline->opcode == ZEND_SWITCH_LONG ||
opline->opcode == ZEND_SWITCH_STRING ||
opline->opcode == ZEND_FE_FETCH_R ||
opline->opcode == ZEND_FE_FETCH_RW ||
opline->opcode == ZEND_FE_FREE ||
Expand Down Expand Up @@ -4603,6 +4605,58 @@ void zend_compile_if(zend_ast *ast) /* {{{ */
}
/* }}} */

static zend_uchar determine_switch_jumptable_type(zend_ast_list *cases) {
uint32_t i;
zend_uchar common_type = IS_UNDEF;
for (i = 0; i < cases->children; i++) {
zend_ast *case_ast = cases->child[i];
zend_ast **cond_ast = &case_ast->child[0];
zval *cond_zv;
if (!case_ast->child[0]) {
/* Skip default clause */
continue;
}

zend_eval_const_expr(cond_ast);
if ((*cond_ast)->kind != ZEND_AST_ZVAL) {
/* Non-constant case */
return IS_UNDEF;
}

cond_zv = zend_ast_get_zval(case_ast->child[0]);
if (Z_TYPE_P(cond_zv) != IS_LONG && Z_TYPE_P(cond_zv) != IS_STRING) {
/* We only optimize switched on integers and strings */
return IS_UNDEF;
}

if (common_type == IS_UNDEF) {
common_type = Z_TYPE_P(cond_zv);
} else if (common_type != Z_TYPE_P(cond_zv)) {
/* Non-uniform case types */
return IS_UNDEF;
}

if (Z_TYPE_P(cond_zv) == IS_STRING
&& is_numeric_string(Z_STRVAL_P(cond_zv), Z_STRLEN_P(cond_zv), NULL, NULL, 0)) {
/* Numeric strings cannot be compared with a simple hash lookup */
return IS_UNDEF;
}
}

return common_type;
}

static zend_bool should_use_jumptable(zend_ast_list *cases, zend_uchar jumptable_type) {
/* Thresholds are chosen based on when the average switch time for equidistributed
* input becomes smaller when using the jumptable optimization. */
if (jumptable_type == IS_LONG) {
return cases->children >= 5;
} else {
ZEND_ASSERT(jumptable_type == IS_STRING);
return cases->children >= 2;
}
}

void zend_compile_switch(zend_ast *ast) /* {{{ */
{
zend_ast *expr_ast = ast->child[0];
Expand All @@ -4613,7 +4667,9 @@ void zend_compile_switch(zend_ast *ast) /* {{{ */

znode expr_node, case_node;
zend_op *opline;
uint32_t *jmpnz_opnums, opnum_default_jmp;
uint32_t *jmpnz_opnums, opnum_default_jmp, opnum_switch;
zend_uchar jumptable_type;
HashTable *jumptable = NULL;

zend_compile_expr(&expr_node, expr_ast);

Expand All @@ -4622,6 +4678,24 @@ void zend_compile_switch(zend_ast *ast) /* {{{ */
case_node.op_type = IS_TMP_VAR;
case_node.u.op.var = get_temporary_variable(CG(active_op_array));

jumptable_type = determine_switch_jumptable_type(cases);
if (jumptable_type != IS_UNDEF && should_use_jumptable(cases, jumptable_type)) {
znode jumptable_op;

ALLOC_HASHTABLE(jumptable);
zend_hash_init(jumptable, cases->children, NULL, NULL, 0);
jumptable_op.op_type = IS_CONST;
ZVAL_ARR(&jumptable_op.u.constant, jumptable);

opline = zend_emit_op(NULL,
jumptable_type == IS_LONG ? ZEND_SWITCH_LONG : ZEND_SWITCH_STRING,
&expr_node, &jumptable_op);
if (opline->op1_type == IS_CONST) {
zval_copy_ctor(CT_CONSTANT(opline->op1));
}
opnum_switch = opline - CG(active_op_array)->opcodes;
}

jmpnz_opnums = safe_emalloc(sizeof(uint32_t), cases->children, 0);
for (i = 0; i < cases->children; ++i) {
zend_ast *case_ast = cases->child[i];
Expand Down Expand Up @@ -4666,15 +4740,39 @@ void zend_compile_switch(zend_ast *ast) /* {{{ */

if (cond_ast) {
zend_update_jump_target_to_next(jmpnz_opnums[i]);

if (jumptable) {
zval *cond_zv = zend_ast_get_zval(cond_ast);
zval jmp_target;
ZVAL_LONG(&jmp_target, get_next_op_number(CG(active_op_array)));

ZEND_ASSERT(Z_TYPE_P(cond_zv) == jumptable_type);
if (Z_TYPE_P(cond_zv) == IS_LONG) {
zend_hash_index_add(jumptable, Z_LVAL_P(cond_zv), &jmp_target);
} else {
ZEND_ASSERT(Z_TYPE_P(cond_zv) == IS_STRING);
zend_hash_add(jumptable, Z_STR_P(cond_zv), &jmp_target);
}
}
} else {
zend_update_jump_target_to_next(opnum_default_jmp);

if (jumptable) {
opline = &CG(active_op_array)->opcodes[opnum_switch];
opline->extended_value = get_next_op_number(CG(active_op_array));
}
}

zend_compile_stmt(stmt_ast);
}

if (!has_default_case) {
zend_update_jump_target_to_next(opnum_default_jmp);

if (jumptable) {
opline = &CG(active_op_array)->opcodes[opnum_switch];
opline->extended_value = get_next_op_number(CG(active_op_array));
}
}

zend_end_loop(get_next_op_number(CG(active_op_array)), &expr_node);
Expand Down
13 changes: 13 additions & 0 deletions Zend/zend_opcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,19 @@ ZEND_API int pass_two(zend_op_array *op_array)
opline->opcode = ZEND_GENERATOR_RETURN;
}
break;
case ZEND_SWITCH_LONG:
case ZEND_SWITCH_STRING:
{
/* absolute indexes to relative offsets */
HashTable *jumptable = Z_ARRVAL_P(CT_CONSTANT(opline->op2));
zval *zv;
ZEND_HASH_FOREACH_VAL(jumptable, zv) {
Z_LVAL_P(zv) = ZEND_OPLINE_NUM_TO_OFFSET(op_array, opline, Z_LVAL_P(zv));
} ZEND_HASH_FOREACH_END();

opline->extended_value = ZEND_OPLINE_NUM_TO_OFFSET(op_array, opline, opline->extended_value);
break;
}
}
if (opline->op1_type == IS_CONST) {
ZEND_PASS_TWO_UPDATE_CONSTANT(op_array, opline->op1);
Expand Down
58 changes: 58 additions & 0 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -8043,6 +8043,64 @@ ZEND_VM_HANDLER(51, ZEND_MAKE_REF, VAR|CV, UNUSED)
ZEND_VM_NEXT_OPCODE();
}

ZEND_VM_HANDLER(187, ZEND_SWITCH_LONG, CONST|TMPVAR|CV, CONST, JMP_ADDR)
{
USE_OPLINE
zend_free_op free_op1, free_op2;
zval *op, *jump_zv;
HashTable *jumptable;

op = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
jumptable = Z_ARRVAL_P(GET_OP2_ZVAL_PTR(BP_VAR_R));

if (Z_TYPE_P(op) != IS_LONG) {
ZVAL_DEREF(op);
if (Z_TYPE_P(op) != IS_LONG) {
/* Wrong type, fall back to ZEND_CASE chain */
ZEND_VM_NEXT_OPCODE();
}
}

jump_zv = zend_hash_index_find(jumptable, Z_LVAL_P(op));
if (jump_zv != NULL) {
ZEND_VM_SET_RELATIVE_OPCODE(opline, Z_LVAL_P(jump_zv));
ZEND_VM_CONTINUE();
} else {
/* default */
ZEND_VM_SET_RELATIVE_OPCODE(opline, opline->extended_value);
ZEND_VM_CONTINUE();
}
}

ZEND_VM_HANDLER(188, ZEND_SWITCH_STRING, CONST|TMPVAR|CV, CONST, JMP_ADDR)
{
USE_OPLINE
zend_free_op free_op1, free_op2;
zval *op, *jump_zv;
HashTable *jumptable;

op = GET_OP1_ZVAL_PTR_UNDEF(BP_VAR_R);
jumptable = Z_ARRVAL_P(GET_OP2_ZVAL_PTR(BP_VAR_R));

if (Z_TYPE_P(op) != IS_STRING) {
ZVAL_DEREF(op);
if (Z_TYPE_P(op) != IS_STRING) {
/* Wrong type, fall back to ZEND_CASE chain */
ZEND_VM_NEXT_OPCODE();
}
}

jump_zv = zend_hash_find(jumptable, Z_STR_P(op));
if (jump_zv != NULL) {
ZEND_VM_SET_RELATIVE_OPCODE(opline, Z_LVAL_P(jump_zv));
ZEND_VM_CONTINUE();
} else {
/* default */
ZEND_VM_SET_RELATIVE_OPCODE(opline, opline->extended_value);
ZEND_VM_CONTINUE();
}
}

ZEND_VM_TYPE_SPEC_HANDLER(ZEND_ADD, (res_info == MAY_BE_LONG && op1_info == MAY_BE_LONG && op2_info == MAY_BE_LONG), ZEND_ADD_LONG_NO_OVERFLOW, CONST|TMPVARCV, CONST|TMPVARCV, SPEC(NO_CONST_CONST,COMMUTATIVE))
{
USE_OPLINE
Expand Down
Loading