Skip to content

Commit cad0e55

Browse files
authored
Faster validation logic in bc_str2num() (#14115)
Using SIMD to accelerate the validation. Using the benchmark from #14076. After: ``` 1.3504369258881 1.6206321716309 1.6845638751984 ``` Before: ``` 1.4750170707703 1.9039781093597 1.9632289409637 ```
1 parent 039344c commit cad0e55

File tree

3 files changed

+77
-38
lines changed

3 files changed

+77
-38
lines changed

ext/bcmath/bcmath.c

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,9 @@ PHP_MINFO_FUNCTION(bcmath)
132132

133133
/* {{{ php_str2num
134134
Convert to bc_num detecting scale */
135-
static zend_result php_str2num(bc_num *num, char *str)
135+
static zend_result php_str2num(bc_num *num, const zend_string *str)
136136
{
137-
if (!bc_str2num(num, str, 0, true)) {
137+
if (!bc_str2num(num, ZSTR_VAL(str), ZSTR_VAL(str) + ZSTR_LEN(str), 0, true)) {
138138
return FAILURE;
139139
}
140140

@@ -167,12 +167,12 @@ PHP_FUNCTION(bcadd)
167167
scale = (int) scale_param;
168168
}
169169

170-
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
170+
if (php_str2num(&first, left) == FAILURE) {
171171
zend_argument_value_error(1, "is not well-formed");
172172
goto cleanup;
173173
}
174174

175-
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
175+
if (php_str2num(&second, right) == FAILURE) {
176176
zend_argument_value_error(2, "is not well-formed");
177177
goto cleanup;
178178
}
@@ -214,12 +214,12 @@ PHP_FUNCTION(bcsub)
214214
scale = (int) scale_param;
215215
}
216216

217-
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
217+
if (php_str2num(&first, left) == FAILURE) {
218218
zend_argument_value_error(1, "is not well-formed");
219219
goto cleanup;
220220
}
221221

222-
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
222+
if (php_str2num(&second, right) == FAILURE) {
223223
zend_argument_value_error(2, "is not well-formed");
224224
goto cleanup;
225225
}
@@ -261,12 +261,12 @@ PHP_FUNCTION(bcmul)
261261
scale = (int) scale_param;
262262
}
263263

264-
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
264+
if (php_str2num(&first, left) == FAILURE) {
265265
zend_argument_value_error(1, "is not well-formed");
266266
goto cleanup;
267267
}
268268

269-
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
269+
if (php_str2num(&second, right) == FAILURE) {
270270
zend_argument_value_error(2, "is not well-formed");
271271
goto cleanup;
272272
}
@@ -310,12 +310,12 @@ PHP_FUNCTION(bcdiv)
310310

311311
bc_init_num(&result);
312312

313-
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
313+
if (php_str2num(&first, left) == FAILURE) {
314314
zend_argument_value_error(1, "is not well-formed");
315315
goto cleanup;
316316
}
317317

318-
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
318+
if (php_str2num(&second, right) == FAILURE) {
319319
zend_argument_value_error(2, "is not well-formed");
320320
goto cleanup;
321321
}
@@ -362,12 +362,12 @@ PHP_FUNCTION(bcmod)
362362

363363
bc_init_num(&result);
364364

365-
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
365+
if (php_str2num(&first, left) == FAILURE) {
366366
zend_argument_value_error(1, "is not well-formed");
367367
goto cleanup;
368368
}
369369

370-
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
370+
if (php_str2num(&second, right) == FAILURE) {
371371
zend_argument_value_error(2, "is not well-formed");
372372
goto cleanup;
373373
}
@@ -415,17 +415,17 @@ PHP_FUNCTION(bcpowmod)
415415

416416
bc_init_num(&result);
417417

418-
if (php_str2num(&bc_base, ZSTR_VAL(base_str)) == FAILURE) {
418+
if (php_str2num(&bc_base, base_str) == FAILURE) {
419419
zend_argument_value_error(1, "is not well-formed");
420420
goto cleanup;
421421
}
422422

423-
if (php_str2num(&bc_expo, ZSTR_VAL(exponent_str)) == FAILURE) {
423+
if (php_str2num(&bc_expo, exponent_str) == FAILURE) {
424424
zend_argument_value_error(2, "is not well-formed");
425425
goto cleanup;
426426
}
427427

428-
if (php_str2num(&bc_modulus, ZSTR_VAL(modulus_str)) == FAILURE) {
428+
if (php_str2num(&bc_modulus, modulus_str) == FAILURE) {
429429
zend_argument_value_error(3, "is not well-formed");
430430
goto cleanup;
431431
}
@@ -489,12 +489,12 @@ PHP_FUNCTION(bcpow)
489489

490490
bc_init_num(&result);
491491

492-
if (php_str2num(&first, ZSTR_VAL(base_str)) == FAILURE) {
492+
if (php_str2num(&first, base_str) == FAILURE) {
493493
zend_argument_value_error(1, "is not well-formed");
494494
goto cleanup;
495495
}
496496

497-
if (php_str2num(&bc_exponent, ZSTR_VAL(exponent_str)) == FAILURE) {
497+
if (php_str2num(&bc_exponent, exponent_str) == FAILURE) {
498498
zend_argument_value_error(2, "is not well-formed");
499499
goto cleanup;
500500
}
@@ -546,7 +546,7 @@ PHP_FUNCTION(bcsqrt)
546546
scale = (int) scale_param;
547547
}
548548

549-
if (php_str2num(&result, ZSTR_VAL(left)) == FAILURE) {
549+
if (php_str2num(&result, left) == FAILURE) {
550550
zend_argument_value_error(1, "is not well-formed");
551551
goto cleanup;
552552
}
@@ -588,12 +588,12 @@ PHP_FUNCTION(bccomp)
588588
scale = (int) scale_param;
589589
}
590590

591-
if (!bc_str2num(&first, ZSTR_VAL(left), scale, false)) {
591+
if (!bc_str2num(&first, ZSTR_VAL(left), ZSTR_VAL(left) + ZSTR_LEN(left), scale, false)) {
592592
zend_argument_value_error(1, "is not well-formed");
593593
goto cleanup;
594594
}
595595

596-
if (!bc_str2num(&second, ZSTR_VAL(right), scale, false)) {
596+
if (!bc_str2num(&second, ZSTR_VAL(right), ZSTR_VAL(right) + ZSTR_LEN(right), scale, false)) {
597597
zend_argument_value_error(2, "is not well-formed");
598598
goto cleanup;
599599
}
@@ -617,7 +617,7 @@ static void bcfloor_or_bcceil(INTERNAL_FUNCTION_PARAMETERS, bool is_floor)
617617
Z_PARAM_STR(numstr)
618618
ZEND_PARSE_PARAMETERS_END();
619619

620-
if (php_str2num(&num, ZSTR_VAL(numstr)) == FAILURE) {
620+
if (php_str2num(&num, numstr) == FAILURE) {
621621
zend_argument_value_error(1, "is not well-formed");
622622
goto cleanup;
623623
}
@@ -678,7 +678,7 @@ PHP_FUNCTION(bcround)
678678

679679
bc_init_num(&result);
680680

681-
if (php_str2num(&num, ZSTR_VAL(numstr)) == FAILURE) {
681+
if (php_str2num(&num, numstr) == FAILURE) {
682682
zend_argument_value_error(1, "is not well-formed");
683683
goto cleanup;
684684
}

ext/bcmath/libbcmath/src/bcmath.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ static inline bc_num bc_copy_num(bc_num num)
9797

9898
void bc_init_num(bc_num *num);
9999

100-
bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale);
100+
bool bc_str2num(bc_num *num, const char *str, const char *end, size_t scale, bool auto_scale);
101101

102102
zend_string *bc_num2str_ex(bc_num num, size_t scale);
103103

ext/bcmath/libbcmath/src/str2num.c

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,55 @@
3333
#include "convert.h"
3434
#include <stdbool.h>
3535
#include <stddef.h>
36+
#ifdef __SSE2__
37+
# include <emmintrin.h>
38+
#endif
3639

3740
/* Convert strings to bc numbers. Base 10 only.*/
41+
static const char *bc_count_digits(const char *str, const char *end)
42+
{
43+
/* Process in bulk */
44+
#ifdef __SSE2__
45+
const __m128i offset = _mm_set1_epi8((signed char) (SCHAR_MIN - '0'));
46+
/* we use the less than comparator, so add 1 */
47+
const __m128i threshold = _mm_set1_epi8(SCHAR_MIN + ('9' + 1 - '0'));
48+
49+
while (str + sizeof(__m128i) <= end) {
50+
__m128i bytes = _mm_loadu_si128((const __m128i *) str);
51+
/* Wrapping-add the offset to the bytes, such that all bytes below '0' are positive and others are negative.
52+
* More specifically, '0' will be -128 and '9' will be -119. */
53+
bytes = _mm_add_epi8(bytes, offset);
54+
/* Now mark all bytes that are <= '9', i.e. <= -119, i.e. < -118, i.e. the threshold. */
55+
bytes = _mm_cmplt_epi8(bytes, threshold);
56+
57+
int mask = _mm_movemask_epi8(bytes);
58+
if (mask != 0xffff) {
59+
/* At least one of the bytes is not within range. Move to the first offending byte. */
60+
#ifdef PHP_HAVE_BUILTIN_CTZL
61+
return str + __builtin_ctz(~mask);
62+
#else
63+
break;
64+
#endif
65+
}
66+
67+
str += sizeof(__m128i);
68+
}
69+
#endif
70+
71+
while (*str >= '0' && *str <= '9') {
72+
str++;
73+
}
74+
75+
return str;
76+
}
3877

3978
/* Assumes `num` points to NULL, i.e. does yet not hold a number. */
40-
bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale)
79+
bool bc_str2num(bc_num *num, const char *str, const char *end, size_t scale, bool auto_scale)
4180
{
42-
size_t digits = 0;
4381
size_t str_scale = 0;
44-
char *ptr = str;
45-
char *fractional_ptr = NULL;
46-
char *fractional_end = NULL;
82+
const char *ptr = str;
83+
const char *fractional_ptr = NULL;
84+
const char *fractional_end = NULL;
4785
bool zero_int = false;
4886

4987
ZEND_ASSERT(*num == NULL);
@@ -59,12 +97,10 @@ bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale)
5997
}
6098
const char *integer_ptr = ptr;
6199
/* digits before the decimal point */
62-
while (*ptr >= '0' && *ptr <= '9') {
63-
ptr++;
64-
digits++;
65-
}
100+
ptr = bc_count_digits(ptr, end);
101+
size_t digits = ptr - integer_ptr;
66102
/* decimal point */
67-
char *decimal_point = (*ptr == '.') ? ptr : NULL;
103+
const char *decimal_point = (*ptr == '.') ? ptr : NULL;
68104

69105
/* If a non-digit and non-decimal-point indicator is in the string, i.e. an invalid character */
70106
if (!decimal_point && *ptr != '\0') {
@@ -80,14 +116,17 @@ bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale)
80116
}
81117

82118
/* validate */
83-
while (*fractional_ptr >= '0' && *fractional_ptr <= '9') {
84-
fractional_end = *fractional_ptr != '0' ? fractional_ptr + 1 : fractional_end;
85-
fractional_ptr++;
86-
}
87-
if (*fractional_ptr != '\0') {
119+
fractional_end = bc_count_digits(fractional_ptr, end);
120+
if (*fractional_end != '\0') {
88121
/* invalid num */
89122
goto fail;
90123
}
124+
125+
/* Exclude trailing zeros. */
126+
while (fractional_end - 1 > decimal_point && fractional_end[-1] == '0') {
127+
fractional_end--;
128+
}
129+
91130
/* Move the pointer to the beginning of the fraction. */
92131
fractional_ptr = decimal_point + 1;
93132

0 commit comments

Comments
 (0)