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
```
This commit is contained in:
Niels Dossche 2024-05-03 17:34:38 +02:00 committed by GitHub
parent 039344cf70
commit cad0e555ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 77 additions and 38 deletions

View File

@ -132,9 +132,9 @@ PHP_MINFO_FUNCTION(bcmath)
/* {{{ php_str2num
Convert to bc_num detecting scale */
static zend_result php_str2num(bc_num *num, char *str)
static zend_result php_str2num(bc_num *num, const zend_string *str)
{
if (!bc_str2num(num, str, 0, true)) {
if (!bc_str2num(num, ZSTR_VAL(str), ZSTR_VAL(str) + ZSTR_LEN(str), 0, true)) {
return FAILURE;
}
@ -167,12 +167,12 @@ PHP_FUNCTION(bcadd)
scale = (int) scale_param;
}
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
if (php_str2num(&first, left) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
if (php_str2num(&second, right) == FAILURE) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
@ -214,12 +214,12 @@ PHP_FUNCTION(bcsub)
scale = (int) scale_param;
}
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
if (php_str2num(&first, left) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
if (php_str2num(&second, right) == FAILURE) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
@ -261,12 +261,12 @@ PHP_FUNCTION(bcmul)
scale = (int) scale_param;
}
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
if (php_str2num(&first, left) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
if (php_str2num(&second, right) == FAILURE) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
@ -310,12 +310,12 @@ PHP_FUNCTION(bcdiv)
bc_init_num(&result);
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
if (php_str2num(&first, left) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
if (php_str2num(&second, right) == FAILURE) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
@ -362,12 +362,12 @@ PHP_FUNCTION(bcmod)
bc_init_num(&result);
if (php_str2num(&first, ZSTR_VAL(left)) == FAILURE) {
if (php_str2num(&first, left) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (php_str2num(&second, ZSTR_VAL(right)) == FAILURE) {
if (php_str2num(&second, right) == FAILURE) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
@ -415,17 +415,17 @@ PHP_FUNCTION(bcpowmod)
bc_init_num(&result);
if (php_str2num(&bc_base, ZSTR_VAL(base_str)) == FAILURE) {
if (php_str2num(&bc_base, base_str) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (php_str2num(&bc_expo, ZSTR_VAL(exponent_str)) == FAILURE) {
if (php_str2num(&bc_expo, exponent_str) == FAILURE) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
if (php_str2num(&bc_modulus, ZSTR_VAL(modulus_str)) == FAILURE) {
if (php_str2num(&bc_modulus, modulus_str) == FAILURE) {
zend_argument_value_error(3, "is not well-formed");
goto cleanup;
}
@ -489,12 +489,12 @@ PHP_FUNCTION(bcpow)
bc_init_num(&result);
if (php_str2num(&first, ZSTR_VAL(base_str)) == FAILURE) {
if (php_str2num(&first, base_str) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (php_str2num(&bc_exponent, ZSTR_VAL(exponent_str)) == FAILURE) {
if (php_str2num(&bc_exponent, exponent_str) == FAILURE) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
@ -546,7 +546,7 @@ PHP_FUNCTION(bcsqrt)
scale = (int) scale_param;
}
if (php_str2num(&result, ZSTR_VAL(left)) == FAILURE) {
if (php_str2num(&result, left) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
@ -588,12 +588,12 @@ PHP_FUNCTION(bccomp)
scale = (int) scale_param;
}
if (!bc_str2num(&first, ZSTR_VAL(left), scale, false)) {
if (!bc_str2num(&first, ZSTR_VAL(left), ZSTR_VAL(left) + ZSTR_LEN(left), scale, false)) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
if (!bc_str2num(&second, ZSTR_VAL(right), scale, false)) {
if (!bc_str2num(&second, ZSTR_VAL(right), ZSTR_VAL(right) + ZSTR_LEN(right), scale, false)) {
zend_argument_value_error(2, "is not well-formed");
goto cleanup;
}
@ -617,7 +617,7 @@ static void bcfloor_or_bcceil(INTERNAL_FUNCTION_PARAMETERS, bool is_floor)
Z_PARAM_STR(numstr)
ZEND_PARSE_PARAMETERS_END();
if (php_str2num(&num, ZSTR_VAL(numstr)) == FAILURE) {
if (php_str2num(&num, numstr) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}
@ -678,7 +678,7 @@ PHP_FUNCTION(bcround)
bc_init_num(&result);
if (php_str2num(&num, ZSTR_VAL(numstr)) == FAILURE) {
if (php_str2num(&num, numstr) == FAILURE) {
zend_argument_value_error(1, "is not well-formed");
goto cleanup;
}

View File

@ -97,7 +97,7 @@ static inline bc_num bc_copy_num(bc_num num)
void bc_init_num(bc_num *num);
bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale);
bool bc_str2num(bc_num *num, const char *str, const char *end, size_t scale, bool auto_scale);
zend_string *bc_num2str_ex(bc_num num, size_t scale);

View File

@ -33,17 +33,55 @@
#include "convert.h"
#include <stdbool.h>
#include <stddef.h>
#ifdef __SSE2__
# include <emmintrin.h>
#endif
/* Convert strings to bc numbers. Base 10 only.*/
static const char *bc_count_digits(const char *str, const char *end)
{
/* Process in bulk */
#ifdef __SSE2__
const __m128i offset = _mm_set1_epi8((signed char) (SCHAR_MIN - '0'));
/* we use the less than comparator, so add 1 */
const __m128i threshold = _mm_set1_epi8(SCHAR_MIN + ('9' + 1 - '0'));
while (str + sizeof(__m128i) <= end) {
__m128i bytes = _mm_loadu_si128((const __m128i *) str);
/* Wrapping-add the offset to the bytes, such that all bytes below '0' are positive and others are negative.
* More specifically, '0' will be -128 and '9' will be -119. */
bytes = _mm_add_epi8(bytes, offset);
/* Now mark all bytes that are <= '9', i.e. <= -119, i.e. < -118, i.e. the threshold. */
bytes = _mm_cmplt_epi8(bytes, threshold);
int mask = _mm_movemask_epi8(bytes);
if (mask != 0xffff) {
/* At least one of the bytes is not within range. Move to the first offending byte. */
#ifdef PHP_HAVE_BUILTIN_CTZL
return str + __builtin_ctz(~mask);
#else
break;
#endif
}
str += sizeof(__m128i);
}
#endif
while (*str >= '0' && *str <= '9') {
str++;
}
return str;
}
/* Assumes `num` points to NULL, i.e. does yet not hold a number. */
bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale)
bool bc_str2num(bc_num *num, const char *str, const char *end, size_t scale, bool auto_scale)
{
size_t digits = 0;
size_t str_scale = 0;
char *ptr = str;
char *fractional_ptr = NULL;
char *fractional_end = NULL;
const char *ptr = str;
const char *fractional_ptr = NULL;
const char *fractional_end = NULL;
bool zero_int = false;
ZEND_ASSERT(*num == NULL);
@ -59,12 +97,10 @@ bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale)
}
const char *integer_ptr = ptr;
/* digits before the decimal point */
while (*ptr >= '0' && *ptr <= '9') {
ptr++;
digits++;
}
ptr = bc_count_digits(ptr, end);
size_t digits = ptr - integer_ptr;
/* decimal point */
char *decimal_point = (*ptr == '.') ? ptr : NULL;
const char *decimal_point = (*ptr == '.') ? ptr : NULL;
/* If a non-digit and non-decimal-point indicator is in the string, i.e. an invalid character */
if (!decimal_point && *ptr != '\0') {
@ -80,14 +116,17 @@ bool bc_str2num(bc_num *num, char *str, size_t scale, bool auto_scale)
}
/* validate */
while (*fractional_ptr >= '0' && *fractional_ptr <= '9') {
fractional_end = *fractional_ptr != '0' ? fractional_ptr + 1 : fractional_end;
fractional_ptr++;
}
if (*fractional_ptr != '\0') {
fractional_end = bc_count_digits(fractional_ptr, end);
if (*fractional_end != '\0') {
/* invalid num */
goto fail;
}
/* Exclude trailing zeros. */
while (fractional_end - 1 > decimal_point && fractional_end[-1] == '0') {
fractional_end--;
}
/* Move the pointer to the beginning of the fraction. */
fractional_ptr = decimal_point + 1;