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:
Timothy Flynn 2026-02-23 09:24:23 -05:00 committed by Jelle Raaijmakers
parent 721c05802c
commit 3355fb39ae
13 changed files with 139 additions and 1247 deletions

View file

@ -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

View file

@ -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;

View file

@ -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);
}

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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);

View file

@ -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",

View file

@ -68,7 +68,6 @@ set(AK_TEST_SOURCES
TestStdLibExtras.cpp
TestString.cpp
TestStringConversions.cpp
TestStringFloatingPointConversions.cpp
TestStringUtils.cpp
TestStringView.cpp
TestTime.cpp

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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"