Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 2c5ed50

Browse files
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 ×ばつ 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/-1337142"
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)

AltStyle によって変換されたページ (->オリジナル) /