Skip to content

Commit 9fc7a73

Browse files
committed
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.
1 parent ccc1ac8 commit 9fc7a73

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed

Zend/zend_compile.c

Lines changed: 167 additions & 0 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+
if (args->children < 1) {
4722+
return FAILURE;
4723+
}
4724+
4725+
zend_eval_const_expr(&args->child[0]);
4726+
if (args->child[0]->kind != ZEND_AST_ZVAL) {
4727+
return FAILURE;
4728+
}
4729+
4730+
zval *format_string = zend_ast_get_zval(args->child[0]);
4731+
if (Z_TYPE_P(format_string) != IS_STRING) {
4732+
return FAILURE;
4733+
}
4734+
if (Z_STRLEN_P(format_string) >= 256) {
4735+
return FAILURE;
4736+
}
4737+
4738+
char *p;
4739+
char *end;
4740+
uint32_t string_placeholder_count;
4741+
4742+
string_placeholder_count = 0;
4743+
p = Z_STRVAL_P(format_string);
4744+
end = p + Z_STRLEN_P(format_string);
4745+
4746+
for (;;) {
4747+
p = memchr(p, '%', end - p);
4748+
if (!p) {
4749+
break;
4750+
}
4751+
4752+
char *q = p + 1;
4753+
if (q == end) {
4754+
return FAILURE;
4755+
}
4756+
4757+
switch (*q) {
4758+
case 's':
4759+
string_placeholder_count++;
4760+
break;
4761+
case '%':
4762+
break;
4763+
default:
4764+
return FAILURE;
4765+
}
4766+
4767+
p = q;
4768+
p++;
4769+
}
4770+
4771+
/* Bail out if the number of placeholders does not match the number of values. */
4772+
if (string_placeholder_count != (args->children - 1)) {
4773+
return FAILURE;
4774+
}
4775+
4776+
znode *elements = NULL;
4777+
4778+
if (string_placeholder_count > 0) {
4779+
elements = safe_emalloc(sizeof(*elements), string_placeholder_count, 0);
4780+
}
4781+
4782+
/* Compile the value expressions first for error handling that is consistent
4783+
* with a function call: Values that fail to convert to a string may emit errors.
4784+
*/
4785+
for (uint32_t i = 0; i < string_placeholder_count; i++) {
4786+
zend_compile_expr(elements + i, args->child[1 + i]);
4787+
if (elements[i].op_type == IS_CONST) {
4788+
if (Z_TYPE(elements[i].u.constant) != IS_ARRAY) {
4789+
convert_to_string(&elements[i].u.constant);
4790+
}
4791+
}
4792+
}
4793+
4794+
uint32_t rope_elements = 0;
4795+
uint32_t rope_init_lineno = -1;
4796+
zend_op *opline = NULL;
4797+
4798+
string_placeholder_count = 0;
4799+
p = Z_STRVAL_P(format_string);
4800+
end = p + Z_STRLEN_P(format_string);
4801+
char *offset = p;
4802+
for (;;) {
4803+
p = memchr(p, '%', end - p);
4804+
if (!p) {
4805+
break;
4806+
}
4807+
4808+
char *q = p + 1;
4809+
ZEND_ASSERT(q < end);
4810+
ZEND_ASSERT(*q == 's' || *q == '%');
4811+
4812+
if (*q == '%') {
4813+
/* Optimization to not create a dedicated rope element for the literal '%':
4814+
* Include the first '%' within the "constant" part instead of dropping the
4815+
* full placeholder.
4816+
*/
4817+
p++;
4818+
}
4819+
4820+
if (p != offset) {
4821+
znode const_node;
4822+
const_node.op_type = IS_CONST;
4823+
ZVAL_STRINGL(&const_node.u.constant, offset, p - offset);
4824+
if (rope_elements == 0) {
4825+
rope_init_lineno = get_next_op_number();
4826+
}
4827+
opline = zend_compile_rope_add(result, rope_elements++, &const_node);
4828+
}
4829+
4830+
if (*q == 's') {
4831+
/* Perform the cast of constant arrays when actually evaluating corresponding placeholder
4832+
* for correct error reporting.
4833+
*/
4834+
if (elements[string_placeholder_count].op_type == IS_CONST) {
4835+
if (Z_TYPE(elements[string_placeholder_count].u.constant) == IS_ARRAY) {
4836+
zend_emit_op_tmp(&elements[string_placeholder_count], ZEND_CAST, &elements[string_placeholder_count], NULL)->extended_value = IS_STRING;
4837+
}
4838+
}
4839+
if (rope_elements == 0) {
4840+
rope_init_lineno = get_next_op_number();
4841+
}
4842+
opline = zend_compile_rope_add(result, rope_elements++, &elements[string_placeholder_count]);
4843+
4844+
string_placeholder_count++;
4845+
}
4846+
4847+
p = q;
4848+
p++;
4849+
offset = p;
4850+
}
4851+
if (end != offset) {
4852+
/* Add the constant part after the last placeholder. */
4853+
znode const_node;
4854+
const_node.op_type = IS_CONST;
4855+
ZVAL_STRINGL(&const_node.u.constant, offset, end - offset);
4856+
if (rope_elements == 0) {
4857+
rope_init_lineno = get_next_op_number();
4858+
}
4859+
opline = zend_compile_rope_add(result, rope_elements++, &const_node);
4860+
}
4861+
if (rope_elements == 0) {
4862+
/* Handle empty format strings. */
4863+
znode const_node;
4864+
const_node.op_type = IS_CONST;
4865+
ZVAL_EMPTY_STRING(&const_node.u.constant);
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
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
--TEST--
2+
Test sprintf() function : Rope Optimization
3+
--FILE--
4+
<?php
5+
function func($str) {
6+
return strtoupper($str);
7+
}
8+
function sideeffect() {
9+
echo "Called!\n";
10+
return "foo";
11+
}
12+
class Foo {
13+
public function __construct() {
14+
echo "Called\n";
15+
}
16+
}
17+
18+
$a = "foo";
19+
$b = "bar";
20+
$c = new stdClass();
21+
22+
try {
23+
var_dump(sprintf("const"));
24+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
25+
26+
try {
27+
var_dump(sprintf("%s", $a));
28+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
29+
30+
try {
31+
var_dump(sprintf("%s/%s", $a, $b));
32+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
33+
34+
try {
35+
var_dump(sprintf("%s/%s/%s", $a, $b));
36+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
37+
38+
try {
39+
var_dump(sprintf("%s/%s/%s", $a, $b, $c));
40+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
41+
42+
try {
43+
var_dump(sprintf("%s/", func("baz")));
44+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
45+
46+
try {
47+
var_dump(sprintf("/%s", func("baz")));
48+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
49+
50+
try {
51+
var_dump(sprintf("/%s/", func("baz")));
52+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
53+
54+
try {
55+
var_dump(sprintf("%s%s%s%s", $a, $b, func("baz"), $a));
56+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
57+
58+
try {
59+
var_dump(sprintf("%s/%s", sprintf("%s:%s", $a, $b), sprintf("%s-%s", func('baz'), func('baz'))));
60+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
61+
62+
try {
63+
var_dump(sprintf(sideeffect()));
64+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
65+
66+
try {
67+
var_dump(sprintf("%s-%s-%s", __FILE__, __LINE__, 1));
68+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
69+
70+
try {
71+
$values = range('a', 'z');
72+
var_dump(sprintf("%s%s%s", "{$values[0]}{$values[1]}{$values[2]}", "{$values[3]}{$values[4]}{$values[5]}", "{$values[6]}{$values[7]}{$values[8]}"));
73+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
74+
75+
try {
76+
var_dump(sprintf("%s%s%s", new Foo(), new Foo(), new Foo(), ));
77+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
78+
79+
try {
80+
var_dump(sprintf(...));
81+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
82+
83+
try {
84+
var_dump(sprintf('%%s'));
85+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
86+
87+
try {
88+
var_dump(sprintf('%%s', 'test'));
89+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
90+
91+
try {
92+
var_dump(sprintf('%s-%s-%s', [], [], []));
93+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
94+
95+
try {
96+
var_dump(sprintf(""));
97+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
98+
99+
echo "Done";
100+
?>
101+
--EXPECTF--
102+
string(5) "const"
103+
104+
string(3) "foo"
105+
106+
string(7) "foo/bar"
107+
108+
ArgumentCountError: 4 arguments are required, 3 given in %s:32
109+
Stack trace:
110+
#0 %s(32): sprintf('%s/%s/%s', 'foo', 'bar')
111+
#1 {main}
112+
113+
Error: Object of class stdClass could not be converted to string in %s:36
114+
Stack trace:
115+
#0 {main}
116+
117+
string(4) "BAZ/"
118+
119+
string(4) "/BAZ"
120+
121+
string(5) "/BAZ/"
122+
123+
string(12) "foobarBAZfoo"
124+
125+
string(15) "foo:bar/BAZ-BAZ"
126+
127+
Called!
128+
string(3) "foo"
129+
130+
string(%d) "%ssprintf_rope_optimization_001.php-%d-1"
131+
132+
string(9) "abcdefghi"
133+
134+
Called
135+
Called
136+
Called
137+
Error: Object of class Foo could not be converted to string in %s:73
138+
Stack trace:
139+
#0 {main}
140+
141+
object(Closure)#3 (2) {
142+
["function"]=>
143+
string(7) "sprintf"
144+
["parameter"]=>
145+
array(2) {
146+
["$format"]=>
147+
string(10) "<required>"
148+
["$values"]=>
149+
string(10) "<optional>"
150+
}
151+
}
152+
153+
string(2) "%s"
154+
155+
string(2) "%s"
156+
157+
158+
Warning: Array to string conversion in %s on line 89
159+
160+
Warning: Array to string conversion in %s on line 89
161+
162+
Warning: Array to string conversion in %s on line 89
163+
string(17) "Array-Array-Array"
164+
165+
string(0) ""
166+
167+
Done
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--TEST--
2+
Test sprintf() function : Rope Optimization with a throwing error handler.
3+
--FILE--
4+
<?php
5+
6+
function exception_error_handler(int $errno, string $errstr, ?string $errfile, int $errline) {
7+
if (!(error_reporting() & $errno)) {
8+
// This error code is not included in error_reporting
9+
return;
10+
}
11+
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
12+
}
13+
set_error_handler(exception_error_handler(...));
14+
15+
try {
16+
var_dump(sprintf("%s-%s", new stdClass(), []));
17+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
18+
19+
echo "Done";
20+
?>
21+
--EXPECTF--
22+
Error: Object of class stdClass could not be converted to string in %s:13
23+
Stack trace:
24+
#0 {main}
25+
26+
Done

0 commit comments

Comments
 (0)