[Peter Dimov]
lexical_cast does a good job of optimizing the from_chars and to_chars cases when it detects it can do so, but that by definition can't be better than the programmer just calling the primitive operations from_chars and to_chars directly, as they are (a) locale-independent, (b) non- allocating, (c) non-throwing.
I wondered how lexical_cast's "good job" (limited by the algorithms at the time, not its fault!) compared to charconv, so I dusted off the performance comparison I wrote for CppCon 2019 and added lexical_cast. MSVC's charconv is approximately 14x (for float) and 25x (for double) faster - that's times, not percent. (Thanks to Ryu by Ulf Adams.) This is with avoiding dynamic memory allocation in lexical_cast by converting into a small std::array. My absolute numbers (in a Ryzen 5950X VM): 452.3 ns | Boost float 1003.0 ns | Boost double 32.1 ns | STL float plain shortest 39.9 ns | STL double plain shortest Here's my full test case, built with VS 2022 17.6 Preview 7 x64 (note that x64 vs. x86 makes a HUGE difference to charconv). I didn't bother to compare the from_chars scenario (as MSVC doesn't use a highly optimized algorithm there; investigating Eisel-Lemire is on our todo list). C:\Temp>type charconv.cpp // cl /std:c++17 /EHsc /nologo /W4 /MT /O2 /Iboost_1_82_0 charconv.cpp && charconv #ifndef _MSC_VER #define AVOID_CHARCONV #define AVOID_SPRINTF_S #endif // _MSC_VER #ifndef AVOID_CHARCONV #include <charconv> #endif // AVOID_CHARCONV #include <array> #include <chrono> #include <exception> #include <random> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <system_error> #include <type_traits> #include <vector> #include <boost/lexical_cast.hpp> using namespace std; using namespace std::chrono; void verify(const bool b) { if (!b) { puts("FAIL"); exit(EXIT_FAILURE); } } enum class RoundTrip { Sci, Fix, Gen, Hex, Lossy }; constexpr size_t N = 2'000'000; // how many floating-point values to test constexpr size_t K = 5; // how many times to repeat the test, for cleaner timing constexpr size_t BufSize = 2'000; // more than enough unsigned int global_dummy = 0; template <typename Floating> void test_lexical_cast(const char* const str, const vector<Floating>& vec) { const auto start = steady_clock::now(); for (size_t k = 0; k < K; ++k) { for (const auto& elem : vec) { const auto ret = boost::lexical_cast<array<char, 25>>(elem); // "-1.2345678901234567e-100" plus null term global_dummy += static_cast<unsigned int>(ret[0]); } } const auto finish = steady_clock::now(); printf("%6.1f ns | %s\n", duration<double, nano>{finish - start}.count() / (N * K), str); } template <typename Floating> int sprintf_wrapper(char (&buf)[BufSize], const char* const fmt, const Floating elem) { #ifdef AVOID_SPRINTF_S return sprintf(buf, fmt, elem); #else // AVOID_SPRINTF_S return sprintf_s(buf, BufSize, fmt, elem); #endif // AVOID_SPRINTF_S } template <RoundTrip RT, typename Floating> void test_sprintf(const char* const str, const vector<Floating>& vec, const char* const fmt) { char buf[BufSize]; const auto start = steady_clock::now(); for (size_t k = 0; k < K; ++k) { for (const auto& elem : vec) { const int ret = sprintf_wrapper(buf, fmt, elem); global_dummy += static_cast<unsigned int>(ret); global_dummy += static_cast<unsigned int>(buf[0]); } } const auto finish = steady_clock::now(); printf("%6.1f ns | %s\n", duration<double, nano>{finish - start}.count() / (N * K), str); for (const auto& elem : vec) { verify(sprintf_wrapper(buf, fmt, elem) != -1); if constexpr (RT == RoundTrip::Lossy) { // skip lossy conversions } else if constexpr (is_same_v<Floating, float>) { verify(strtof(buf, nullptr) == elem); } else { verify(strtod(buf, nullptr) == elem); } } } #ifndef AVOID_CHARCONV constexpr chars_format chars_format_from_RoundTrip(const RoundTrip rt) { switch (rt) { case RoundTrip::Sci: return chars_format::scientific; case RoundTrip::Fix: return chars_format::fixed; case RoundTrip::Gen: return chars_format::general; case RoundTrip::Hex: return chars_format::hex; case RoundTrip::Lossy: default: puts("FAIL"); exit(EXIT_FAILURE); } } template <RoundTrip RT, typename Floating, typename... Args> void test_to_chars(const char* const str, const vector<Floating>& vec, const Args&... args) { char buf[BufSize]; const auto start = steady_clock::now(); for (size_t k = 0; k < K; ++k) { for (const auto& elem : vec) { const auto result = to_chars(buf, buf + BufSize, elem, args...); global_dummy += static_cast<unsigned int>(result.ptr - buf); global_dummy += static_cast<unsigned int>(buf[0]); } } const auto finish = steady_clock::now(); printf("%6.1f ns | %s\n", duration<double, nano>{finish - start}.count() / (N * K), str); for (const auto& elem : vec) { const auto result = to_chars(buf, buf + BufSize, elem, args...); verify(result.ec == errc{}); if constexpr (RT == RoundTrip::Lossy) { // skip lossy conversions } else { Floating round_trip; const auto from_result = from_chars(buf, result.ptr, round_trip, chars_format_from_RoundTrip(RT)); verify(from_result.ec == errc{}); verify(from_result.ptr == result.ptr); verify(round_trip == elem); } } } #endif // AVOID_CHARCONV template <RoundTrip RT, typename Floating> vector<char> prepare_strings(const vector<Floating>& vec) { vector<char> output; char buf[BufSize]; for (const auto& elem : vec) { int ret; if constexpr (RT == RoundTrip::Sci) { if constexpr (is_same_v<Floating, float>) { ret = sprintf_wrapper(buf, "%.8e", elem); } else { ret = sprintf_wrapper(buf, "%.16e", elem); } } else { static_assert(RT == RoundTrip::Hex); if constexpr (is_same_v<Floating, float>) { ret = sprintf_wrapper(buf, "%.6a", elem); } else { ret = sprintf_wrapper(buf, "%.13a", elem); } } verify(ret != -1); output.insert(output.end(), buf, buf + ret + 1); // include null terminator } return output; } template <typename Floating> void test_strtox(const char* const str, const vector<Floating>& original, const vector<char>& strings) { vector<Floating> round_trip(N); const auto start = steady_clock::now(); for (size_t k = 0; k < K; ++k) { const char* ptr = strings.data(); char* endptr = nullptr; for (size_t n = 0; n < N; ++n) { if constexpr (is_same_v<Floating, float>) { round_trip[n] = strtof(ptr, &endptr); } else { round_trip[n] = strtod(ptr, &endptr); } ptr = endptr + 1; // advance past null terminator } } const auto finish = steady_clock::now(); printf("%6.1f ns | %s\n", duration<double, nano>{finish - start}.count() / (N * K), str); verify(round_trip == original); } #ifndef AVOID_CHARCONV vector<char> erase_0x(const vector<char>& strings) { vector<char> output; output.reserve(strings.size() - 2 * N); for (auto i = strings.begin(); i != strings.end();) { if (*i == '-') { output.push_back('-'); i += 3; // advance past "-0x"; } else { i += 2; // advance past "0x"; } for (;;) { const char c = *i++; output.push_back(c); if (c == '\0') { break; } } } return output; } template <RoundTrip RT, typename Floating> void test_from_chars(const char* const str, const vector<Floating>& original, const vector<char>& strings) { const char* const last = strings.data() + strings.size(); vector<Floating> round_trip(N); const auto start = steady_clock::now(); for (size_t k = 0; k < K; ++k) { const char* first = strings.data(); for (size_t n = 0; n < N; ++n) { const auto from_result = from_chars(first, last, round_trip[n], chars_format_from_RoundTrip(RT)); first = from_result.ptr + 1; // advance past null terminator } } const auto finish = steady_clock::now(); printf("%6.1f ns | %s\n", duration<double, nano>{finish - start}.count() / (N * K), str); verify(round_trip == original); } #endif // AVOID_CHARCONV void test_all() { #if defined(__clang__) && defined(_M_IX86) const char* const toolset = "Clang/LLVM x86 + MSVC STL"; #elif defined(__clang__) && defined(_M_X64) const char* const toolset = "Clang/LLVM x64 + MSVC STL"; #elif !defined(__clang__) && defined(_M_IX86) const char* const toolset = "C1XX/C2 x86 + MSVC STL"; #elif !defined(__clang__) && defined(_M_X64) const char* const toolset = "C1XX/C2 x64 + MSVC STL"; #else const char* const toolset = "Unknown Toolset"; #endif puts(toolset); vector<float> vec_flt; vector<double> vec_dbl; { mt19937_64 mt64; vec_flt.reserve(N); while (vec_flt.size() < N) { const uint32_t val = static_cast<uint32_t>(mt64()); constexpr uint32_t inf_nan = 0x7F800000U; if ((val & inf_nan) == inf_nan) { continue; // skip INF/NAN } float flt; static_assert(sizeof(flt) == sizeof(val)); memcpy(&flt, &val, sizeof(flt)); vec_flt.push_back(flt); } vec_dbl.reserve(N); while (vec_dbl.size() < N) { const uint64_t val = mt64(); constexpr uint64_t inf_nan = 0x7FF0000000000000ULL; if ((val & inf_nan) == inf_nan) { continue; // skip INF/NAN } double dbl; static_assert(sizeof(dbl) == sizeof(val)); memcpy(&dbl, &val, sizeof(dbl)); vec_dbl.push_back(dbl); } } test_lexical_cast("Boost float", vec_flt); test_lexical_cast("Boost double", vec_dbl); test_sprintf<RoundTrip::Sci>("CRT float scientific 8", vec_flt, "%.8e"); test_sprintf<RoundTrip::Sci>("CRT double scientific 16", vec_dbl, "%.16e"); test_sprintf<RoundTrip::Lossy>("CRT float fixed 6 (lossy)", vec_flt, "%f"); test_sprintf<RoundTrip::Lossy>("CRT double fixed 6 (lossy)", vec_dbl, "%f"); test_sprintf<RoundTrip::Gen>("CRT float general 9", vec_flt, "%.9g"); test_sprintf<RoundTrip::Gen>("CRT double general 17", vec_dbl, "%.17g"); test_sprintf<RoundTrip::Hex>("CRT float hex 6", vec_flt, "%.6a"); test_sprintf<RoundTrip::Hex>("CRT double hex 13", vec_dbl, "%.13a"); #ifndef AVOID_CHARCONV test_to_chars<RoundTrip::Gen>("STL float plain shortest", vec_flt); test_to_chars<RoundTrip::Gen>("STL double plain shortest", vec_dbl); test_to_chars<RoundTrip::Sci>("STL float scientific shortest", vec_flt, chars_format::scientific); test_to_chars<RoundTrip::Sci>("STL double scientific shortest", vec_dbl, chars_format::scientific); test_to_chars<RoundTrip::Fix>("STL float fixed shortest", vec_flt, chars_format::fixed); test_to_chars<RoundTrip::Fix>("STL double fixed shortest", vec_dbl, chars_format::fixed); test_to_chars<RoundTrip::Gen>("STL float general shortest", vec_flt, chars_format::general); test_to_chars<RoundTrip::Gen>("STL double general shortest", vec_dbl, chars_format::general); test_to_chars<RoundTrip::Hex>("STL float hex shortest", vec_flt, chars_format::hex); test_to_chars<RoundTrip::Hex>("STL double hex shortest", vec_dbl, chars_format::hex); test_to_chars<RoundTrip::Sci>("STL float scientific 8", vec_flt, chars_format::scientific, 8); test_to_chars<RoundTrip::Sci>("STL double scientific 16", vec_dbl, chars_format::scientific, 16); test_to_chars<RoundTrip::Lossy>("STL float fixed 6 (lossy)", vec_flt, chars_format::fixed, 6); test_to_chars<RoundTrip::Lossy>("STL double fixed 6 (lossy)", vec_dbl, chars_format::fixed, 6); test_to_chars<RoundTrip::Gen>("STL float general 9", vec_flt, chars_format::general, 9); test_to_chars<RoundTrip::Gen>("STL double general 17", vec_dbl, chars_format::general, 17); test_to_chars<RoundTrip::Hex>("STL float hex 6", vec_flt, chars_format::hex, 6); test_to_chars<RoundTrip::Hex>("STL double hex 13", vec_dbl, chars_format::hex, 13); #endif // AVOID_CHARCONV puts("----------"); const vector<char> strings_sci_flt = prepare_strings<RoundTrip::Sci>(vec_flt); const vector<char> strings_sci_dbl = prepare_strings<RoundTrip::Sci>(vec_dbl); const vector<char> strings_hex_flt = prepare_strings<RoundTrip::Hex>(vec_flt); const vector<char> strings_hex_dbl = prepare_strings<RoundTrip::Hex>(vec_dbl); test_strtox("CRT strtof float scientific", vec_flt, strings_sci_flt); test_strtox("CRT strtod double scientific", vec_dbl, strings_sci_dbl); test_strtox("CRT strtof float hex", vec_flt, strings_hex_flt); test_strtox("CRT strtod double hex", vec_dbl, strings_hex_dbl); #ifndef AVOID_CHARCONV test_from_chars<RoundTrip::Sci>("STL from_chars float scientific", vec_flt, strings_sci_flt); test_from_chars<RoundTrip::Sci>("STL from_chars double scientific", vec_dbl, strings_sci_dbl); test_from_chars<RoundTrip::Hex>("STL from_chars float hex", vec_flt, erase_0x(strings_hex_flt)); test_from_chars<RoundTrip::Hex>("STL from_chars double hex", vec_dbl, erase_0x(strings_hex_dbl)); #endif // AVOID_CHARCONV printf("global_dummy: %u\n", global_dummy); } int main() { try { test_all(); } catch (const exception& e) { printf("Exception: %s\n", e.what()); } catch (...) { printf("Unknown exception.\n"); } } C:\Temp>cl /std:c++17 /EHsc /nologo /W4 /MT /O2 /Iboost_1_82_0 charconv.cpp && charconv charconv.cpp C1XX/C2 x64 + MSVC STL 452.3 ns | Boost float 1003.0 ns | Boost double 222.7 ns | CRT float scientific 8 381.3 ns | CRT double scientific 16 231.3 ns | CRT float fixed 6 (lossy) 818.3 ns | CRT double fixed 6 (lossy) 260.1 ns | CRT float general 9 807.6 ns | CRT double general 17 96.0 ns | CRT float hex 6 117.4 ns | CRT double hex 13 32.1 ns | STL float plain shortest 39.9 ns | STL double plain shortest 31.2 ns | STL float scientific shortest 39.7 ns | STL double scientific shortest 37.6 ns | STL float fixed shortest 111.7 ns | STL double fixed shortest 31.8 ns | STL float general shortest 39.9 ns | STL double general shortest 14.9 ns | STL float hex shortest 18.3 ns | STL double hex shortest 48.9 ns | STL float scientific 8 57.4 ns | STL double scientific 16 35.2 ns | STL float fixed 6 (lossy) 93.2 ns | STL double fixed 6 (lossy) 59.4 ns | STL float general 9 70.4 ns | STL double general 17 16.2 ns | STL float hex 6 19.1 ns | STL double hex 13 ---------- 103.7 ns | CRT strtof float scientific 191.2 ns | CRT strtod double scientific 56.6 ns | CRT strtof float hex 86.0 ns | CRT strtod double hex 77.2 ns | STL from_chars float scientific 154.3 ns | STL from_chars double scientific 26.2 ns | STL from_chars float hex 30.1 ns | STL from_chars double hex global_dummy: 3715056096 Hope this helps, STL
participants (1)
-
Stephan T. Lavavej