AK+LibJS: Replace home-grown Ryu implementation with fmt's dragonbox
In the benchmark added here, fmt's dragonbox is ~3x faster than our own Ryu implementation (1197ms for dragonbox vs. 3435ms for Ryu). Daniel Lemire recently published an article about these algorithms: https://lemire.me/blog/2026/02/01/converting-floats-to-strings-quickly/ In this article, fmt's dragonbox implementation is actually one of the slower ones (with the caveat that some comments note that the article is a bit out-of-date). I've gone with fmt here because: 1. It has a readily available recent version on vcpkg. 2. It provides the methods we need to actually convert a floating point to decimal exponential form. 3. There is an ongoing effort to replace dragonbox with a new algorithm, zmij, which promises to be faster. 4. It is one of the only users of AK/UFixedBigInt, so we can potentially remove that as well soon. 5. Bringing in fmt opens the door to replacing a bunch of AK::format facilities with fmt as well.
This commit is contained in:
parent
721c05802c
commit
3355fb39ae
13 changed files with 139 additions and 1247 deletions
|
|
@ -25,7 +25,6 @@ set(SOURCES
|
|||
StringBase.cpp
|
||||
StringBuilder.cpp
|
||||
StringConversions.cpp
|
||||
StringFloatingPointConversions.cpp
|
||||
StringUtils.cpp
|
||||
StringView.cpp
|
||||
Time.cpp
|
||||
|
|
@ -73,6 +72,9 @@ target_link_libraries(AK PRIVATE simdutf::simdutf)
|
|||
find_package(FastFloat CONFIG REQUIRED)
|
||||
target_link_libraries(AK PRIVATE FastFloat::fast_float)
|
||||
|
||||
find_package(fmt CONFIG REQUIRED)
|
||||
target_link_libraries(AK PRIVATE fmt::fmt)
|
||||
|
||||
# FIXME: Make this generic for all imported shared library dependencies and apply globally
|
||||
if (BUILD_SHARED_LIBS AND NOT CMAKE_SKIP_INSTALL_RULES AND NOT "${VCPKG_INSTALLED_DIR}" STREQUAL "")
|
||||
install(IMPORTED_RUNTIME_ARTIFACTS simdutf::simdutf
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
#include <AK/LexicalPath.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/StringFloatingPointConversions.h>
|
||||
#include <AK/StringConversions.h>
|
||||
#include <AK/Time.h>
|
||||
#include <math.h>
|
||||
#include <pthread.h>
|
||||
|
|
@ -592,7 +592,7 @@ ErrorOr<void> FormatBuilder::put_f32_or_f64(
|
|||
return put_string(special_case_builder.string_view(), align, min_width, NumericLimits<size_t>::max(), fill);
|
||||
}
|
||||
|
||||
auto const [sign, mantissa, exponent] = convert_floating_point_to_decimal_exponential_form(value);
|
||||
auto const [sign, mantissa, exponent] = convert_to_decimal_exponential_form(value);
|
||||
|
||||
auto convert_to_decimal_digits_array = [](auto x, auto& digits) -> size_t {
|
||||
size_t length = 0;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2022, David Tuin <davidot@serenityos.org>
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
* Copyright (c) 2025-2026, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/FloatingPoint.h>
|
||||
#include <AK/StringConversions.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/Utf16View.h>
|
||||
#include <math.h>
|
||||
|
||||
#include <fast_float/fast_float.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace AK {
|
||||
|
||||
|
|
@ -151,4 +154,20 @@ ENUMERATE_INTEGRAL_TYPES
|
|||
ENUMERATE_INTEGRAL_TYPES
|
||||
#undef __ENUMERATE_TYPE
|
||||
|
||||
template<FloatingPoint T>
|
||||
DecimalExponentialForm convert_to_decimal_exponential_form(T value)
|
||||
{
|
||||
ASSERT(!isinf(value));
|
||||
ASSERT(!isnan(value));
|
||||
|
||||
FloatExtractor<T> extractor;
|
||||
extractor.d = value;
|
||||
|
||||
auto [significand, exponent] = fmt::detail::dragonbox::to_decimal(value);
|
||||
return { static_cast<bool>(extractor.sign), significand, exponent };
|
||||
}
|
||||
|
||||
template DecimalExponentialForm convert_to_decimal_exponential_form(float);
|
||||
template DecimalExponentialForm convert_to_decimal_exponential_form(double);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (c) 2022, David Tuin <davidot@serenityos.org>
|
||||
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
|
||||
* Copyright (c) 2025-2026, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
|
@ -37,4 +37,15 @@ Optional<T> parse_hexadecimal_number(StringView, TrimWhitespace = TrimWhitespace
|
|||
template<Integral T>
|
||||
Optional<T> parse_hexadecimal_number(Utf16View const&, TrimWhitespace = TrimWhitespace::Yes);
|
||||
|
||||
struct DecimalExponentialForm {
|
||||
constexpr bool operator==(DecimalExponentialForm const& other) const = default;
|
||||
|
||||
bool sign { false };
|
||||
u64 fraction { 0 };
|
||||
i32 exponent { 0 };
|
||||
};
|
||||
|
||||
template<FloatingPoint T>
|
||||
DecimalExponentialForm convert_to_decimal_exponential_form(T value);
|
||||
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Dan Klishch <danilklishch@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Concepts.h>
|
||||
#include <AK/Types.h>
|
||||
|
||||
namespace AK {
|
||||
|
||||
struct FloatingPointExponentialForm {
|
||||
bool sign;
|
||||
u64 fraction;
|
||||
i32 exponent;
|
||||
};
|
||||
|
||||
/// This function finds the representation of `value' in the form of
|
||||
/// `(-1) ^ sign * fraction * 10 ^ exponent', such that (applying in the order of enumeration):
|
||||
///
|
||||
/// 1. sign is either 0 or 1, fraction is a non-negative number, exponent is an integer.
|
||||
/// 2. For +0, it is {.sign = 0, .fraction = 0, .exponent = 0},
|
||||
/// for -0, is {.sign = 1, .fraction = 0, .exponent = 0},
|
||||
/// for +inf, -inf, and NaN is undefined.
|
||||
/// 3. `(-1) ^ sign * fraction * 10 ^ exponent', computed with an infinite precision, rounds to
|
||||
/// `value' in the half to even mode.
|
||||
/// 4. len(str(fraction)) is minimal.
|
||||
/// 5. `abs((-1) ^ sign * fraction * 10 ^ exponent - value)' is minimal.
|
||||
/// 6. fraction is even.
|
||||
template<FloatingPoint T>
|
||||
FloatingPointExponentialForm convert_floating_point_to_decimal_exponential_form(T value);
|
||||
|
||||
}
|
||||
|
||||
#if USING_AK_GLOBALLY
|
||||
using AK::convert_floating_point_to_decimal_exponential_form;
|
||||
#endif
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
#include <AK/Array.h>
|
||||
#include <AK/FloatingPoint.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/StringFloatingPointConversions.h>
|
||||
#include <AK/StringConversions.h>
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <LibCrypto/BigInt/UnsignedBigInteger.h>
|
||||
#include <LibJS/Runtime/AbstractOperations.h>
|
||||
|
|
@ -102,7 +102,7 @@ static SignificandAndExponent compute_significand_and_exponent_with_precision(do
|
|||
static auto TWO_BIGINT = 2_bigint;
|
||||
static auto TEN_BIGINT = 10_bigint;
|
||||
|
||||
auto result = convert_floating_point_to_decimal_exponential_form(number);
|
||||
auto result = AK::convert_to_decimal_exponential_form(number);
|
||||
auto exponent = result.exponent + count_digits(result.fraction) - 1;
|
||||
|
||||
// Decompose the number into its exact binary representation. An IEEE-754 double is exactly equal to:
|
||||
|
|
@ -287,7 +287,7 @@ JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_exponential)
|
|||
// i. Let e, n, and f be integers such that f ≥ 0, 10^f ≤ n < 10^(f+1), 𝔽(n × 10^(e-f)) is 𝔽(x), and f is
|
||||
// as small as possible. Note that the decimal representation of n has f + 1 digits, n is not divisible
|
||||
// by 10, and the least significant digit of n is not necessarily uniquely determined by these criteria.
|
||||
auto result = convert_floating_point_to_decimal_exponential_form(number);
|
||||
auto result = AK::convert_to_decimal_exponential_form(number);
|
||||
|
||||
significand = result.fraction;
|
||||
fraction_digits = count_digits(result.fraction) - 1;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
#include <AK/ByteString.h>
|
||||
#include <AK/CharacterTypes.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/StringFloatingPointConversions.h>
|
||||
#include <AK/StringConversions.h>
|
||||
#include <AK/Utf16String.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <LibCrypto/BigInt/SignedBigInteger.h>
|
||||
|
|
@ -104,14 +104,13 @@ void number_to_string(StringBuilder& builder, double d, NumberToStringMode mode)
|
|||
return;
|
||||
}
|
||||
|
||||
// 5. Let n, k, and s be integers such that k ≥ 1, radix ^ (k - 1) ≤ s < radix ^ k,
|
||||
// 𝔽(s × radix ^ (n - k)) is x, and k is as small as possible. Note that k is the number of
|
||||
// digits in the representation of s using radix radix, that s is not divisible by radix, and
|
||||
// that the least significant digit of s is not necessarily uniquely determined by these criteria.
|
||||
// 5. Let n, k, and s be integers such that k ≥ 1, radix ^ (k - 1) ≤ s < radix ^ k, 𝔽(s × radix ^ (n - k)) is x, and
|
||||
// k is as small as possible. Note that k is the number of digits in the representation of s using radix radix,
|
||||
// that s is not divisible by radix, and that the least significant digit of s is not necessarily uniquely
|
||||
// determined by these criteria.
|
||||
//
|
||||
// Note: guarantees provided by convert_floating_point_to_decimal_exponential_form satisfy
|
||||
// requirements of NOTE 2.
|
||||
auto [sign, mantissa, exponent] = convert_floating_point_to_decimal_exponential_form(d);
|
||||
// NB: guarantees provided by convert_to_decimal_exponential_form satisfy requirements of NOTE 2.
|
||||
auto [sign, mantissa, exponent] = AK::convert_to_decimal_exponential_form(d);
|
||||
i32 k = 0;
|
||||
AK::Array<char, 20> mantissa_digits;
|
||||
convert_to_decimal_digits_array(mantissa, mantissa_digits, k);
|
||||
|
|
|
|||
|
|
@ -187,6 +187,25 @@
|
|||
"./angle-install.sh"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fmt",
|
||||
"buildsystem": "cmake-ninja",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/fmtlib/fmt.git",
|
||||
"tag": "12.1.0"
|
||||
}
|
||||
],
|
||||
"config-opts": [
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DCMAKE_PREFIX_PATH=/app",
|
||||
"-DCMAKE_INSTALL_LIBDIR=lib",
|
||||
"-DBUILD_SHARED_LIBS=ON",
|
||||
"-DFMT_TEST=OFF",
|
||||
"-DFMT_DOC=OFF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "icu",
|
||||
"buildsystem": "autotools",
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ set(AK_TEST_SOURCES
|
|||
TestStdLibExtras.cpp
|
||||
TestString.cpp
|
||||
TestStringConversions.cpp
|
||||
TestStringFloatingPointConversions.cpp
|
||||
TestStringUtils.cpp
|
||||
TestStringView.cpp
|
||||
TestTime.cpp
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
* Copyright (c) 2022, Dan Klishch <danilklishch@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibTest/TestCase.h>
|
||||
|
||||
#include <AK/Array.h>
|
||||
#include <AK/StringConversions.h>
|
||||
#include <AK/Utf16View.h>
|
||||
|
||||
|
|
@ -690,3 +692,69 @@ TEST_CASE(octal)
|
|||
EXPECT_EQ(actual.has_value(), true);
|
||||
EXPECT_EQ(actual.value(), 0177777u);
|
||||
}
|
||||
|
||||
template<>
|
||||
struct AK::Formatter<AK::DecimalExponentialForm> : Formatter<FormatString> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, AK::DecimalExponentialForm value)
|
||||
{
|
||||
return Formatter<FormatString>::format(builder, "(s={} f={} e={})"sv, value.sign, value.fraction, value.exponent);
|
||||
}
|
||||
};
|
||||
|
||||
#define DOES_CONVERT_TO(type, value, sign, fraction, exponent) \
|
||||
do { \
|
||||
EXPECT_EQ( \
|
||||
AK::convert_to_decimal_exponential_form(static_cast<type>(value)), \
|
||||
(AK::DecimalExponentialForm { sign, fraction, exponent })); \
|
||||
} while (false)
|
||||
|
||||
TEST_CASE(double_conversion)
|
||||
{
|
||||
DOES_CONVERT_TO(double, 0, 0, 0, 0);
|
||||
DOES_CONVERT_TO(double, -0., 1, 0, 0);
|
||||
DOES_CONVERT_TO(double, 1, 0, 1, 0);
|
||||
DOES_CONVERT_TO(double, -1, 1, 1, 0);
|
||||
DOES_CONVERT_TO(double, .1, 0, 1, -1);
|
||||
DOES_CONVERT_TO(double, .2, 0, 2, -1);
|
||||
DOES_CONVERT_TO(double, .3, 0, 3, -1);
|
||||
DOES_CONVERT_TO(double, .12345, 0, 12345, -5);
|
||||
DOES_CONVERT_TO(double, .0012345, 0, 12345, -7);
|
||||
DOES_CONVERT_TO(double, .1 + .2, 0, 30000000000000004, -17);
|
||||
DOES_CONVERT_TO(double, 17976931348623157e292, 0, 17976931348623157, 292);
|
||||
DOES_CONVERT_TO(double, -17976931348623157e292, 1, 17976931348623157, 292);
|
||||
DOES_CONVERT_TO(double, 22250738585072014e-324, 0, 22250738585072014, -324);
|
||||
DOES_CONVERT_TO(double, -22250738585072014e-324, 1, 22250738585072014, -324);
|
||||
DOES_CONVERT_TO(double, bit_cast<double>(0xc3c04222300db8acULL), 1, 23430728857074627, 2);
|
||||
}
|
||||
|
||||
TEST_CASE(float_conversion)
|
||||
{
|
||||
DOES_CONVERT_TO(float, 0, 0, 0, 0);
|
||||
DOES_CONVERT_TO(float, -0., 1, 0, 0);
|
||||
DOES_CONVERT_TO(float, 1, 0, 1, 0);
|
||||
DOES_CONVERT_TO(float, -1, 1, 1, 0);
|
||||
DOES_CONVERT_TO(float, .1, 0, 1, -1);
|
||||
DOES_CONVERT_TO(float, .2, 0, 2, -1);
|
||||
DOES_CONVERT_TO(float, .3, 0, 3, -1);
|
||||
DOES_CONVERT_TO(float, 0.025, 0, 25, -3);
|
||||
DOES_CONVERT_TO(float, 34028235e31, 0, 34028235, 31);
|
||||
DOES_CONVERT_TO(float, -34028235e31, 1, 34028235, 31);
|
||||
DOES_CONVERT_TO(float, 11754944e-45, 0, 11754944, -45);
|
||||
DOES_CONVERT_TO(float, -11754944e-45, 1, 11754944, -45);
|
||||
}
|
||||
|
||||
BENCHMARK_CASE(bench_float_conversion)
|
||||
{
|
||||
static constexpr auto numbers = to_array<double>({
|
||||
123.456789,
|
||||
0.000000123456789,
|
||||
9876543210123456.0,
|
||||
1.23e-7,
|
||||
58.0,
|
||||
});
|
||||
|
||||
for (size_t i = 0; i < 100'000'000; i++) {
|
||||
auto result = AK::convert_to_decimal_exponential_form(numbers[i % numbers.size()]);
|
||||
AK::taint_for_optimizer(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022, Dan Klishch <danilklishch@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/BitCast.h>
|
||||
#include <AK/StringFloatingPointConversions.h>
|
||||
#include <LibTest/TestCase.h>
|
||||
|
||||
namespace AK {
|
||||
|
||||
static bool operator!=(FloatingPointExponentialForm a, FloatingPointExponentialForm b)
|
||||
{
|
||||
return a.sign != b.sign || a.exponent != b.exponent || a.fraction != b.fraction;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<>
|
||||
struct AK::Formatter<AK::FloatingPointExponentialForm> : Formatter<FormatString> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, AK::FloatingPointExponentialForm value)
|
||||
{
|
||||
return Formatter<FormatString>::format(builder, "(s={} f={} e={})"sv, value.sign, value.fraction, value.exponent);
|
||||
}
|
||||
};
|
||||
|
||||
#define DOES_CONVERT_DOUBLE_TO(value, sign, fraction, exponent) \
|
||||
do { \
|
||||
EXPECT_EQ( \
|
||||
convert_floating_point_to_decimal_exponential_form(static_cast<double>(value)), \
|
||||
(AK::FloatingPointExponentialForm { sign, fraction, exponent })); \
|
||||
} while (false)
|
||||
|
||||
// Tests here only check basic cases. While writing, I mostly relied on the benchmarks and
|
||||
// stress tests, which can be found at https://github.com/DanShaders/serenity-arithmetic-benchmark/blob/master/StringFloatingPointConversions.cpp .
|
||||
|
||||
TEST_CASE(double_conversion)
|
||||
{
|
||||
DOES_CONVERT_DOUBLE_TO(0, 0, 0, 0);
|
||||
DOES_CONVERT_DOUBLE_TO(-0., 1, 0, 0);
|
||||
DOES_CONVERT_DOUBLE_TO(1, 0, 1, 0);
|
||||
DOES_CONVERT_DOUBLE_TO(-1, 1, 1, 0);
|
||||
DOES_CONVERT_DOUBLE_TO(.1, 0, 1, -1);
|
||||
DOES_CONVERT_DOUBLE_TO(.2, 0, 2, -1);
|
||||
DOES_CONVERT_DOUBLE_TO(.3, 0, 3, -1);
|
||||
DOES_CONVERT_DOUBLE_TO(.12345, 0, 12345, -5);
|
||||
DOES_CONVERT_DOUBLE_TO(.0012345, 0, 12345, -7);
|
||||
DOES_CONVERT_DOUBLE_TO(.1 + .2, 0, 30000000000000004, -17);
|
||||
DOES_CONVERT_DOUBLE_TO(17976931348623157e292, 0, 17976931348623157, 292);
|
||||
DOES_CONVERT_DOUBLE_TO(-17976931348623157e292, 1, 17976931348623157, 292);
|
||||
DOES_CONVERT_DOUBLE_TO(22250738585072014e-324, 0, 22250738585072014, -324);
|
||||
DOES_CONVERT_DOUBLE_TO(-22250738585072014e-324, 1, 22250738585072014, -324);
|
||||
DOES_CONVERT_DOUBLE_TO(bit_cast<double>(0xc3c04222300db8acULL), 1, 23430728857074627, 2);
|
||||
}
|
||||
|
||||
#define DOES_CONVERT_FLOAT_TO(value, sign, fraction, exponent) \
|
||||
do { \
|
||||
EXPECT_EQ( \
|
||||
convert_floating_point_to_decimal_exponential_form(static_cast<float>(value)), \
|
||||
(AK::FloatingPointExponentialForm { sign, fraction, exponent })); \
|
||||
} while (false)
|
||||
|
||||
TEST_CASE(float_conversion)
|
||||
{
|
||||
DOES_CONVERT_FLOAT_TO(0, 0, 0, 0);
|
||||
DOES_CONVERT_FLOAT_TO(-0., 1, 0, 0);
|
||||
DOES_CONVERT_FLOAT_TO(1, 0, 1, 0);
|
||||
DOES_CONVERT_FLOAT_TO(-1, 1, 1, 0);
|
||||
DOES_CONVERT_FLOAT_TO(.1, 0, 1, -1);
|
||||
DOES_CONVERT_FLOAT_TO(.2, 0, 2, -1);
|
||||
DOES_CONVERT_FLOAT_TO(.3, 0, 3, -1);
|
||||
DOES_CONVERT_FLOAT_TO(0.025, 0, 25, -3);
|
||||
DOES_CONVERT_FLOAT_TO(34028235e31, 0, 34028235, 31);
|
||||
DOES_CONVERT_FLOAT_TO(-34028235e31, 1, 34028235, 31);
|
||||
DOES_CONVERT_FLOAT_TO(11754944e-45, 0, 11754944, -45);
|
||||
DOES_CONVERT_FLOAT_TO(-11754944e-45, 1, 11754944, -45);
|
||||
}
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
"zlib"
|
||||
]
|
||||
},
|
||||
"fmt",
|
||||
{
|
||||
"name": "fontconfig",
|
||||
"platform": "linux | bsd | osx"
|
||||
|
|
@ -256,6 +257,10 @@
|
|||
"name": "ffmpeg",
|
||||
"version": "7.1.1#5"
|
||||
},
|
||||
{
|
||||
"name": "fmt",
|
||||
"version": "12.1.0#0"
|
||||
},
|
||||
{
|
||||
"name": "fontconfig",
|
||||
"version": "2.15.0#4"
|
||||
|
|
|
|||
Loading…
Reference in a new issue