Skip to content

Commit 26c96d3

Browse files
realFlowControlarnaud-lb
authored andcommitted
re-use sprintf() optimisation for printf()
Closes phpGH-19658
1 parent a7fde28 commit 26c96d3

File tree

4 files changed

+70
-2
lines changed

4 files changed

+70
-2
lines changed

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,10 @@ PHP 8.6 UPGRADE NOTES
9999
14. Performance Improvements
100100
========================================
101101

102+
- Core:
103+
. `printf()` using only `%s` and `%d` will be compiled into the equivalent
104+
string interpolation, avoiding the overhead of a function call and repeatedly
105+
parsing the format string.
106+
102107
- JSON:
103108
. Improve performance of encoding arrays and objects.

Zend/Optimizer/dce.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ static inline bool may_have_side_effects(
124124
case ZEND_FUNC_NUM_ARGS:
125125
case ZEND_FUNC_GET_ARGS:
126126
case ZEND_ARRAY_KEY_EXISTS:
127+
case ZEND_COPY_TMP:
127128
/* No side effects */
128129
return false;
129130
case ZEND_FREE:
@@ -425,10 +426,12 @@ static bool dce_instr(const context *ctx, zend_op *opline, zend_ssa_op *ssa_op)
425426
return false;
426427
}
427428

428-
if ((opline->op1_type & (IS_VAR|IS_TMP_VAR))&& !is_var_dead(ctx, ssa_op->op1_use)) {
429+
if ((opline->op1_type & (IS_VAR|IS_TMP_VAR)) && !is_var_dead(ctx, ssa_op->op1_use)) {
429430
if (!try_remove_var_def(ctx, ssa_op->op1_use, ssa_op->op1_use_chain, opline)) {
430431
if (may_be_refcounted(ssa->var_info[ssa_op->op1_use].type)
431-
&& opline->opcode != ZEND_CASE && opline->opcode != ZEND_CASE_STRICT) {
432+
&& opline->opcode != ZEND_CASE
433+
&& opline->opcode != ZEND_CASE_STRICT
434+
&& opline->opcode != ZEND_COPY_TMP) {
432435
free_var = ssa_op->op1_use;
433436
free_var_type = opline->op1_type;
434437
}

Zend/Optimizer/pass1.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,16 @@ void zend_optimizer_pass1(zend_op_array *op_array, zend_optimizer_ctx *ctx)
264264
collect_constants = false;
265265
break;
266266
}
267+
case ZEND_DO_UCALL:
268+
case ZEND_DO_FCALL:
269+
case ZEND_DO_FCALL_BY_NAME:
270+
case ZEND_FRAMELESS_ICALL_0:
271+
case ZEND_FRAMELESS_ICALL_1:
272+
case ZEND_FRAMELESS_ICALL_2:
273+
case ZEND_FRAMELESS_ICALL_3:
274+
/* don't collect constants after any UCALL/FCALL/FRAMELESS ICALL */
275+
collect_constants = 0;
276+
break;
267277
case ZEND_STRLEN:
268278
if (opline->op1_type == IS_CONST &&
269279
zend_optimizer_eval_strlen(&result, &ZEND_OP1_LITERAL(opline)) == SUCCESS) {

Zend/zend_compile.c

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "zend_API.h"
2929
#include "zend_exceptions.h"
3030
#include "zend_interfaces.h"
31+
#include "zend_types.h"
3132
#include "zend_virtual_cwd.h"
3233
#include "zend_multibyte.h"
3334
#include "zend_language_scanner.h"
@@ -4968,6 +4969,53 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args)
49684969
return SUCCESS;
49694970
}
49704971

4972+
static zend_result zend_compile_func_printf(znode *result, zend_ast_list *args) /* {{{ */
4973+
{
4974+
/* Special case: printf with a single constant string argument and no format specifiers.
4975+
* In this case, just emit ECHO and return the string length if needed. */
4976+
if (args->children == 1) {
4977+
zend_eval_const_expr(&args->child[0]);
4978+
if (args->child[0]->kind != ZEND_AST_ZVAL) {
4979+
return FAILURE;
4980+
}
4981+
zval *format_string = zend_ast_get_zval(args->child[0]);
4982+
if (Z_TYPE_P(format_string) != IS_STRING) {
4983+
return FAILURE;
4984+
}
4985+
/* Check if there are any format specifiers */
4986+
if (!memchr(Z_STRVAL_P(format_string), '%', Z_STRLEN_P(format_string))) {
4987+
/* No format specifiers - just emit ECHO and return string length */
4988+
znode format_node;
4989+
zend_compile_expr(&format_node, args->child[0]);
4990+
zend_emit_op(NULL, ZEND_ECHO, &format_node, NULL);
4991+
4992+
/* Return the string length as a constant if the result is used */
4993+
result->op_type = IS_CONST;
4994+
ZVAL_LONG(&result->u.constant, Z_STRLEN_P(format_string));
4995+
return SUCCESS;
4996+
}
4997+
}
4998+
4999+
/* Fall back to sprintf optimization for format strings with specifiers */
5000+
znode rope_result;
5001+
if (zend_compile_func_sprintf(&rope_result, args) != SUCCESS) {
5002+
return FAILURE;
5003+
}
5004+
5005+
/* printf() returns the amount of bytes written, so just an ECHO of the
5006+
* resulting sprintf() optimisation might not be enough. At this early
5007+
* stage we can't detect if the result is actually used, so we just emit
5008+
* the opcodes and let them be cleaned up by the dead code elimination
5009+
* pass in the Zend Optimizer if the result of the printf() is in fact
5010+
* unused */
5011+
znode copy;
5012+
zend_emit_op_tmp(&copy, ZEND_COPY_TMP, &rope_result, NULL);
5013+
zend_emit_op(NULL, ZEND_ECHO, &rope_result, NULL);
5014+
zend_emit_op_tmp(result, ZEND_STRLEN, &copy, NULL);
5015+
5016+
return SUCCESS;
5017+
}
5018+
49715019
static zend_result zend_compile_func_clone(znode *result, zend_ast_list *args)
49725020
{
49735021
znode arg_node;
@@ -5050,6 +5098,8 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *
50505098
return zend_compile_func_array_key_exists(result, args);
50515099
} else if (zend_string_equals_literal(lcname, "sprintf")) {
50525100
return zend_compile_func_sprintf(result, args);
5101+
} else if (zend_string_equals_literal(lcname, "printf")) {
5102+
return zend_compile_func_printf(result, args);
50535103
} else if (zend_string_equals(lcname, ZSTR_KNOWN(ZEND_STR_CLONE))) {
50545104
return zend_compile_func_clone(result, args);
50555105
} else {

0 commit comments

Comments
 (0)