Skip to content

Commit 1e7aac3

Browse files
TimWollailuuu1994
andauthored
zend_compile: Optimize sprintf() into a rope (#14546)
* zend_compile: Add `zend_compile_rope_finalize()` This just extracts the implementation as-is into a dedicated function to make it reusable in preparation of a future commit. * zend_compile: Use clearer parameter names for `zend_compile_rope_finalize()` * zend_compile: Fix `zend_compile_rope_finalize()` for ropes containing a single constant string Without this Opcache will trigger a use-after-free in `zend_optimizer_compact_literals()`. Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com> * zend_compile: Optimize `sprintf()` into a rope This optimization will compile `sprintf()` using only `%s` placeholders into a rope at compile time, effectively making those calls equivalent to the use of string interpolation, with the added benefit of supporting arbitrary expressions instead of just expressions starting with a `$`. For a synthetic test using: <?php $a = 'foo'; $b = 'bar'; for ($i = 0; $i < 100_000_000; $i++) { sprintf("%s-%s", $a, $b); } This optimization yields a 2.1× performance improvement: $ hyperfine 'sapi/cli/php -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php' \ '/tmp/unoptimized -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php' Benchmark 1: sapi/cli/php -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php Time (mean ± σ): 1.869 s ± 0.033 s [User: 1.865 s, System: 0.003 s] Range (min … max): 1.840 s … 1.945 s 10 runs Benchmark 2: /tmp/unoptimized -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php Time (mean ± σ): 4.011 s ± 0.034 s [User: 4.006 s, System: 0.005 s] Range (min … max): 3.964 s … 4.079 s 10 runs Summary sapi/cli/php -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php ran 2.15 ± 0.04 times faster than /tmp/unoptimized -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php This optimization comes with a small and probably insignificant behavioral change: If one of the values cannot be (cleanly) converted to a string, for example when attempting to insert an object that is not `Stringable`, the resulting Exception will naturally not show the `sprintf()` call in the resulting stack trace, because there is no call to `sprintf()`. Nevertheless it will correctly point out the line of the `sprintf()` call as the source of the Exception, pointing the user towards the correct location. * zend_compile: Eagerly handle empty format strings in `sprintf()` optimization * zend_compile: Add additional explanatory comments to zend_compile_func_sprintf() * Add zero-argument test to sprintf_rope_optimization_001.phpt --------- Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
1 parent 18cfd94 commit 1e7aac3

File tree

3 files changed

+423
-47
lines changed

3 files changed

+423
-47
lines changed

Zend/zend_compile.c

Lines changed: 221 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4712,6 +4712,171 @@ static void zend_compile_ns_call(znode *result, znode *name_node, zend_ast *args
47124712
}
47134713
/* }}} */
47144714

4715+
static zend_op *zend_compile_rope_add(znode *result, uint32_t num, znode *elem_node);
4716+
static zend_op *zend_compile_rope_add_ex(zend_op *opline, znode *result, uint32_t num, znode *elem_node);
4717+
static void zend_compile_rope_finalize(znode *result, uint32_t j, zend_op *init_opline, zend_op *opline);
4718+
4719+
static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args) /* {{{ */
4720+
{
4721+
/* Bail out if we do not have a format string. */
4722+
if (args->children < 1) {
4723+
return FAILURE;
4724+
}
4725+
4726+
zend_eval_const_expr(&args->child[0]);
4727+
/* Bail out if the format string is not constant. */
4728+
if (args->child[0]->kind != ZEND_AST_ZVAL) {
4729+
return FAILURE;
4730+
}
4731+
4732+
zval *format_string = zend_ast_get_zval(args->child[0]);
4733+
if (Z_TYPE_P(format_string) != IS_STRING) {
4734+
return FAILURE;
4735+
}
4736+
if (Z_STRLEN_P(format_string) >= 256) {
4737+
return FAILURE;
4738+
}
4739+
4740+
char *p;
4741+
char *end;
4742+
uint32_t string_placeholder_count;
4743+
4744+
string_placeholder_count = 0;
4745+
p = Z_STRVAL_P(format_string);
4746+
end = p + Z_STRLEN_P(format_string);
4747+
4748+
for (;;) {
4749+
p = memchr(p, '%', end - p);
4750+
if (!p) {
4751+
break;
4752+
}
4753+
4754+
char *q = p + 1;
4755+
if (q == end) {
4756+
return FAILURE;
4757+
}
4758+
4759+
switch (*q) {
4760+
case 's':
4761+
string_placeholder_count++;
4762+
break;
4763+
case '%':
4764+
break;
4765+
default:
4766+
return FAILURE;
4767+
}
4768+
4769+
p = q;
4770+
p++;
4771+
}
4772+
4773+
/* Bail out if the number of placeholders does not match the number of values. */
4774+
if (string_placeholder_count != (args->children - 1)) {
4775+
return FAILURE;
4776+
}
4777+
4778+
/* Handle empty format strings. */
4779+
if (Z_STRLEN_P(format_string) == 0) {
4780+
result->op_type = IS_CONST;
4781+
ZVAL_EMPTY_STRING(&result->u.constant);
4782+
4783+
return SUCCESS;
4784+
}
4785+
4786+
znode *elements = NULL;
4787+
4788+
if (string_placeholder_count > 0) {
4789+
elements = safe_emalloc(sizeof(*elements), string_placeholder_count, 0);
4790+
}
4791+
4792+
/* Compile the value expressions first for error handling that is consistent
4793+
* with a function call: Values that fail to convert to a string may emit errors.
4794+
*/
4795+
for (uint32_t i = 0; i < string_placeholder_count; i++) {
4796+
zend_compile_expr(elements + i, args->child[1 + i]);
4797+
if (elements[i].op_type == IS_CONST) {
4798+
if (Z_TYPE(elements[i].u.constant) != IS_ARRAY) {
4799+
convert_to_string(&elements[i].u.constant);
4800+
}
4801+
}
4802+
}
4803+
4804+
uint32_t rope_elements = 0;
4805+
uint32_t rope_init_lineno = -1;
4806+
zend_op *opline = NULL;
4807+
4808+
string_placeholder_count = 0;
4809+
p = Z_STRVAL_P(format_string);
4810+
end = p + Z_STRLEN_P(format_string);
4811+
char *offset = p;
4812+
for (;;) {
4813+
p = memchr(p, '%', end - p);
4814+
if (!p) {
4815+
break;
4816+
}
4817+
4818+
char *q = p + 1;
4819+
ZEND_ASSERT(q < end);
4820+
ZEND_ASSERT(*q == 's' || *q == '%');
4821+
4822+
if (*q == '%') {
4823+
/* Optimization to not create a dedicated rope element for the literal '%':
4824+
* Include the first '%' within the "constant" part instead of dropping the
4825+
* full placeholder.
4826+
*/
4827+
p++;
4828+
}
4829+
4830+
if (p != offset) {
4831+
znode const_node;
4832+
const_node.op_type = IS_CONST;
4833+
ZVAL_STRINGL(&const_node.u.constant, offset, p - offset);
4834+
if (rope_elements == 0) {
4835+
rope_init_lineno = get_next_op_number();
4836+
}
4837+
opline = zend_compile_rope_add(result, rope_elements++, &const_node);
4838+
}
4839+
4840+
if (*q == 's') {
4841+
/* Perform the cast of constant arrays when actually evaluating corresponding placeholder
4842+
* for correct error reporting.
4843+
*/
4844+
if (elements[string_placeholder_count].op_type == IS_CONST) {
4845+
if (Z_TYPE(elements[string_placeholder_count].u.constant) == IS_ARRAY) {
4846+
zend_emit_op_tmp(&elements[string_placeholder_count], ZEND_CAST, &elements[string_placeholder_count], NULL)->extended_value = IS_STRING;
4847+
}
4848+
}
4849+
if (rope_elements == 0) {
4850+
rope_init_lineno = get_next_op_number();
4851+
}
4852+
opline = zend_compile_rope_add(result, rope_elements++, &elements[string_placeholder_count]);
4853+
4854+
string_placeholder_count++;
4855+
}
4856+
4857+
p = q;
4858+
p++;
4859+
offset = p;
4860+
}
4861+
if (end != offset) {
4862+
/* Add the constant part after the last placeholder. */
4863+
znode const_node;
4864+
const_node.op_type = IS_CONST;
4865+
ZVAL_STRINGL(&const_node.u.constant, offset, end - offset);
4866+
if (rope_elements == 0) {
4867+
rope_init_lineno = get_next_op_number();
4868+
}
4869+
opline = zend_compile_rope_add(result, rope_elements++, &const_node);
4870+
}
4871+
ZEND_ASSERT(opline != NULL);
4872+
4873+
zend_op *init_opline = CG(active_op_array)->opcodes + rope_init_lineno;
4874+
zend_compile_rope_finalize(result, rope_elements, init_opline, opline);
4875+
efree(elements);
4876+
4877+
return SUCCESS;
4878+
}
4879+
47154880
static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, zend_function *fbc, uint32_t type) /* {{{ */
47164881
{
47174882
if (zend_string_equals_literal(lcname, "strlen")) {
@@ -4778,6 +4943,8 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *
47784943
return zend_compile_func_array_slice(result, args);
47794944
} else if (zend_string_equals_literal(lcname, "array_key_exists")) {
47804945
return zend_compile_func_array_key_exists(result, args);
4946+
} else if (zend_string_equals_literal(lcname, "sprintf")) {
4947+
return zend_compile_func_sprintf(result, args);
47814948
} else {
47824949
return FAILURE;
47834950
}
@@ -10188,6 +10355,59 @@ static zend_op *zend_compile_rope_add(znode *result, uint32_t num, znode *elem_n
1018810355
}
1018910356
/* }}} */
1019010357

10358+
static void zend_compile_rope_finalize(znode *result, uint32_t rope_elements, zend_op *init_opline, zend_op *opline)
10359+
{
10360+
if (rope_elements == 1) {
10361+
if (opline->op2_type == IS_CONST) {
10362+
GET_NODE(result, opline->op2);
10363+
ZVAL_UNDEF(CT_CONSTANT(opline->op2));
10364+
SET_UNUSED(opline->op2);
10365+
MAKE_NOP(opline);
10366+
} else {
10367+
opline->opcode = ZEND_CAST;
10368+
opline->extended_value = IS_STRING;
10369+
opline->op1_type = opline->op2_type;
10370+
opline->op1 = opline->op2;
10371+
SET_UNUSED(opline->op2);
10372+
zend_make_tmp_result(result, opline);
10373+
}
10374+
} else if (rope_elements == 2) {
10375+
opline->opcode = ZEND_FAST_CONCAT;
10376+
opline->extended_value = 0;
10377+
opline->op1_type = init_opline->op2_type;
10378+
opline->op1 = init_opline->op2;
10379+
zend_make_tmp_result(result, opline);
10380+
MAKE_NOP(init_opline);
10381+
} else {
10382+
uint32_t var;
10383+
10384+
init_opline->extended_value = rope_elements;
10385+
opline->opcode = ZEND_ROPE_END;
10386+
zend_make_tmp_result(result, opline);
10387+
var = opline->op1.var = get_temporary_variable();
10388+
10389+
/* Allocates the necessary number of zval slots to keep the rope */
10390+
uint32_t i = ((rope_elements * sizeof(zend_string*)) + (sizeof(zval) - 1)) / sizeof(zval);
10391+
while (i > 1) {
10392+
get_temporary_variable();
10393+
i--;
10394+
}
10395+
10396+
/* Update all the previous opcodes to use the same variable */
10397+
while (opline != init_opline) {
10398+
opline--;
10399+
if (opline->opcode == ZEND_ROPE_ADD &&
10400+
opline->result.var == (uint32_t)-1) {
10401+
opline->op1.var = var;
10402+
opline->result.var = var;
10403+
} else if (opline->opcode == ZEND_ROPE_INIT &&
10404+
opline->result.var == (uint32_t)-1) {
10405+
opline->result.var = var;
10406+
}
10407+
}
10408+
}
10409+
}
10410+
1019110411
static void zend_compile_encaps_list(znode *result, zend_ast *ast) /* {{{ */
1019210412
{
1019310413
uint32_t i, j;
@@ -10263,53 +10483,7 @@ static void zend_compile_encaps_list(znode *result, zend_ast *ast) /* {{{ */
1026310483
opline = zend_compile_rope_add_ex(opline, result, j++, &last_const_node);
1026410484
}
1026510485
init_opline = CG(active_op_array)->opcodes + rope_init_lineno;
10266-
if (j == 1) {
10267-
if (opline->op2_type == IS_CONST) {
10268-
GET_NODE(result, opline->op2);
10269-
MAKE_NOP(opline);
10270-
} else {
10271-
opline->opcode = ZEND_CAST;
10272-
opline->extended_value = IS_STRING;
10273-
opline->op1_type = opline->op2_type;
10274-
opline->op1 = opline->op2;
10275-
SET_UNUSED(opline->op2);
10276-
zend_make_tmp_result(result, opline);
10277-
}
10278-
} else if (j == 2) {
10279-
opline->opcode = ZEND_FAST_CONCAT;
10280-
opline->extended_value = 0;
10281-
opline->op1_type = init_opline->op2_type;
10282-
opline->op1 = init_opline->op2;
10283-
zend_make_tmp_result(result, opline);
10284-
MAKE_NOP(init_opline);
10285-
} else {
10286-
uint32_t var;
10287-
10288-
init_opline->extended_value = j;
10289-
opline->opcode = ZEND_ROPE_END;
10290-
zend_make_tmp_result(result, opline);
10291-
var = opline->op1.var = get_temporary_variable();
10292-
10293-
/* Allocates the necessary number of zval slots to keep the rope */
10294-
i = ((j * sizeof(zend_string*)) + (sizeof(zval) - 1)) / sizeof(zval);
10295-
while (i > 1) {
10296-
get_temporary_variable();
10297-
i--;
10298-
}
10299-
10300-
/* Update all the previous opcodes to use the same variable */
10301-
while (opline != init_opline) {
10302-
opline--;
10303-
if (opline->opcode == ZEND_ROPE_ADD &&
10304-
opline->result.var == (uint32_t)-1) {
10305-
opline->op1.var = var;
10306-
opline->result.var = var;
10307-
} else if (opline->opcode == ZEND_ROPE_INIT &&
10308-
opline->result.var == (uint32_t)-1) {
10309-
opline->result.var = var;
10310-
}
10311-
}
10312-
}
10486+
zend_compile_rope_finalize(result, j, init_opline, opline);
1031310487
}
1031410488
/* }}} */
1031510489

0 commit comments

Comments
 (0)