From 42e3489abd88f22ae26a2d08e01eb90c034443c8 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Wed, 30 Sep 2026 10:12:50 +0200 Subject: [PATCH] Prefer strtod_l over libc++'s std::from_chars libc++'s std::from_chars for float and double is 1.3x to 2.8x slower per number than Apple's strtod_l, and it was tried before Clinger's fast path. With Apple clang in C++17 mode, parsing random doubles took 1.75x as long as on develop, short numbers such as 123.45 1.3x, and mesh.json 1.2x. Use libc++'s std::from_chars only where the C library has no strtod_l. On Apple platforms, floats are now converted by Clinger's fast path and strtod_l, which is 0.90x to 1.01x the time of develop. Signed-off-by: Niels Lohmann --- .../docs/features/types/number_handling.md | 10 +-- .../nlohmann/detail/input/number_parse.hpp | 80 ++++++++++--------- single_include/nlohmann/json.hpp | 80 ++++++++++--------- 3 files changed, 87 insertions(+), 83 deletions(-) diff --git a/docs/mkdocs/docs/features/types/number_handling.md b/docs/mkdocs/docs/features/types/number_handling.md index 7f4810469..c19a9152c 100644 --- a/docs/mkdocs/docs/features/types/number_handling.md +++ b/docs/mkdocs/docs/features/types/number_handling.md @@ -77,11 +77,11 @@ otherwise, it uses unsigned integer storage. [`std::strtod`](https://en.cppreference.com/w/cpp/string/byte/strtof), respectively. - The result of converting floating-point numbers does not depend on the C locale (`LC_NUMERIC`). They are converted with [`std::from_chars`](https://en.cppreference.com/w/cpp/utility/from_chars) where the standard - library implements it for the number type (including libc++ 20 or later for `#!c float` and `#!c double`), - otherwise with `strtod_l` and the "C" locale where the C library provides it (glibc, macOS, MSVC), and otherwise - with `std::strtod` and the decimal point of the current locale. Before version 3.13.0, the last way was used much - more often, and a locale whose decimal point is longer than one byte (e.g., `fa_IR.UTF-8`) truncated numbers at - the decimal point. + library implements it for the number type (with libc++ 20 or later, only for `#!c float` and `#!c double`, and + only where `strtod_l` is unavailable, because that is faster), otherwise with `strtod_l` and the "C" locale where + the C library provides it (glibc, macOS, MSVC), and otherwise with `std::strtod` and the decimal point of the + current locale. Before version 3.13.0, the last way was used much more often, and a locale whose decimal point + is longer than one byte (e.g., `fa_IR.UTF-8`) truncated numbers at the decimal point. !!! example "Examples" diff --git a/include/nlohmann/detail/input/number_parse.hpp b/include/nlohmann/detail/input/number_parse.hpp index 86e5df742..2e0e13bd7 100644 --- a/include/nlohmann/detail/input/number_parse.hpp +++ b/include/nlohmann/detail/input/number_parse.hpp @@ -20,45 +20,6 @@ #include -// std::from_chars lives in , but being in C++17 mode does not -// guarantee the header exists: GCC 7 sets __cplusplus to C++17 yet ships no -// (added in GCC 8; floating-point support in GCC 11). Guard the -// include with __has_include so such toolchains fall back to the scalar path. -#if defined(JSON_HAS_CPP_17) && defined(__has_include) - #if __has_include() - #include // from_chars - #include // errc - - // std::from_chars is used for floating-point numbers - // - for float, double, and long double if __cpp_lib_to_chars announces - // complete support (only checked in C++17 or later: some standard - // libraries, e.g. libstdc++ 15, define it even in C++14 mode, where - // is not included); - // - for float and double with libc++ 20 or later, which does not define - // __cpp_lib_to_chars because long double is missing. On Apple - // platforms, the implementation is part of the system's libc++ and - // only available when deploying to macOS/iOS 26 or later; for older - // deployment targets, _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT - // is 0, and the fallbacks below are used. - #if defined(__cpp_lib_to_chars) - #define JSON_HAS_FLOAT_FROM_CHARS 1 - #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 1 - #elif defined(_LIBCPP_VERSION) && defined(_LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT) - #if _LIBCPP_VERSION >= 200000 && _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT - #define JSON_HAS_FLOAT_FROM_CHARS 1 - #endif - #endif - #endif -#endif - -#ifndef JSON_HAS_FLOAT_FROM_CHARS - #define JSON_HAS_FLOAT_FROM_CHARS 0 -#endif - -#ifndef JSON_HAS_LONG_DOUBLE_FROM_CHARS - #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 0 -#endif - // strtof_l/strtod_l/strtold_l convert with a given locale object instead of the // global C locale. They are not part of ISO C or C++, so they are only used where // the C library is known to declare them: Microsoft's UCRT (as _strtod_l etc.), @@ -77,6 +38,47 @@ #define JSON_HAS_C_LOCALE_STRTOD 0 #endif +// std::from_chars lives in , but being in C++17 mode does not +// guarantee the header exists: GCC 7 sets __cplusplus to C++17 yet ships no +// (added in GCC 8; floating-point support in GCC 11). Guard the +// include with __has_include so such toolchains fall back to the scalar path. +#if defined(JSON_HAS_CPP_17) && defined(__has_include) + #if __has_include() + #include // from_chars + #include // errc + + // std::from_chars is used for floating-point numbers + // - for float, double, and long double if __cpp_lib_to_chars announces + // complete support (only checked in C++17 or later: some standard + // libraries, e.g. libstdc++ 15, define it even in C++14 mode, where + // is not included); + // - for float and double with libc++ 20 or later, which does not define + // __cpp_lib_to_chars because long double is missing, but only where + // the C library offers no strtod_l: libc++'s implementation is slower + // than Apple's strtod_l (by 1.3x to 2.8x per number), and it would be + // tried before Clinger's fast path. On Apple platforms, it is also only + // available when deploying to macOS/iOS 26 or later; for older + // deployment targets, _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT + // is 0. + #if defined(__cpp_lib_to_chars) + #define JSON_HAS_FLOAT_FROM_CHARS 1 + #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 1 + #elif !JSON_HAS_C_LOCALE_STRTOD && defined(_LIBCPP_VERSION) && defined(_LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT) + #if _LIBCPP_VERSION >= 200000 && _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT + #define JSON_HAS_FLOAT_FROM_CHARS 1 + #endif + #endif + #endif +#endif + +#ifndef JSON_HAS_FLOAT_FROM_CHARS + #define JSON_HAS_FLOAT_FROM_CHARS 0 +#endif + +#ifndef JSON_HAS_LONG_DOUBLE_FROM_CHARS + #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 0 +#endif + // This file contains the value-conversion helpers used by the lexer to turn an // already-validated number token into a value, where possible without the // locale/errno overhead of std::strtoull/std::strtod. They are free functions so diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 5e1c200bd..5fc8743a1 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -8505,45 +8505,6 @@ NLOHMANN_JSON_NAMESPACE_END // #include -// std::from_chars lives in , but being in C++17 mode does not -// guarantee the header exists: GCC 7 sets __cplusplus to C++17 yet ships no -// (added in GCC 8; floating-point support in GCC 11). Guard the -// include with __has_include so such toolchains fall back to the scalar path. -#if defined(JSON_HAS_CPP_17) && defined(__has_include) - #if __has_include() - #include // from_chars - #include // errc - - // std::from_chars is used for floating-point numbers - // - for float, double, and long double if __cpp_lib_to_chars announces - // complete support (only checked in C++17 or later: some standard - // libraries, e.g. libstdc++ 15, define it even in C++14 mode, where - // is not included); - // - for float and double with libc++ 20 or later, which does not define - // __cpp_lib_to_chars because long double is missing. On Apple - // platforms, the implementation is part of the system's libc++ and - // only available when deploying to macOS/iOS 26 or later; for older - // deployment targets, _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT - // is 0, and the fallbacks below are used. - #if defined(__cpp_lib_to_chars) - #define JSON_HAS_FLOAT_FROM_CHARS 1 - #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 1 - #elif defined(_LIBCPP_VERSION) && defined(_LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT) - #if _LIBCPP_VERSION >= 200000 && _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT - #define JSON_HAS_FLOAT_FROM_CHARS 1 - #endif - #endif - #endif -#endif - -#ifndef JSON_HAS_FLOAT_FROM_CHARS - #define JSON_HAS_FLOAT_FROM_CHARS 0 -#endif - -#ifndef JSON_HAS_LONG_DOUBLE_FROM_CHARS - #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 0 -#endif - // strtof_l/strtod_l/strtold_l convert with a given locale object instead of the // global C locale. They are not part of ISO C or C++, so they are only used where // the C library is known to declare them: Microsoft's UCRT (as _strtod_l etc.), @@ -8562,6 +8523,47 @@ NLOHMANN_JSON_NAMESPACE_END #define JSON_HAS_C_LOCALE_STRTOD 0 #endif +// std::from_chars lives in , but being in C++17 mode does not +// guarantee the header exists: GCC 7 sets __cplusplus to C++17 yet ships no +// (added in GCC 8; floating-point support in GCC 11). Guard the +// include with __has_include so such toolchains fall back to the scalar path. +#if defined(JSON_HAS_CPP_17) && defined(__has_include) + #if __has_include() + #include // from_chars + #include // errc + + // std::from_chars is used for floating-point numbers + // - for float, double, and long double if __cpp_lib_to_chars announces + // complete support (only checked in C++17 or later: some standard + // libraries, e.g. libstdc++ 15, define it even in C++14 mode, where + // is not included); + // - for float and double with libc++ 20 or later, which does not define + // __cpp_lib_to_chars because long double is missing, but only where + // the C library offers no strtod_l: libc++'s implementation is slower + // than Apple's strtod_l (by 1.3x to 2.8x per number), and it would be + // tried before Clinger's fast path. On Apple platforms, it is also only + // available when deploying to macOS/iOS 26 or later; for older + // deployment targets, _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT + // is 0. + #if defined(__cpp_lib_to_chars) + #define JSON_HAS_FLOAT_FROM_CHARS 1 + #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 1 + #elif !JSON_HAS_C_LOCALE_STRTOD && defined(_LIBCPP_VERSION) && defined(_LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT) + #if _LIBCPP_VERSION >= 200000 && _LIBCPP_AVAILABILITY_HAS_FROM_CHARS_FLOATING_POINT + #define JSON_HAS_FLOAT_FROM_CHARS 1 + #endif + #endif + #endif +#endif + +#ifndef JSON_HAS_FLOAT_FROM_CHARS + #define JSON_HAS_FLOAT_FROM_CHARS 0 +#endif + +#ifndef JSON_HAS_LONG_DOUBLE_FROM_CHARS + #define JSON_HAS_LONG_DOUBLE_FROM_CHARS 0 +#endif + // This file contains the value-conversion helpers used by the lexer to turn an // already-validated number token into a value, where possible without the // locale/errno overhead of std::strtoull/std::strtod. They are free functions so