Skip to content

Commit

Permalink
Add fast_float impl for string-to-float conversion in folly
Browse files Browse the repository at this point in the history
Summary:
Adds implementations of `folly::detail::str_to_floating` that uses `fast_float::from_chars`, which is expected to be ~2-4x as fast as libdouble-conversion [0][1]. The version of `folly::detail::str_to_floating`  that uses libdouble-conversion remains the same. This implementation can be replaced with `std::from_chars` on libstdc++ version 12 and greater which also uses fast_float [3].

This implementation is enabled with a buck config switch, so clients can opt in.

0: https://master.charconv.cpp.al/#benchmark_results_
1: https://github.com/boostorg/charconv/blob/develop/benchmark/from_chars_floating.cpp
3: gcc-mirror/gcc@490e230#diff-d3c32d9c9c566f7f3888d150c6448428ea194170146a1a166917ba45b1252187

Reviewed By: yfeldblum

Differential Revision: D60997956

fbshipit-source-id: f62edefdde80aa76c409cb7b9d0078932cfce7e0
  • Loading branch information
skrueger authored and facebook-github-bot committed Aug 24, 2024
1 parent 784fb14 commit 10b1f27
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 18 deletions.
30 changes: 30 additions & 0 deletions CMake/FindFastFloat.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

#
# - Try to find fast_float library
# This will define
# FASTFLOAT_FOUND
# FASTFLOAT_INCLUDE_DIR
#

find_path(FASTFLOAT_INCLUDE_DIR NAMES fast_float/fast_float.h)

include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
FastFloat DEFAULT_MSG
FASTFLOAT_INCLUDE_DIR
)

mark_as_advanced(FASTFLOAT_INCLUDE_DIR)
3 changes: 3 additions & 0 deletions CMake/folly-deps.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ find_package(DoubleConversion MODULE REQUIRED)
list(APPEND FOLLY_LINK_LIBRARIES ${DOUBLE_CONVERSION_LIBRARY})
list(APPEND FOLLY_INCLUDE_DIRECTORIES ${DOUBLE_CONVERSION_INCLUDE_DIR})

find_package(FastFloat MODULE REQUIRED)
list(APPEND FOLLY_INCLUDE_DIRECTORIES ${FASTFLOAT_INCLUDE_DIR})

find_package(Gflags MODULE)
set(FOLLY_HAVE_LIBGFLAGS ${LIBGFLAGS_FOUND})
if(LIBGFLAGS_FOUND)
Expand Down
7 changes: 7 additions & 0 deletions folly/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,16 @@ cpp_library(
"-DFOLLY_CONV_DTOA_TO_CHARS=1",
],
"DEFAULT": [],
}) + select({
"//folly/buck_config:folly-conv-atod-mode-fastfloat": [
# Use fast_float::from_chars for ASCII-to-Double conversions
"-DFOLLY_CONV_ATOD_MODE=1",
],
"DEFAULT": [],
}),
undefined_symbols = True, # TODO(T23121628): fix deps and remove
deps = [
"fbsource//third-party/fast_float:fast_float",
"//folly/lang:safe_assert",
],
exported_deps = [
Expand Down
72 changes: 65 additions & 7 deletions folly/Conv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

#include <folly/lang/SafeAssert.h>

#include <fast_float/fast_float.h> // @manual=fbsource//third-party/fast_float:fast_float

namespace folly {
namespace detail {

Expand Down Expand Up @@ -340,12 +342,11 @@ Expected<bool, ConversionCode> str_to_bool(StringPiece* src) noexcept {
return result;
}

/**
* StringPiece to double, with progress information. Alters the
* StringPiece parameter to munch the already-parsed characters.
*/
/// Uses `double_conversion` library to convert from string to a floating
/// point.
template <class Tgt>
Expected<Tgt, ConversionCode> str_to_floating(StringPiece* src) noexcept {
Expected<Tgt, ConversionCode> str_to_floating_double_conversion(
StringPiece* src) noexcept {
using namespace double_conversion;
static StringToDoubleConverter conv(
StringToDoubleConverter::ALLOW_TRAILING_JUNK |
Expand Down Expand Up @@ -461,6 +462,63 @@ Expected<Tgt, ConversionCode> str_to_floating(StringPiece* src) noexcept {
return Tgt(result);
}

/// Uses `fast_float::from_chars` to convert from string to an integer.
template <class Tgt>
Expected<Tgt, ConversionCode> str_to_floating_fast_float_from_chars(
StringPiece* src) noexcept {
if (src->empty()) {
return makeUnexpected(ConversionCode::EMPTY_INPUT_STRING);
}

// move through leading whitespace characters
auto* e = src->end();
auto* b = std::find_if_not(src->begin(), e, [](char c) {
return (c >= '\t' && c <= '\r') || c == ' ';
});
if (b == e) {
return makeUnexpected(ConversionCode::EMPTY_INPUT_STRING);
}

Tgt result;
auto [ptr, ec] = fast_float::from_chars(b, e, result);
bool isOutOfRange{ec == std::errc::result_out_of_range};
bool isOk{ec == std::errc()};
if (!isOk && !isOutOfRange) {
return makeUnexpected(ConversionCode::STRING_TO_FLOAT_ERROR);
}

auto numMatchedChars = ptr - src->data();
src->advance(numMatchedChars);

if (isOutOfRange) {
if (*b == '-') {
return -std::numeric_limits<Tgt>::infinity();
} else {
return std::numeric_limits<Tgt>::infinity();
}
}

return result;
}

template Expected<float, ConversionCode>
str_to_floating_fast_float_from_chars<float>(StringPiece* src) noexcept;
template Expected<double, ConversionCode>
str_to_floating_fast_float_from_chars<double>(StringPiece* src) noexcept;

/**
* StringPiece to double, with progress information. Alters the
* StringPiece parameter to munch the already-parsed characters.
*/
template <class Tgt>
Expected<Tgt, ConversionCode> str_to_floating(StringPiece* src) noexcept {
#if defined(FOLLY_CONV_ATOD_MODE) && FOLLY_CONV_ATOD_MODE == 1
return detail::str_to_floating_fast_float_from_chars<Tgt>(src);
#else
return detail::str_to_floating_double_conversion<Tgt>(src);
#endif
}

template Expected<float, ConversionCode> str_to_floating<float>(
StringPiece* src) noexcept;
template Expected<double, ConversionCode> str_to_floating<double>(
Expand Down Expand Up @@ -1014,8 +1072,8 @@ std::pair<char*, char*> formatAsDoubleConversion(

unsigned int numTrailingZerosToAdd = 0;
if (!flagsSet.noTrailingZero() && mode == DtoaMode::PRECISION) {
// std::to_chars outputs no trailing zeros, so if it's not set, add trailing
// zeros
// std::to_chars outputs no trailing zeros, so if it's not set, add
// trailing zeros
unsigned int numPrecisionFigures = parsedDecimal.numPrecisionFigures();
if (numDigits > numPrecisionFigures) {
numTrailingZerosToAdd = numDigits - numPrecisionFigures;
Expand Down
9 changes: 9 additions & 0 deletions folly/Conv.h
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,15 @@ extern template Expected<float, ConversionCode> str_to_floating<float>(
extern template Expected<double, ConversionCode> str_to_floating<double>(
StringPiece* src) noexcept;

template <typename T>
Expected<T, ConversionCode> str_to_floating_fast_float_from_chars(
StringPiece* src) noexcept;

extern template Expected<float, ConversionCode>
str_to_floating_fast_float_from_chars<float>(StringPiece* src) noexcept;
extern template Expected<double, ConversionCode>
str_to_floating_fast_float_from_chars<double>(StringPiece* src) noexcept;

template <class Tgt>
Expected<Tgt, ConversionCode> digits_to(const char* b, const char* e) noexcept;

Expand Down
11 changes: 11 additions & 0 deletions folly/buck_config/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ load("@fbsource//tools/build_defs:fb_native_wrapper.bzl", "fb_native")

oncall("fbcode_entropy_wardens_folly")

# Config setting to use std::to_chars instead of libdouble-conversion in //folly:conv
fb_native.config_setting(
name = "folly-conv-dtoa-to-chars",
values = {
"folly.conv_dtoa_to_chars": "true",
},
visibility = ["PUBLIC"],
)

# Config setting to change the implementation of folly::detail::str_to_float
# to use fast_float::from_chars
fb_native.config_setting(
name = "folly-conv-atod-mode-fastfloat",
values = {
"folly.conv_atod_mode": "fastfloat",
},
visibility = ["PUBLIC"],
)
155 changes: 144 additions & 11 deletions folly/test/ConvTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1606,23 +1606,55 @@ TEST(Conv, TryStringToEnum) {
EXPECT_EQ(static_cast<A>(50), rv3.value());
}

namespace {
/// Simple pure virtual class used by tests to change the function that converts
/// string to float.
template <class String>
class StrToFloat {
public:
virtual ~StrToFloat() = default;
/// Converts a string to a float.
/// The input string is expected to be a number in
/// decimal or exponential notation (e.g., "3.14", "3.14e-2").
virtual Expected<float, ConversionCode> operator()(String src) const = 0;

/// Returns true if `operator()` returns an error when the input string has
/// trailing junk.
/// e.g., when the input string is "3.14junk", `operator()` returns an error.
virtual bool returnsErrorOnTrailingJunk() const = 0;
};

/// Uses `folly::TryTo` to convert a string to a float.
template <class String>
class StrToFloatTryTo : public StrToFloat<String> {
public:
Expected<float, ConversionCode> operator()(String src) const override {
return folly::tryTo<float>(src);
}

bool returnsErrorOnTrailingJunk() const override { return true; }
};
} // namespace

/// `strToFloat` is used to test out different implementations of string
/// to float conversions.
template <class String>
void tryStringToFloat() {
auto rv1 = folly::tryTo<float>(String(""));
void tryStringToFloat(const StrToFloat<String>& strToFloat) {
auto rv1 = strToFloat(String(""));
EXPECT_FALSE(rv1.hasValue());
auto rv2 = folly::tryTo<float>(String("3.14"));
auto rv2 = strToFloat(String("3.14"));
EXPECT_TRUE(rv2.hasValue());
EXPECT_NEAR(rv2.value(), 3.14, 1e-5);
// No trailing '\0' to expose 1-byte buffer over-read
char x = '-';
auto rv3 = folly::tryTo<float>(folly::StringPiece(&x, 1));
auto rv3 = strToFloat(String(&x, 1));
EXPECT_FALSE(rv3.hasValue());

// Exact conversion at numeric limits (8+ decimal digits)
auto rv4 = folly::tryTo<float>(String("-3.4028235E38"));
auto rv4 = strToFloat(String("-3.4028235E38"));
EXPECT_TRUE(rv4.hasValue());
EXPECT_EQ(rv4.value(), numeric_limits<float>::lowest());
auto rv5 = folly::tryTo<float>(String("3.40282346E38"));
auto rv5 = strToFloat(String("3.40282346E38"));
EXPECT_TRUE(rv5.hasValue());
EXPECT_EQ(rv5.value(), numeric_limits<float>::max());

Expand All @@ -1635,7 +1667,7 @@ void tryStringToFloat() {
"-3.4028236E38",
}};
for (const auto& input : kOversizedInputs) {
auto rv = folly::tryTo<float>(input);
auto rv = strToFloat(input);
EXPECT_EQ(rv.value(), -numeric_limits<float>::infinity()) << input;
}

Expand All @@ -1649,15 +1681,116 @@ void tryStringToFloat() {
"-NAN",
}};
for (const auto& input : kNanInputs) {
auto rv = folly::tryTo<float>(input);
auto rv = strToFloat(input);
EXPECT_TRUE(std::isnan(rv.value())) << input;
}

const std::array<String, 6> kInfinityInputs{{
"-inf",
"-INF",
"-iNf",
"-infinity",
"-INFINITY",
"-INFInITY",
}};
for (const auto& input : kInfinityInputs) {
{
auto rv = strToFloat(input);
EXPECT_EQ(rv.value(), -numeric_limits<float>::infinity()) << input;
}

{
auto positiveInput = input.substr(1);
auto rv = strToFloat(positiveInput);
EXPECT_EQ(rv.value(), numeric_limits<float>::infinity()) << positiveInput;
}
}

const std::array<String, 11> kScientificNotation{{
"123.4560e0",
"123.4560e+0",
"123.4560e-0",
"123456.0e-3",
"123456.0E-3",
"0.123456e3",
"0.123456e+3",
"0.123456E+3",
".123456e3",
".123456e+3",
".123456E+3",
}};
for (const auto& input : kScientificNotation) {
auto rv = strToFloat(input);
EXPECT_EQ(rv.value(), 123.456f) << input;
}

const std::array<String, 8> kSurroundingWhitespace{{
" 123.456",
"\n123.456",
"\r123.456",
"\t123.456",
"\n123.456",
"123.456 ",
"123.456\n",
" 123.456 ",
}};
for (const auto& input : kSurroundingWhitespace) {
EXPECT_EQ(strToFloat(input).value(), 123.456f);
}

EXPECT_EQ(strToFloat(" ").error(), ConversionCode::EMPTY_INPUT_STRING);

const std::array<String, 2> kJunkValues{{"junk", "a123.456"}};
for (const auto& input : kJunkValues) {
EXPECT_EQ(strToFloat(input).error(), ConversionCode::STRING_TO_FLOAT_ERROR);
}

const std::array<String, 8> kNonWhitespaceAfterEnd{{
"123.456X",
"123.456e",
"123.456E",
"123.456E-",
"123.456E+",
"123.456E-2f",
"123.456E-f",
"123.456E~2",
}};
for (const auto& input : kNonWhitespaceAfterEnd) {
auto rv = strToFloat(input);
EXPECT_EQ(strToFloat.returnsErrorOnTrailingJunk(), rv.hasError())
<< "input: " << input << " value " << rv.value();
if (rv.hasError()) {
EXPECT_EQ(rv.error(), ConversionCode::NON_WHITESPACE_AFTER_END)
<< "input: " << input;
}
}
}

TEST(Conv, TryStringToFloat) {
tryStringToFloat<std::string>();
tryStringToFloat<std::string_view>();
tryStringToFloat<folly::StringPiece>();
tryStringToFloat<std::string>(StrToFloatTryTo<std::string>());
tryStringToFloat<std::string_view>(StrToFloatTryTo<std::string_view>());
tryStringToFloat<folly::StringPiece>(StrToFloatTryTo<folly::StringPiece>());
}

/// Uses `folly::detail::str_to_floating_fast_float_from_chars` to convert a
/// string to a float.
template <class String>
class StrToFloatFastFloatFromChars : public StrToFloat<String> {
public:
Expected<float, ConversionCode> operator()(String src) const override {
StringPiece sp{src};
return folly::detail::str_to_floating_fast_float_from_chars<float>(&sp);
}

bool returnsErrorOnTrailingJunk() const override { return false; }
};

TEST(Conv, TryStringToFloat_FastFloatFromChars) {
tryStringToFloat<std::string>(StrToFloatFastFloatFromChars<std::string>());
tryStringToFloat<std::string_view>(
StrToFloatFastFloatFromChars<std::string_view>());
tryStringToFloat<folly::StringPiece>(
StrToFloatFastFloatFromChars<folly::StringPiece>());
}

template <class String>
Expand Down

0 comments on commit 10b1f27

Please sign in to comment.