From 9e67b47c0d84c7b5b333078b23f3f7df0d3cc54c Mon Sep 17 00:00:00 2001 From: Charlie Gordon Date: Sun, 26 May 2024 08:06:36 +0200 Subject: [PATCH] Improve number to string conversions (#400) integer conversions: - improve `u32toa_radix` and `u64toa_radix`, add `i32toa_radix` - use `i32toa_radix` for small ints in `js_number_toString` floating point conversions (`js_dtoa`): - complete rewrite with fewer calls to `snprintf` - remove `JS_DTOA_FORMAT`, define 4 possible modes for `js_dtoa` - remove the radix argument in `js_dtoa` - merge `js_dtoa1` into `js_dtoa` - add `js_dtoa_infinite` for non finite values - simplify sign handling - handle locale specific decimal point transparently helper function `js_fcvt`: - simplify `js_fcvt`, remove `js_fcvt1`, reduce overhead - round up manually instead of using `fesetround(FE_UPWARD)`. helper function `js_ecvt`: - document `js_ecvt` and `js_ecvt1` behavior - avoid redundant `js_ecvt1` calls in `js_ecvt` - fixed buffer contents, no buffer copies - simplify decimal point handling - round up manually instead of using `fesetround(FE_UPWARD)`. miscellaneous: - remove `CONFIG_PRINTF_RNDN`. This fixes some of the conversion errors on Windows. Updated the tests accordingly - this fixes a v8.sh bug on macOS: `0.5.toFixed(0)` used to produce `0` instead of `1` - add regression tests, update test_conv unit tests - add benchmarks for `toFixed`, `toPrecision` and `toExponential` number methods - benchmarks show all conversions are now 40 to 45% faster (M2) --- cutils.c | 31 ++- cutils.h | 1 + quickjs.c | 628 +++++++++++++++++++++++------------------- tests/microbench.js | 54 +++- tests/test_builtin.js | 9 +- tests/test_conv.c | 76 ++--- 6 files changed, 474 insertions(+), 325 deletions(-) diff --git a/cutils.c b/cutils.c index 88cd72d..bdd1256 100644 --- a/cutils.c +++ b/cutils.c @@ -575,6 +575,9 @@ overflow: /* 2 <= base <= 36 */ char const digits36[36] = "0123456789abcdefghijklmnopqrstuvwxyz"; +#define USE_SPECIAL_RADIX_10 1 // special case base 10 radix conversions +#define USE_SINGLE_CASE_FAST 1 // special case single digit numbers + /* using u32toa_shift variant */ #define gen_digit(buf, c) if (is_be()) \ @@ -613,11 +616,13 @@ size_t u07toa_shift(char dest[minimum_length(8)], uint32_t n, size_t len) size_t u32toa(char buf[minimum_length(11)], uint32_t n) { +#ifdef USE_SINGLE_CASE_FAST /* 10% */ if (n < 10) { buf[0] = (char)('0' + n); buf[1] = '\0'; return 1; } +#endif #define TEN_POW_7 10000000 if (n >= TEN_POW_7) { uint32_t quo = n / TEN_POW_7; @@ -679,6 +684,8 @@ static uint8_t const radix_shift[64] = { size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned base) { + int shift; + #ifdef USE_SPECIAL_RADIX_10 if (likely(base == 10)) return u32toa(buf, n); @@ -688,13 +695,13 @@ size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned base) buf[1] = '\0'; return 1; } - int shift = radix_shift[base & 63]; + shift = radix_shift[base & 63]; if (shift) { uint32_t mask = (1 << shift) - 1; size_t len = (32 - clz32(n) + shift - 1) / shift; size_t last = n & mask; - n /= base; char *end = buf + len; + n >>= shift; *end-- = '\0'; *end-- = digits36[last]; while (n >= base) { @@ -728,11 +735,13 @@ size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned base) size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned base) { + int shift; + #ifdef USE_SPECIAL_RADIX_10 if (likely(base == 10)) return u64toa(buf, n); #endif - int shift = radix_shift[base & 63]; + shift = radix_shift[base & 63]; if (shift) { if (n < base) { buf[0] = digits36[n]; @@ -742,8 +751,8 @@ size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned base) uint64_t mask = (1 << shift) - 1; size_t len = (64 - clz64(n) + shift - 1) / shift; size_t last = n & mask; - n /= base; char *end = buf + len; + n >>= shift; *end-- = '\0'; *end-- = digits36[last]; while (n >= base) { @@ -777,6 +786,15 @@ size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned base) } } +size_t i32toa_radix(char buf[minimum_length(34)], int32_t n, unsigned int base) +{ + if (likely(n >= 0)) + return u32toa_radix(buf, n, base); + + buf[0] = '-'; + return 1 + u32toa_radix(buf + 1, -(uint32_t)n, base); +} + size_t i64toa_radix(char buf[minimum_length(66)], int64_t n, unsigned int base) { if (likely(n >= 0)) @@ -786,6 +804,11 @@ size_t i64toa_radix(char buf[minimum_length(66)], int64_t n, unsigned int base) return 1 + u64toa_radix(buf + 1, -(uint64_t)n, base); } +#undef gen_digit +#undef TEN_POW_7 +#undef USE_SPECIAL_RADIX_10 +#undef USE_SINGLE_CASE_FAST + /*---- sorting with opaque argument ----*/ typedef void (*exchange_f)(void *a, void *b, size_t size); diff --git a/cutils.h b/cutils.h index 853277e..6c7a0a3 100644 --- a/cutils.h +++ b/cutils.h @@ -463,6 +463,7 @@ size_t i32toa(char buf[minimum_length(12)], int32_t n); size_t u64toa(char buf[minimum_length(21)], uint64_t n); size_t i64toa(char buf[minimum_length(22)], int64_t n); size_t u32toa_radix(char buf[minimum_length(33)], uint32_t n, unsigned int base); +size_t i32toa_radix(char buf[minimum_length(34)], int32_t n, unsigned base); size_t u64toa_radix(char buf[minimum_length(65)], uint64_t n, unsigned int base); size_t i64toa_radix(char buf[minimum_length(66)], int64_t n, unsigned int base); diff --git a/quickjs.c b/quickjs.c index 1d01eb6..8e49631 100644 --- a/quickjs.c +++ b/quickjs.c @@ -58,11 +58,6 @@ #define MALLOC_OVERHEAD 8 #endif -#if !defined(_WIN32) && !defined(__wasi__) -/* define it if printf uses the RNDN rounding mode instead of RNDNA */ -#define CONFIG_PRINTF_RNDN -#endif - #if defined(__NEWLIB__) #define NO_TM_GMTOFF #endif @@ -10965,262 +10960,354 @@ static JSValue js_bigint_to_string(JSContext *ctx, JSValue val) return js_bigint_to_string1(ctx, val, 10); } -/* buf1 contains the printf result */ -static void js_ecvt1(double d, int n_digits, int *decpt, int *sign, char *buf, - int rounding_mode, char *buf1, int buf1_size) +/*---- floating point number to string conversions ----*/ + +/* JavaScript rounding is specified as round to nearest tie away + from zero (RNDNA), but in `printf` the "ties" case is not + specified (in most cases it is RNDN, round to nearest, tie to even), + so we must round manually. We generate 2 extra places and make + an extra call to snprintf if these are exactly '50'. + We set the current rounding mode to FE_DOWNWARD to check if the + last 2 places become '49'. If not, we must round up, which is + performed in place using the string digits. + + Note that we cannot rely on snprintf for rounding up: + the code below fails on macOS for `0.5.toFixed(0)`: gives `0` expected `1` + fesetround(FE_UPWARD); + snprintf(dest, size, "%.*f", n_digits, d); + fesetround(FE_TONEAREST); + */ + +/* `js_fcvt` minimum buffer length: + - up to 21 digits in integral part + - 1 potential decimal point + - up to 102 decimals + - 1 null terminator + */ +#define JS_FCVT_BUF_SIZE (21+1+102+1) + +/* `js_ecvt` minimum buffer length: + - 1 leading digit + - 1 potential decimal point + - up to 102 decimals + - 5 exponent characters (from 'e-324' to 'e+308') + - 1 null terminator + */ +#define JS_ECVT_BUF_SIZE (1+1+102+5+1) + +/* `js_dtoa` minimum buffer length: + - 8 byte prefix + - either JS_FCVT_BUF_SIZE or JS_ECVT_BUF_SIZE + - JS_FCVT_BUF_SIZE is larger than JS_ECVT_BUF_SIZE + */ +#define JS_DTOA_BUF_SIZE (8+JS_FCVT_BUF_SIZE) + +/* `js_ecvt1`: compute the digits and decimal point spot for a double + - `d` is finite, positive or zero + - `n_digits` number of significant digits in range 1..103 + - `buf` receives the printf result + - `buf` has a fixed format: n_digits with a decimal point at offset 1 + and exponent 'e{+/-}xx[x]' at offset n_digits+1 + Return n_digits + Store the position of the decimal point into `*decpt` + */ +static int js_ecvt1(double d, int n_digits, + char dest[minimum_length(JS_ECVT_BUF_SIZE)], + size_t size, int *decpt) { - if (rounding_mode != FE_TONEAREST) - fesetround(rounding_mode); - snprintf(buf1, buf1_size, "%+.*e", n_digits - 1, d); - if (rounding_mode != FE_TONEAREST) - fesetround(FE_TONEAREST); - *sign = (buf1[0] == '-'); - /* mantissa */ - buf[0] = buf1[1]; - if (n_digits > 1) - memcpy(buf + 1, buf1 + 3, n_digits - 1); - buf[n_digits] = '\0'; - /* exponent */ - *decpt = atoi(buf1 + n_digits + 2 + (n_digits > 1)) + 1; -} - -/* maximum buffer size for js_dtoa */ -#define JS_DTOA_BUF_SIZE 128 - -/* needed because ecvt usually limits the number of digits to - 17. Return the number of digits. */ -static int js_ecvt(double d, int n_digits, int *decpt, int *sign, char *buf, - BOOL is_fixed) -{ - int rounding_mode; - char buf_tmp[JS_DTOA_BUF_SIZE]; - - if (!is_fixed) { - unsigned int n_digits_min, n_digits_max; - /* find the minimum amount of digits (XXX: inefficient but simple) */ - n_digits_min = 1; - n_digits_max = 17; - while (n_digits_min < n_digits_max) { - n_digits = (n_digits_min + n_digits_max) / 2; - js_ecvt1(d, n_digits, decpt, sign, buf, FE_TONEAREST, - buf_tmp, sizeof(buf_tmp)); - if (strtod(buf_tmp, NULL) == d) { - /* no need to keep the trailing zeros */ - while (n_digits >= 2 && buf[n_digits - 1] == '0') - n_digits--; - n_digits_max = n_digits; - } else { - n_digits_min = n_digits + 1; - } - } - n_digits = n_digits_max; - rounding_mode = FE_TONEAREST; - } else { - rounding_mode = FE_TONEAREST; -#ifdef CONFIG_PRINTF_RNDN - { - char buf1[JS_DTOA_BUF_SIZE], buf2[JS_DTOA_BUF_SIZE]; - int decpt1, sign1, decpt2, sign2; - /* The JS rounding is specified as round to nearest ties away - from zero (RNDNA), but in printf the "ties" case is not - specified (for example it is RNDN for glibc, RNDNA for - Windows), so we must round manually. */ - js_ecvt1(d, n_digits + 1, &decpt1, &sign1, buf1, FE_TONEAREST, - buf_tmp, sizeof(buf_tmp)); - /* XXX: could use 2 digits to reduce the average running time */ - if (buf1[n_digits] == '5') { - js_ecvt1(d, n_digits + 1, &decpt1, &sign1, buf1, FE_DOWNWARD, - buf_tmp, sizeof(buf_tmp)); - js_ecvt1(d, n_digits + 1, &decpt2, &sign2, buf2, FE_UPWARD, - buf_tmp, sizeof(buf_tmp)); - if (memcmp(buf1, buf2, n_digits + 1) == 0 && decpt1 == decpt2) { - /* exact result: round away from zero */ - if (sign1) - rounding_mode = FE_DOWNWARD; - else - rounding_mode = FE_UPWARD; - } - } - } -#endif /* CONFIG_PRINTF_RNDN */ - } - js_ecvt1(d, n_digits, decpt, sign, buf, rounding_mode, - buf_tmp, sizeof(buf_tmp)); + /* d is positive, ensure decimal point is always present */ + snprintf(dest, size, "%#.*e", n_digits - 1, d); + /* dest contents: + 0: first digit + 1: '.' decimal point (locale specific) + 2..n_digits: (n_digits-1) additional digits + n_digits+1: 'e' exponent mark + n_digits+2..: exponent sign, value and null terminator + */ + /* extract the exponent (actually the position of the decimal point) */ + *decpt = 1 + atoi(dest + n_digits + 2); return n_digits; } -static size_t js_fcvt1(char (*buf)[JS_DTOA_BUF_SIZE], double d, int n_digits, - int rounding_mode) +/* `js_ecvt`: compute the digits and decimal point spot for a double + with proper javascript rounding. We cannot use `ecvt` for multiple + resasons: portability, because of the number of digits is typically + limited to 17, finally because the default rounding is inadequate. + `d` is finite and positive or zero. + `n_digits` number of significant digits in range 1..101 + or 0 for automatic (only as many digits as necessary) + Return the number of digits produced in `dest`. + Store the position of the decimal point into `*decpt` + */ +static int js_ecvt(double d, int n_digits, + char dest[minimum_length(JS_ECVT_BUF_SIZE)], + size_t size, int *decpt) { - size_t n; - if (rounding_mode != FE_TONEAREST) - fesetround(rounding_mode); - n = snprintf(*buf, sizeof(*buf), "%.*f", n_digits, d); - if (rounding_mode != FE_TONEAREST) - fesetround(FE_TONEAREST); - assert(n < sizeof(*buf)); - return n; -} + int i; -static size_t js_fcvt(char (*buf)[JS_DTOA_BUF_SIZE], double d, int n_digits) -{ - int rounding_mode; - rounding_mode = FE_TONEAREST; -#ifdef CONFIG_PRINTF_RNDN - { - int n1, n2; - char buf1[JS_DTOA_BUF_SIZE]; - char buf2[JS_DTOA_BUF_SIZE]; - - /* The JS rounding is specified as round to nearest ties away from - zero (RNDNA), but in printf the "ties" case is not specified - (for example it is RNDN for glibc, RNDNA for Windows), so we - must round manually. */ - n1 = js_fcvt1(&buf1, d, n_digits + 1, FE_TONEAREST); - rounding_mode = FE_TONEAREST; - /* XXX: could use 2 digits to reduce the average running time */ - if (buf1[n1 - 1] == '5') { - n1 = js_fcvt1(&buf1, d, n_digits + 1, FE_DOWNWARD); - n2 = js_fcvt1(&buf2, d, n_digits + 1, FE_UPWARD); - if (n1 == n2 && memcmp(buf1, buf2, n1) == 0) { - /* exact result: round away from zero */ - if (buf1[0] == '-') - rounding_mode = FE_DOWNWARD; - else - rounding_mode = FE_UPWARD; + if (n_digits == 0) { + /* find the minimum number of digits (XXX: inefficient but simple) */ + // TODO(chqrlie) use direct method from quickjs-printf + unsigned int n_digits_min = 1; + unsigned int n_digits_max = 17; + for (;;) { + n_digits = (n_digits_min + n_digits_max) / 2; + js_ecvt1(d, n_digits, dest, size, decpt); + if (n_digits_min == n_digits_max) + return n_digits; + /* dest contents: + 0: first digit + 1: '.' decimal point (locale specific) + 2..n_digits: (n_digits-1) additional digits + n_digits+1: 'e' exponent mark + n_digits+2..: exponent sign, value and null terminator + */ + if (strtod(dest, NULL) == d) { + unsigned int n0 = n_digits; + /* enough digits */ + /* strip the trailing zeros */ + while (dest[n_digits] == '0') + n_digits--; + if (n_digits == n_digits_min) + return n_digits; + /* done if trailing zeros and not denormal or huge */ + if (n_digits < n0 && d > 3e-308 && d < 8e307) + return n_digits; + n_digits_max = n_digits; + } else { + /* need at least one more digit */ + n_digits_min = n_digits + 1; } } - } -#endif /* CONFIG_PRINTF_RNDN */ - return js_fcvt1(buf, d, n_digits, rounding_mode); -} - -/* radix != 10 is only supported with flags = JS_DTOA_VAR_FORMAT */ -/* use as many digits as necessary */ -#define JS_DTOA_VAR_FORMAT (0 << 0) -/* use n_digits significant digits (1 <= n_digits <= 101) */ -#define JS_DTOA_FIXED_FORMAT (1 << 0) -/* force fractional format: [-]dd.dd with n_digits fractional digits */ -#define JS_DTOA_FRAC_FORMAT (2 << 0) -/* force exponential notation either in fixed or variable format */ -#define JS_DTOA_FORCE_EXP (1 << 2) - -/* XXX: slow and maybe not fully correct. Use libbf when it is fast enough. - XXX: radix != 10 is only supported for small integers -*/ -static size_t js_dtoa1(char (*buf)[JS_DTOA_BUF_SIZE], double d, - int radix, int n_digits, int flags) -{ - char *q; - - if (!isfinite(d)) { - if (isnan(d)) { - memcpy(*buf, "NaN", sizeof "NaN"); - return sizeof("NaN") - 1; - } else if (d < 0) { - memcpy(*buf, "-Infinity", sizeof "-Infinity"); - return sizeof("-Infinity") - 1; - } else { - memcpy(*buf, "Infinity", sizeof "Infinity"); - return sizeof("Infinity") - 1; - } - } else if (flags == JS_DTOA_VAR_FORMAT) { - int64_t i64; - char buf1[72], *ptr; - if (d > (double)MAX_SAFE_INTEGER || d < (double)-MAX_SAFE_INTEGER) - goto generic_conv; - i64 = (int64_t)d; - if (d != i64) - goto generic_conv; - /* fast path for integers */ - return i64toa_radix(*buf, i64, radix); } else { - if (d == 0.0) - d = 0.0; /* convert -0 to 0 */ - if (flags == JS_DTOA_FRAC_FORMAT) { - return js_fcvt(buf, d, n_digits); - } else { - char buf1[JS_DTOA_BUF_SIZE]; - int sign, decpt, k, n, i, p, n_max; - BOOL is_fixed; - generic_conv: - is_fixed = ((flags & 3) == JS_DTOA_FIXED_FORMAT); - if (is_fixed) { - n_max = n_digits; - } else { - n_max = 21; - } - /* the number has k digits (k >= 1) */ - k = js_ecvt(d, n_digits, &decpt, &sign, buf1, is_fixed); - n = decpt; /* d=10^(n-k)*(buf1) i.e. d= < x.yyyy 10^(n-1) */ - q = *buf; - if (sign) - *q++ = '-'; - if (flags & JS_DTOA_FORCE_EXP) - goto force_exp; - if (n >= 1 && n <= n_max) { - if (k <= n) { - memcpy(q, buf1, k); - q += k; - for(i = 0; i < (n - k); i++) - *q++ = '0'; - *q = '\0'; - } else { - /* k > n */ - memcpy(q, buf1, n); - q += n; - *q++ = '.'; - for(i = 0; i < (k - n); i++) - *q++ = buf1[n + i]; - *q = '\0'; - } - } else if (n >= -5 && n <= 0) { - *q++ = '0'; - *q++ = '.'; - for(i = 0; i < -n; i++) - *q++ = '0'; - memcpy(q, buf1, k); - q += k; - *q = '\0'; - } else { - force_exp: - /* exponential notation */ - *q++ = buf1[0]; - if (k > 1) { - *q++ = '.'; - for(i = 1; i < k; i++) - *q++ = buf1[i]; - } - *q++ = 'e'; - p = n - 1; - if (p >= 0) - *q++ = '+'; - q += snprintf(q, *buf + sizeof(*buf) - q, "%d", p); - } - return q - *buf; +#if defined(FE_DOWNWARD) && defined(FE_TONEAREST) + /* generate 2 extra digits: 99% chances to avoid 2 calls */ + js_ecvt1(d, n_digits + 2, dest, size, decpt); + if (dest[n_digits + 1] < '5') + return n_digits; /* truncate the 2 extra digits */ + if (dest[n_digits + 1] == '5' && dest[n_digits + 2] == '0') { + /* close to half-way: try rounding toward 0 */ + fesetround(FE_DOWNWARD); + js_ecvt1(d, n_digits + 2, dest, size, decpt); + fesetround(FE_TONEAREST); + if (dest[n_digits + 1] < '5') + return n_digits; /* truncate the 2 extra digits */ } + /* round up in the string */ + for(i = n_digits;; i--) { + /* ignore the locale specific decimal point */ + if (is_digit(dest[i])) { + if (dest[i]++ < '9') + break; + dest[i] = '0'; + if (i == 0) { + dest[0] = '1'; + (*decpt)++; + break; + } + } + } + return n_digits; /* truncate the 2 extra digits */ +#else + /* No disambiguation available, eg: __wasi__ targets */ + return js_ecvt1(d, n_digits, dest, size, decpt); +#endif } } -static JSValue js_dtoa(JSContext *ctx, - double d, int radix, int n_digits, int flags) +/* `js_fcvt`: convert a floating point value to %f format using RNDNA + `d` is finite and positive or zero. + `n_digits` number of decimal places in range 0..100 + Return the number of characters produced in `dest`. + */ +static size_t js_fcvt(double d, int n_digits, + char dest[minimum_length(JS_FCVT_BUF_SIZE)], size_t size) +{ +#if defined(FE_DOWNWARD) && defined(FE_TONEAREST) + int i, n1, n2; + /* generate 2 extra digits: 99% chances to avoid 2 calls */ + n1 = snprintf(dest, size, "%.*f", n_digits + 2, d) - 2; + if (dest[n1] >= '5') { + if (dest[n1] == '5' && dest[n1 + 1] == '0') { + /* close to half-way: try rounding toward 0 */ + fesetround(FE_DOWNWARD); + n1 = snprintf(dest, size, "%.*f", n_digits + 2, d) - 2; + fesetround(FE_TONEAREST); + } + if (dest[n1] >= '5') { /* number should be rounded up */ + /* d is either exactly half way or greater: round the string manually */ + for (i = n1 - 1;; i--) { + /* ignore the locale specific decimal point */ + if (is_digit(dest[i])) { + if (dest[i]++ < '9') + break; + dest[i] = '0'; + if (i == 0) { + dest[0] = '1'; + dest[n1] = '0'; + dest[n1 - n_digits - 1] = '0'; + dest[n1 - n_digits] = '.'; + n1++; + break; + } + } + } + } + } + /* truncate the extra 2 digits and the decimal point if !n_digits */ + n1 -= !n_digits; + //dest[n1] = '\0'; // optional + return n1; +#else + /* No disambiguation available, eg: __wasi__ targets */ + return snprintf(dest, size, "%.*f", n_digits, d); +#endif +} + +static JSValue js_dtoa_infinite(JSContext *ctx, double d) +{ + // TODO(chqrlie) use atoms for NaN and Infinite? + if (isnan(d)) + return js_new_string8(ctx, "NaN"); + if (d < 0) + return js_new_string8(ctx, "-Infinity"); + else + return js_new_string8(ctx, "Infinity"); +} + +#define JS_DTOA_TOSTRING 0 /* use as many digits as necessary */ +#define JS_DTOA_EXPONENTIAL 1 /* use exponential notation either fixed or variable digits */ +#define JS_DTOA_FIXED 2 /* force fixed number of fractional digits */ +#define JS_DTOA_PRECISION 3 /* use n_digits significant digits (1 <= n_digits <= 101) */ + +/* `js_dtoa`: convert a floating point number to a string + - `mode`: one of the 4 supported formats + - `n_digits`: digit number according to mode + - TOSTRING: 0 only. As many digits as necessary + - EXPONENTIAL: 0 as many decimals as necessary + - 1..101 number of significant digits + - FIXED: 0..100 number of decimal places + - PRECISION: 1..101 number of significant digits + */ +// XXX: should use libbf or quickjs-printf. +static JSValue js_dtoa(JSContext *ctx, double d, int n_digits, int mode) { char buf[JS_DTOA_BUF_SIZE]; - size_t len = js_dtoa1(&buf, d, radix, n_digits, flags); - return js_new_string8_len(ctx, buf, len); + size_t len; + char *start; + int sign, decpt, exp, i, k, n, n_max; + + if (!isfinite(d)) + return js_dtoa_infinite(ctx, d); + + sign = (d < 0); + start = buf + 8; + d = fabs(d); /* also converts -0 to 0 */ + + if (mode != JS_DTOA_EXPONENTIAL && n_digits == 0) { + /* fast path for exact integers in variable format: + clip to MAX_SAFE_INTEGER because to ensure insignificant + digits are generated as 0. + used for JS_DTOA_TOSTRING and JS_DTOA_FIXED without decimals. + */ + if (d <= (double)MAX_SAFE_INTEGER) { + uint64_t u64 = (uint64_t)d; + if (d == u64) { + len = u64toa(start, u64); + goto done; + } + } + } + if (mode == JS_DTOA_FIXED) { + len = js_fcvt(d, n_digits, start, sizeof(buf) - 8); + // TODO(chqrlie) patch the locale specific decimal point + goto done; + } + + n_max = (n_digits > 0) ? n_digits : 21; + /* the number has k digits (1 <= k <= n_max) */ + k = js_ecvt(d, n_digits, start, sizeof(buf) - 8, &decpt); + /* buffer contents: + 0: first digit + 1: '.' decimal point + 2..k: (k-1) additional digits + */ + n = decpt; /* d=10^(n-k)*(buf1) i.e. d= < x.yyyy 10^(n-1) */ + if (mode != JS_DTOA_EXPONENTIAL) { + /* mode is JS_DTOA_PRECISION or JS_DTOA_TOSTRING */ + if (n >= 1 && n <= n_max) { + /* between 1 and n_max digits before the decimal point */ + if (k <= n) { + /* all digits before the point, append zeros */ + start[1] = start[0]; + start++; + for(i = k; i < n; i++) + start[i] = '0'; + len = n; + } else { + /* k > n: move digits before the point */ + for(i = 1; i < n; i++) + start[i] = start[i + 1]; + start[i] = '.'; + len = 1 + k; + } + goto done; + } + if (n >= -5 && n <= 0) { + /* insert -n leading 0 decimals and a '0.' prefix */ + n = -n; + start[1] = start[0]; + start -= n + 1; + start[0] = '0'; + start[1] = '.'; + for(i = 0; i < n; i++) + start[2 + i] = '0'; + len = 2 + k + n; + goto done; + } + } + /* exponential notation */ + exp = n - 1; + /* count the digits and the decimal point if at least one decimal */ + len = k + (k > 1); + start[1] = '.'; /* patch the locale specific decimal point */ + start[len] = 'e'; + start[len + 1] = '+'; + if (exp < 0) { + start[len + 1] = '-'; + exp = -exp; + } + len += 2 + 1 + (exp > 9) + (exp > 99); + for (i = len - 1; exp > 9;) { + int quo = exp / 10; + start[i--] = (char)('0' + exp % 10); + exp = quo; + } + start[i] = (char)('0' + exp); + + done: + start[-1] = '-'; /* prepend the sign if negative */ + return js_new_string8_len(ctx, start - sign, len + sign); } -/* d is guaranteed to be finite */ +/* `js_dtoa_radix`: convert a floating point number using a specific base + - `d` must be finite + - `radix` must be in range 2..36 + */ static JSValue js_dtoa_radix(JSContext *ctx, double d, int radix) { char buf[2200], *ptr, *ptr2, *ptr3; - /* d is finite */ - int sign = d < 0; - int digit; + int sign, digit; double frac, d0; - int64_t n0 = 0; + int64_t n0; + + if (!isfinite(d)) + return js_dtoa_infinite(ctx, d); + + sign = (d < 0); d = fabs(d); d0 = trunc(d); + n0 = 0; frac = d - d0; ptr2 = buf + 1100; /* ptr2 points to the end of the string */ ptr = ptr2; /* ptr points to the beginning of the string */ @@ -11284,7 +11371,7 @@ static JSValue js_dtoa_radix(JSContext *ctx, double d, int radix) ptr2[-1] = (ptr2[-1] == '9') ? 'a' : ptr2[-1] + 1; } } else { - /* strip trailing fractional zeroes */ + /* strip trailing fractional zeros */ while (ptr2[-1] == '0') ptr2--; /* strip the 'decimal' point if last */ @@ -11340,8 +11427,7 @@ JSValue JS_ToStringInternal(JSContext *ctx, JSValue val, BOOL is_ToPropertyKey) return JS_ThrowTypeError(ctx, "cannot convert symbol to string"); } case JS_TAG_FLOAT64: - return js_dtoa(ctx, JS_VALUE_GET_FLOAT64(val), 10, 0, - JS_DTOA_VAR_FORMAT); + return js_dtoa(ctx, JS_VALUE_GET_FLOAT64(val), 0, JS_DTOA_TOSTRING); case JS_TAG_BIG_INT: return js_bigint_to_string(ctx, val); default: @@ -38961,7 +39047,7 @@ static int js_get_radix(JSContext *ctx, JSValue val) if (JS_ToInt32Sat(ctx, &radix, val)) return -1; if (radix < 2 || radix > 36) { - JS_ThrowRangeError(ctx, "radix must be between 2 and 36"); + JS_ThrowRangeError(ctx, "toString() radix argument must be between 2 and 36"); return -1; } return radix; @@ -38986,22 +39072,21 @@ static JSValue js_number_toString(JSContext *ctx, JSValue this_val, base = 10; } else { base = js_get_radix(ctx, argv[0]); - if (base < 0) - goto fail; + if (base < 0) { + JS_FreeValue(ctx, val); + return JS_EXCEPTION; + } } if (JS_VALUE_GET_TAG(val) == JS_TAG_INT) { - size_t len = i64toa_radix(buf, JS_VALUE_GET_INT(val), base); + size_t len = i32toa_radix(buf, JS_VALUE_GET_INT(val), base); return js_new_string8_len(ctx, buf, len); } if (JS_ToFloat64Free(ctx, &d, val)) return JS_EXCEPTION; - if (base != 10 && isfinite(d)) { + if (base != 10) return js_dtoa_radix(ctx, d, base); - } - return js_dtoa(ctx, d, base, 0, JS_DTOA_VAR_FORMAT); - fail: - JS_FreeValue(ctx, val); - return JS_EXCEPTION; + + return js_dtoa(ctx, d, 0, JS_DTOA_TOSTRING); } static JSValue js_number_toFixed(JSContext *ctx, JSValue this_val, @@ -39019,13 +39104,13 @@ static JSValue js_number_toFixed(JSContext *ctx, JSValue this_val, if (JS_ToInt32Sat(ctx, &f, argv[0])) return JS_EXCEPTION; if (f < 0 || f > 100) { - return JS_ThrowRangeError(ctx, "%s() argument must be between 1 and 100", - "toFixed"); + return JS_ThrowRangeError(ctx, "toFixed() digits argument must be between 0 and 100"); } if (fabs(d) >= 1e21) { - return JS_ToStringFree(ctx, js_float64(d)); + // use ToString(d) + return js_dtoa(ctx, d, 0, JS_DTOA_TOSTRING); } else { - return js_dtoa(ctx, d, 10, f, JS_DTOA_FRAC_FORMAT); + return js_dtoa(ctx, d, f, JS_DTOA_FIXED); } } @@ -39033,8 +39118,8 @@ static JSValue js_number_toExponential(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSValue val; - int f, flags; double d; + int f; val = js_thisNumberValue(ctx, this_val); if (JS_IsException(val)) @@ -39043,21 +39128,15 @@ static JSValue js_number_toExponential(JSContext *ctx, JSValue this_val, return JS_EXCEPTION; if (JS_ToInt32Sat(ctx, &f, argv[0])) return JS_EXCEPTION; - if (!isfinite(d)) { - return JS_ToStringFree(ctx, js_float64(d)); - } - if (JS_IsUndefined(argv[0])) { - flags = 0; - f = 0; - } else { + if (!isfinite(d)) + return js_dtoa_infinite(ctx, d); + if (!JS_IsUndefined(argv[0])) { if (f < 0 || f > 100) { - return JS_ThrowRangeError(ctx, "%s() argument must be between 1 and 100", - "toExponential"); + return JS_ThrowRangeError(ctx, "toExponential() argument must be between 0 and 100"); } - f++; - flags = JS_DTOA_FIXED_FORMAT; + f += 1; /* number of significant digits between 1 and 101 */ } - return js_dtoa(ctx, d, 10, f, flags | JS_DTOA_FORCE_EXP); + return js_dtoa(ctx, d, f, JS_DTOA_EXPONENTIAL); } static JSValue js_number_toPrecision(JSContext *ctx, JSValue this_val, @@ -39073,18 +39152,15 @@ static JSValue js_number_toPrecision(JSContext *ctx, JSValue this_val, if (JS_ToFloat64Free(ctx, &d, val)) return JS_EXCEPTION; if (JS_IsUndefined(argv[0])) - goto to_string; + return js_dtoa(ctx, d, 0, JS_DTOA_TOSTRING); if (JS_ToInt32Sat(ctx, &p, argv[0])) return JS_EXCEPTION; - if (!isfinite(d)) { - to_string: - return JS_ToStringFree(ctx, js_float64(d)); - } + if (!isfinite(d)) + return js_dtoa_infinite(ctx, d); if (p < 1 || p > 100) { - return JS_ThrowRangeError(ctx, "%s() argument must be between 1 and 100", - "toPrecision"); + return JS_ThrowRangeError(ctx, "toPrecision() argument must be between 1 and 100"); } - return js_dtoa(ctx, d, 10, p, JS_DTOA_FIXED_FORMAT); + return js_dtoa(ctx, d, p, JS_DTOA_PRECISION); } static const JSCFunctionListEntry js_number_proto_funcs[] = { diff --git a/tests/microbench.js b/tests/microbench.js index f07305e..c8cb4c7 100644 --- a/tests/microbench.js +++ b/tests/microbench.js @@ -919,6 +919,42 @@ function float_toString(n) return n * 3; } +function float_toFixed(n) +{ + var s, r, j; + r = 0; + for(j = 0; j < n; j++) { + s = (j % 10 + 0.1).toFixed(j % 16); + s = (j + 0.1).toFixed(j % 16); + s = (j * 12345678 + 0.1).toFixed(j % 16); + } + return n * 3; +} + +function float_toPrecision(n) +{ + var s, r, j; + r = 0; + for(j = 0; j < n; j++) { + s = (j % 10 + 0.1).toPrecision(j % 16 + 1); + s = (j + 0.1).toPrecision(j % 16 + 1); + s = (j * 12345678 + 0.1).toPrecision(j % 16 + 1); + } + return n * 3; +} + +function float_toExponential(n) +{ + var s, r, j; + r = 0; + for(j = 0; j < n; j++) { + s = (j % 10 + 0.1).toExponential(j % 16); + s = (j + 0.1).toExponential(j % 16); + s = (j * 12345678 + 0.1).toExponential(j % 16); + } + return n * 3; +} + function string_to_int(n) { var s, r, j; @@ -1014,11 +1050,14 @@ function main(argc, argv, g) int_toString, float_to_string, float_toString, + float_toFixed, + float_toPrecision, + float_toExponential, string_to_int, string_to_float, ]; var tests = []; - var i, j, n, f, name; + var i, j, n, f, name, found; if (typeof BigInt == "function") { /* BigInt test */ @@ -1045,14 +1084,14 @@ function main(argc, argv, g) sort_bench.array_size = +argv[i++]; continue; } - for (j = 0; j < test_list.length; j++) { + for (j = 0, found = false; j < test_list.length; j++) { f = test_list[j]; - if (name === f.name) { + if (f.name.startsWith(name)) { tests.push(f); - break; + found = true; } } - if (j == test_list.length) { + if (!found) { console.log("unknown benchmark: " + name); return 1; } @@ -1080,6 +1119,9 @@ function main(argc, argv, g) save_result("microbench-new.txt", log_data); } -if (!scriptArgs) +if (typeof scriptArgs === "undefined") { scriptArgs = []; + if (typeof process.argv === "object") + scriptArgs = process.argv.slice(1); +} main(scriptArgs.length, scriptArgs, this); diff --git a/tests/test_builtin.js b/tests/test_builtin.js index 5a28e6e..d4b04bd 100644 --- a/tests/test_builtin.js +++ b/tests/test_builtin.js @@ -434,18 +434,17 @@ function test_number() assert(Number.isNaN(Number("-"))); assert(Number.isNaN(Number("\x00a"))); - // TODO: Fix rounding errors on Windows/Cygwin. - if (['win32', 'cygwin'].includes(os.platform)) { - return; - } - assert((1-2**-53).toString(12), "0.bbbbbbbbbbbbbba"); + assert((1000000000000000128).toString(), "1000000000000000100"); + assert((1000000000000000128).toFixed(0), "1000000000000000128"); assert((25).toExponential(0), "3e+1"); assert((-25).toExponential(0), "-3e+1"); assert((2.5).toPrecision(1), "3"); assert((-2.5).toPrecision(1), "-3"); assert((1.125).toFixed(2), "1.13"); assert((-1.125).toFixed(2), "-1.13"); + assert((0.5).toFixed(0), "1"); + assert((-0.5).toFixed(0), "-1"); } function test_eval2() diff --git a/tests/test_conv.c b/tests/test_conv.c index bb12a51..9761b30 100644 --- a/tests/test_conv.c +++ b/tests/test_conv.c @@ -864,6 +864,8 @@ static uint8_t const radix_shift[64] = { size_t u32toa_radix_length(char buf[minimum_length(33)], uint32_t n, unsigned base) { + int shift; + #ifdef USE_SPECIAL_RADIX_10 if (likely(base == 10)) return u32toa_length_loop(buf, n); @@ -873,13 +875,13 @@ size_t u32toa_radix_length(char buf[minimum_length(33)], uint32_t n, unsigned ba buf[1] = '\0'; return 1; } - int shift = radix_shift[base & 63]; + shift = radix_shift[base & 63]; if (shift) { uint32_t mask = (1 << shift) - 1; size_t len = (32 - clz32(n) + shift - 1) / shift; size_t last = n & mask; - n /= base; char *end = buf + len; + n >>= shift; *end-- = '\0'; *end-- = digits36[last]; while (n >= base) { @@ -913,11 +915,13 @@ size_t u32toa_radix_length(char buf[minimum_length(33)], uint32_t n, unsigned ba size_t u64toa_radix_length(char buf[minimum_length(65)], uint64_t n, unsigned base) { + int shift; + #ifdef USE_SPECIAL_RADIX_10 if (likely(base == 10)) return u64toa_length_loop(buf, n); #endif - int shift = radix_shift[base & 63]; + shift = radix_shift[base & 63]; if (shift) { if (n < base) { buf[0] = digits36[n]; @@ -927,8 +931,8 @@ size_t u64toa_radix_length(char buf[minimum_length(65)], uint64_t n, unsigned ba uint64_t mask = (1 << shift) - 1; size_t len = (64 - clz64(n) + shift - 1) / shift; size_t last = n & mask; - n /= base; char *end = buf + len; + n >>= shift; *end-- = '\0'; *end-- = digits36[last]; while (n >= base) { @@ -1511,6 +1515,9 @@ int main(int argc, char *argv[]) { clock_t times1[countof(impl1)][4][37]; char buf[100]; uint64_t bases = 0; +#define set_base(bases, b) (*(bases) |= (1ULL << (b))) +#define has_base(bases, b) ((bases) & (1ULL << (b))) +#define single_base(bases) (!((bases) & ((bases) - 1))) int verbose = 0; int average = 1; int enabled = 3; @@ -1521,14 +1528,13 @@ int main(int argc, char *argv[]) { for (int a = 1; a < argc; a++) { char *arg = argv[a]; if (isdigit((unsigned char)*arg)) { - verbose = 1; while (isdigit((unsigned char)*arg)) { int b1 = strtol(arg, &arg, 10); - bases |= (1ULL << b1); + set_base(&bases, b1); if (*arg == '-') { - int b2 = strtol(arg, &arg, 10); + int b2 = strtol(arg + 1, &arg, 10); while (++b1 <= b2) - bases |= (1ULL << b1); + set_base(&bases, b1); } if (*arg == ',') { arg++; @@ -1539,10 +1545,6 @@ int main(int argc, char *argv[]) { fprintf(stderr, "invalid option syntax: %s\n", argv[a]); return 2; } - if (!(bases & (bases - 1))) { /* single base */ - average = 0; - verbose = 1; - } continue; } else if (!strcmp(arg, "-t") || !strcmp(arg, "--terse")) { verbose = 0; @@ -1578,14 +1580,19 @@ int main(int argc, char *argv[]) { } if (!bases) bases = -1; - if (bases & (bases - 1)) /* multiple bases */ + if (single_base(bases)) { + average = 0; + verbose = 1; + } else { average = 1; + } int numvariant = 0; int numvariant1 = 0; int nerrors = 0; - if (bases & (1ULL << 10)) { + /* Checking for correctness */ + if (has_base(bases, 10)) { for (size_t i = 0; i < countof(impl); i++) { unsigned base = 10; if (impl[i].enabled & enabled) { @@ -1599,7 +1606,7 @@ int main(int argc, char *argv[]) { for (size_t i = 0; i < countof(impl1); i++) { if (impl1[i].enabled & enabled) { for (unsigned base = 2; base <= 36; base++) { - if (bases & (1ULL << base)) { + if (has_base(bases, base)) { CHECK(impl1[i], 1000, 0, 32, u32toa_radix(buf, x, base), strtoull(buf, NULL, base)); CHECK(impl1[i], 1000, 1, 32, i32toa_radix(buf, x, base), strtoll(buf, NULL, base)); CHECK(impl1[i], 1000, 0, 64, u64toa_radix(buf, x, base), strtoull(buf, NULL, base)); @@ -1611,13 +1618,14 @@ int main(int argc, char *argv[]) { if (nerrors) return 1; - if (bases & (1ULL << 10)) { + /* Timing conversions */ + if (has_base(bases, 10)) { for (int rep = 0; rep < 100; rep++) { for (size_t i = 0; i < countof(impl); i++) { if (impl[i].enabled & enabled) { numvariant++; #ifdef TEST_SNPRINTF - if (strstr(impl[i].name, "snprintf")) { // avoid function call overhead + if (strstr(impl[i].name, "snprintf")) { // avoid wrapper overhead TIME(times[i][0], 1000, 0, 32, snprintf(buf, 11, "%"PRIu32, x)); TIME(times[i][1], 1000, 1, 32, snprintf(buf, 12, "%"PRIi32, x)); TIME(times[i][2], 1000, 0, 64, snprintf(buf, 21, "%"PRIu64, x)); @@ -1639,42 +1647,42 @@ int main(int argc, char *argv[]) { if (impl1[i].enabled & enabled) { numvariant1++; #ifdef TEST_SNPRINTF - if (strstr(impl[i].name, "snprintf")) { // avoid function call overhead + if (strstr(impl[i].name, "snprintf")) { // avoid wrapper overhead #ifdef PRIb32 - if (bases & (1ULL << 1)) { + if (has_base(bases, 2)) { unsigned base = 2; - TIME(times1[i][0][2], 1000, 0, 32, snprintf(buf, 33, "%"PRIb32, x)); + TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIb32, x)); TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base)); - TIME(times1[i][2][2], 1000, 0, 64, snprintf(buf, 65, "%"PRIb64, x)); + TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 65, "%"PRIb64, x)); TIME(times1[i][3][base], 1000, 1, 64, impl1[i].i64toa_radix(buf, x, base)); } #endif - if (bases & (1ULL << 8)) { + if (has_base(bases, 8)) { unsigned base = 8; - TIME(times1[i][0][8], 1000, 0, 32, snprintf(buf, 33, "%"PRIo32, x)); + TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIo32, x)); TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base)); - TIME(times1[i][2][8], 1000, 0, 64, snprintf(buf, 65, "%"PRIo64, x)); + TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 65, "%"PRIo64, x)); TIME(times1[i][3][base], 1000, 1, 64, impl1[i].i64toa_radix(buf, x, base)); } - if (bases & (1ULL << 10)) { + if (has_base(bases, 10)) { unsigned base = 10; - TIME(times1[i][0][10], 1000, 0, 32, snprintf(buf, 33, "%"PRIu32, x)); - TIME(times1[i][1][10], 1000, 1, 32, snprintf(buf, 34, "%"PRIi32, x)); - TIME(times1[i][2][10], 1000, 0, 64, snprintf(buf, 64, "%"PRIu64, x)); - TIME(times1[i][3][10], 1000, 1, 64, snprintf(buf, 65, "%"PRIi64, x)); + TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIu32, x)); + TIME(times1[i][1][base], 1000, 1, 32, snprintf(buf, 34, "%"PRIi32, x)); + TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 64, "%"PRIu64, x)); + TIME(times1[i][3][base], 1000, 1, 64, snprintf(buf, 65, "%"PRIi64, x)); } - if (bases & (1ULL << 16)) { + if (has_base(bases, 16)) { unsigned base = 16; - TIME(times1[i][0][16], 1000, 0, 32, snprintf(buf, 33, "%"PRIx32, x)); + TIME(times1[i][0][base], 1000, 0, 32, snprintf(buf, 33, "%"PRIx32, x)); TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base)); - TIME(times1[i][2][16], 1000, 0, 64, snprintf(buf, 65, "%"PRIx64, x)); + TIME(times1[i][2][base], 1000, 0, 64, snprintf(buf, 65, "%"PRIx64, x)); TIME(times1[i][3][base], 1000, 1, 64, impl1[i].i64toa_radix(buf, x, base)); } } else #endif { for (unsigned base = 2; base <= 36; base++) { - if (bases & (1ULL << base)) { + if (has_base(bases, base)) { TIME(times1[i][0][base], 1000, 0, 32, impl1[i].u32toa_radix(buf, x, base)); TIME(times1[i][1][base], 1000, 1, 32, impl1[i].i32toa_radix(buf, x, base)); TIME(times1[i][2][base], 1000, 0, 64, impl1[i].u64toa_radix(buf, x, base)); @@ -1704,7 +1712,7 @@ int main(int argc, char *argv[]) { for (size_t i = 0; i < countof(impl1); i++) { int numbases = 0; for (unsigned base = 2; base <= 36; base++) { - if (bases & (1ULL << base)) { + if (has_base(bases, base)) { if (times1[i][0][base]) { numbases++; for (int j = 0; j < 4; j++)