Skip to content

Commit 2c5ed50

Browse files
authored
zend_compile: Add support for %d to sprintf() optimization (#14561)
* zend_compile: Rename `string_placeholder_count` to `placeholder_count` in `zend_compile_func_sprintf()` This is intended to make the diff of a follow-up commit smaller. * zend_compile: Add support for `%d` to `sprintf()` optimization This extends the existing `sprintf()` optimization by support for the `%d` placeholder, which effectively equivalent to an `(int)` cast followed by a `(string)` cast. For a synthetic test using: <?php $a = 'foo'; $b = 42; for ($i = 0; $i < 100_000_000; $i++) { sprintf("%s-%d", $a, $b); } This optimization yields a 1.3× 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 ± σ): 3.296 s ± 0.094 s [User: 3.287 s, System: 0.005 s] Range (min … max): 3.213 s … 3.527 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.300 s ± 0.025 s [User: 4.290 s, System: 0.007 s] Range (min … max): 4.266 s … 4.334 s 10 runs Summary sapi/cli/php -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php ran 1.30 ± 0.04 times faster than /tmp/unoptimized -d zend_extension=php-src/modules/opcache.so -d opcache.enable_cli=1 test.php * Fix sprintf_rope_optimization_003.phpt test expecation for 32-bit integers * zend_compile: Indent switch-case labels in zend_compile_func_sprintf() * Add GMP test to sprintf() rope optimization * Add `%s` test case to sprintf() GMP test
1 parent 9d3907f commit 2c5ed50

File tree

4 files changed

+198
-30
lines changed

4 files changed

+198
-30
lines changed

Zend/zend_compile.c

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4739,9 +4739,9 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args)
47394739

47404740
char *p;
47414741
char *end;
4742-
uint32_t string_placeholder_count;
4742+
uint32_t placeholder_count;
47434743

4744-
string_placeholder_count = 0;
4744+
placeholder_count = 0;
47454745
p = Z_STRVAL_P(format_string);
47464746
end = p + Z_STRLEN_P(format_string);
47474747

@@ -4757,21 +4757,22 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args)
47574757
}
47584758

47594759
switch (*q) {
4760-
case 's':
4761-
string_placeholder_count++;
4762-
break;
4763-
case '%':
4764-
break;
4765-
default:
4766-
return FAILURE;
4760+
case 's':
4761+
case 'd':
4762+
placeholder_count++;
4763+
break;
4764+
case '%':
4765+
break;
4766+
default:
4767+
return FAILURE;
47674768
}
47684769

47694770
p = q;
47704771
p++;
47714772
}
47724773

47734774
/* Bail out if the number of placeholders does not match the number of values. */
4774-
if (string_placeholder_count != (args->children - 1)) {
4775+
if (placeholder_count != (args->children - 1)) {
47754776
return FAILURE;
47764777
}
47774778

@@ -4785,27 +4786,22 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args)
47854786

47864787
znode *elements = NULL;
47874788

4788-
if (string_placeholder_count > 0) {
4789-
elements = safe_emalloc(sizeof(*elements), string_placeholder_count, 0);
4789+
if (placeholder_count > 0) {
4790+
elements = safe_emalloc(sizeof(*elements), placeholder_count, 0);
47904791
}
47914792

47924793
/* Compile the value expressions first for error handling that is consistent
47934794
* with a function call: Values that fail to convert to a string may emit errors.
47944795
*/
4795-
for (uint32_t i = 0; i < string_placeholder_count; i++) {
4796+
for (uint32_t i = 0; i < placeholder_count; i++) {
47964797
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-
}
48024798
}
48034799

48044800
uint32_t rope_elements = 0;
48054801
uint32_t rope_init_lineno = -1;
48064802
zend_op *opline = NULL;
48074803

4808-
string_placeholder_count = 0;
4804+
placeholder_count = 0;
48094805
p = Z_STRVAL_P(format_string);
48104806
end = p + Z_STRLEN_P(format_string);
48114807
char *offset = p;
@@ -4817,7 +4813,7 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args)
48174813

48184814
char *q = p + 1;
48194815
ZEND_ASSERT(q < end);
4820-
ZEND_ASSERT(*q == 's' || *q == '%');
4816+
ZEND_ASSERT(*q == 's' || *q == 'd' || *q == '%');
48214817

48224818
if (*q == '%') {
48234819
/* Optimization to not create a dedicated rope element for the literal '%':
@@ -4837,21 +4833,32 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args)
48374833
opline = zend_compile_rope_add(result, rope_elements++, &const_node);
48384834
}
48394835

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-
}
4836+
if (*q != '%') {
4837+
switch (*q) {
4838+
case 's':
4839+
/* Perform the cast of constants when actually evaluating the corresponding placeholder
4840+
* for correct error reporting.
4841+
*/
4842+
if (elements[placeholder_count].op_type == IS_CONST) {
4843+
if (Z_TYPE(elements[placeholder_count].u.constant) == IS_ARRAY) {
4844+
zend_emit_op_tmp(&elements[placeholder_count], ZEND_CAST, &elements[placeholder_count], NULL)->extended_value = IS_STRING;
4845+
} else {
4846+
convert_to_string(&elements[placeholder_count].u.constant);
4847+
}
4848+
}
4849+
break;
4850+
case 'd':
4851+
zend_emit_op_tmp(&elements[placeholder_count], ZEND_CAST, &elements[placeholder_count], NULL)->extended_value = IS_LONG;
4852+
break;
4853+
EMPTY_SWITCH_DEFAULT_CASE();
48484854
}
4855+
48494856
if (rope_elements == 0) {
48504857
rope_init_lineno = get_next_op_number();
48514858
}
4852-
opline = zend_compile_rope_add(result, rope_elements++, &elements[string_placeholder_count]);
4859+
opline = zend_compile_rope_add(result, rope_elements++, &elements[placeholder_count]);
48534860

4854-
string_placeholder_count++;
4861+
placeholder_count++;
48554862
}
48564863

48574864
p = q;

ext/standard/tests/strings/sprintf_rope_optimization_001.phpt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ try {
100100
var_dump(sprintf());
101101
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
102102

103+
try {
104+
var_dump(sprintf('%s-%s-%s', true, false, true));
105+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
106+
103107
echo "Done";
104108
?>
105109
--EXPECTF--
@@ -173,4 +177,6 @@ Stack trace:
173177
#0 %s(97): sprintf()
174178
#1 {main}
175179

180+
string(4) "1--1"
181+
176182
Done
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
--TEST--
2+
Test sprintf() function : Rope Optimization for '%d'.
3+
--FILE--
4+
<?php
5+
function func($num) {
6+
return $num + 1;
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 = 42;
19+
$b = -1337;
20+
$c = 3.14;
21+
$d = new stdClass();
22+
23+
try {
24+
var_dump(sprintf("%d", $a));
25+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
26+
27+
try {
28+
var_dump(sprintf("%d/%d", $a, $b));
29+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
30+
31+
try {
32+
var_dump(sprintf("%d/%d/%d", $a, $b, $c));
33+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
34+
35+
try {
36+
var_dump(sprintf("%d/%d/%d/%d", $a, $b, $c, $d));
37+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
38+
39+
try {
40+
var_dump(sprintf("%d/", func(0)));
41+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
42+
43+
try {
44+
var_dump(sprintf("/%d", func(0)));
45+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
46+
47+
try {
48+
var_dump(sprintf("/%d/", func(0)));
49+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
50+
51+
try {
52+
var_dump(sprintf("%d/%d/%d/%d", $a, $b, func(0), $a));
53+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
54+
55+
try {
56+
var_dump(sprintf("%d/%d/%d/%d", __FILE__, __LINE__, 1, M_PI));
57+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
58+
59+
try {
60+
var_dump(sprintf("%d/%d/%d", new Foo(), new Foo(), new Foo(), ));
61+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
62+
63+
try {
64+
var_dump(sprintf('%d/%d/%d', [], [], []));
65+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
66+
67+
try {
68+
if (PHP_INT_SIZE == 8) {
69+
var_dump(sprintf('%d/%d/%d', PHP_INT_MAX, 0, PHP_INT_MIN));
70+
var_dump("2147483647/0/-2147483648");
71+
} else {
72+
var_dump("9223372036854775807/0/-9223372036854775808");
73+
var_dump(sprintf('%d/%d/%d', PHP_INT_MAX, 0, PHP_INT_MIN));
74+
}
75+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
76+
77+
try {
78+
var_dump(sprintf('%d/%d/%d', true, false, true));
79+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
80+
81+
try {
82+
var_dump(sprintf("%d/%d", true, 'foo'));
83+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
84+
85+
try {
86+
var_dump(sprintf("%d", 'foo'));
87+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
88+
89+
echo "Done";
90+
?>
91+
--EXPECTF--
92+
string(2) "42"
93+
94+
string(8) "42/-1337"
95+
96+
string(10) "42/-1337/3"
97+
98+
99+
Warning: Object of class stdClass could not be converted to int in %s on line 33
100+
string(12) "42/-1337/3/1"
101+
102+
string(2) "1/"
103+
104+
string(2) "/1"
105+
106+
string(3) "/1/"
107+
108+
string(13) "42/-1337/1/42"
109+
110+
string(8) "0/53/1/3"
111+
112+
Called
113+
Called
114+
Called
115+
116+
Warning: Object of class Foo could not be converted to int in %s on line 57
117+
118+
Warning: Object of class Foo could not be converted to int in %s on line 57
119+
120+
Warning: Object of class Foo could not be converted to int in %s on line 57
121+
string(5) "1/1/1"
122+
123+
string(5) "0/0/0"
124+
125+
string(42) "9223372036854775807/0/-9223372036854775808"
126+
string(24) "2147483647/0/-2147483648"
127+
128+
string(5) "1/0/1"
129+
130+
string(3) "1/0"
131+
132+
string(1) "0"
133+
134+
Done
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
Test sprintf() function : Rope Optimization for '%d' with GMP objects
3+
--EXTENSIONS--
4+
gmp
5+
--FILE--
6+
<?php
7+
8+
$a = new GMP("42");
9+
$b = new GMP("-1337");
10+
$c = new GMP("999999999999999999999999999999999");
11+
12+
try {
13+
var_dump(sprintf("%d/%d/%d/%s", $a, $b, $c, $c + 1));
14+
} catch (\Throwable $e) {echo $e, PHP_EOL; } echo PHP_EOL;
15+
16+
echo "Done";
17+
?>
18+
--EXPECTF--
19+
string(63) "42/-1337/4089650035136921599/1000000000000000000000000000000000"
20+
21+
Done

0 commit comments

Comments
 (0)