fast_float 数字解析库:比 strtod 快 4 倍
fast_float 库为 C++ 的 float
和 double
类型以及整数类型提供了快速的仅头文件实现的 from_chars 函数。这些函数将表示十进制值的 ASCII 字符串(如 1.3e10
)转换为二进制类型。我们提供精确舍入(包括向偶数舍入)。根据我们的经验,这些 fast_float
函数比现有 C++ 标准库中的同类数字解析函数快很多倍。
具体来说,fast_float
提供了以下两个函数来解析浮点数,语法类似于 C++17(库本身只需要 C++11):
from_chars_result from_chars(const char* first, const char* last, float& value, ...);
from_chars_result from_chars(const char* first, const char* last, double& value, ...);
你也可以解析整数类型:
返回类型(from_chars_result
)定义为以下结构体:
struct from_chars_result {
const char* ptr;
std::errc ec;
};
它解析字符序列 [first,last) 中的数字。它解析浮点数时期望一种与 C++17 from_chars 函数等效的与区域设置无关的格式。 结果浮点值是最接近的浮点值(使用 float 或 double),对于恰好落在两个值之间的数值,使用"向偶数舍入"约定。 也就是说,我们根据 IEEE 标准提供精确解析。
解析成功时,返回值中的指针(ptr
)被设置为指向解析后的数字之后,并且引用的 value
被设置为解析后的值。如果出错,返回的 ec
包含一个代表性错误,否则存储默认值(std::errc()
)。
实现不会抛出异常,也不会分配内存(例如,使用 new
或 malloc
)。
它可以解析无穷大和 NaN 值。
示例:
#include "fast_float/fast_float.h"
#include <iostream>
int main() {
const std::string input = "3.1416 xyz ";
double result;
auto answer = fast_float::from_chars(input.data(), input.data()+input.size(), result);
if(answer.ec != std::errc()) { std::cerr << "解析失败\n"; return EXIT_FAILURE; }
std::cout << "解析到的数字为 " << result << std::endl;
return EXIT_SUCCESS;
}
你可以解析带分隔符的数字:
const std::string input = "234532.3426362,7869234.9823,324562.645";
double result;
auto answer = fast_float::from_chars(input.data(), input.data()+input.size(), result);
if(answer.ec != std::errc()) {
// 检查错误
}
// 此时 result == 234532.3426362
if(answer.ptr[0] != ',') {
// 意外的分隔符
}
answer = fast_float::from_chars(answer.ptr + 1, input.data()+input.size(), result);
if(answer.ec != std::errc()) {
// 检查错误
}
// 此时 result == 7869234.9823
if(answer.ptr[0] != ',') {
// 意外的分隔符
}
answer = fast_float::from_chars(answer.ptr + 1, input.data()+input.size(), result);
if(answer.ec != std::errc()) {
// 检查错误
}
// 此时 result == 324562.645
与 C++17 标准一样,fast_float::from_chars
函数接受一个可选的最后参数,类型为 fast_float::chars_format
。这是一个位集值:我们检查 fmt & fast_float::chars_format::fixed
和 fmt & fast_float::chars_format::scientific
是否设置,以确定我们是否允许定点和科学计数法。默认值是 fast_float::chars_format::general
,它允许 fixed
和 scientific
两种格式。
该库遵循 C++17 规范(参见 20.19.3.(7.1))。
from_chars
函数不跳过前导空白字符。- 不允许前导
+
号。 - 通常不可能将十进制值精确表示为二进制浮点数(
float
和double
类型)。我们寻找最接近的值。当介于两个二进制浮点数之间时,我们向偶数尾数舍入。
此外,我们有以下限制:
- 目前我们只支持
float
和double
类型。 - 我们只支持十进制格式:不支持十六进制字符串。
- 对于非常大或非常小的值(例如
1e9999
),我们用无穷大或负无穷大值表示,并将返回的ec
设置为std::errc::result_out_of_range
。
我们支持 Visual Studio、macOS、Linux、freeBSD。我们支持大端和小端。我们支持 32 位和 64 位系统。
我们假设舍入模式设置为最近值(std::fegetround() == FE_TONEAREST
)。
整数类型
你也可以使用不同的进制(例如 2、10、16)解析整数类型。以下代码将打印数字 22250738585072012 三次:
uint64_t i;
const char str[] = "22250738585072012";
auto answer = fast_float::from_chars(str, str + strlen(str), i);
if (answer.ec != std::errc()) {
std::cerr << "解析失败\n";
return EXIT_FAILURE;
}
std::cout << "解析到的数字为 "<< i << std::endl;
const char binstr[] = "1001111000011001110110111001001010110100111000110001100";
answer = fast_float::from_chars(binstr, binstr + strlen(binstr), i, 2);
if (answer.ec != std::errc()) {
std::cerr << "解析失败\n";
return EXIT_FAILURE;
}
std::cout << "解析到的数字为 "<< i << std::endl;
const char hexstr[] = "4f0cedc95a718c";
answer = fast_float::from_chars(hexstr, hexstr + strlen(hexstr), i, 16);
if (answer.ec != std::errc()) {
std::cerr << "解析失败\n";
return EXIT_FAILURE;
}
std::cout << "解析到的数字为 "<< i << std::endl;
C++20:编译时求值(constexpr)
在 C++20 中,你可以使用 fast_float::from_chars
在编译时解析字符串,如下例所示:
// consteval 在 C++20 中强制函数进行编译时求值。
consteval double parse(std::string_view input) {
double result;
auto answer = fast_float::from_chars(input.data(), input.data()+input.size(), result);
if(answer.ec != std::errc()) { return -1.0; }
return result;
}
// 这个函数应该编译为一个仅返回 3.1415 的函数。
constexpr double constexptest() {
return parse("3.1415 input");
}
C++23:固定宽度浮点类型
该库还支持固定宽度的浮点类型,如 std::float32_t
和 std::float64_t
。例如,你可以这样写:
std::float32_t result;
auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result);
非ASCII输入
我们还支持UTF-16和UTF-32输入,以及ASCII/UTF-8,如下例所示:
#include "fast_float/fast_float.h"
#include <iostream>
int main() {
const std::u16string input = u"3.1416 xyz ";
double result;
auto answer = fast_float::from_chars(input.data(), input.data()+input.size(), result);
if(answer.ec != std::errc()) { std::cerr << "parsing failure\n"; return EXIT_FAILURE; }
std::cout << "parsed the number " << result << std::endl;
return EXIT_SUCCESS;
}
高级选项:使用逗号作为小数分隔符、JSON和Fortran
C++标准规定 from_chars
必须与区域设置无关。特别是,小数分隔符必须是句点(.
)。然而,一些用户仍然希望以区域相关的方式使用 fast_float
库。通过使用名为 from_chars_advanced
的单独函数,我们允许用户传递一个包含自定义小数分隔符(例如逗号)的 parse_options
实例。你可以像这样使用它:
#include "fast_float/fast_float.h"
#include <iostream>
int main() {
const std::string input = "3,1416 xyz ";
double result;
fast_float::parse_options options{fast_float::chars_format::general, ','};
auto answer = fast_float::from_chars_advanced(input.data(), input.data()+input.size(), result, options);
if((answer.ec != std::errc()) || ((result != 3.1416))) { std::cerr << "parsing failure\n"; return EXIT_FAILURE; }
std::cout << "parsed the number " << result << std::endl;
return EXIT_SUCCESS;
}
你也可以解析类似Fortran的输入:
#include "fast_float/fast_float.h"
#include <iostream>
int main() {
const std::string input = "1d+4";
double result;
fast_float::parse_options options{ fast_float::chars_format::fortran };
auto answer = fast_float::from_chars_advanced(input.data(), input.data()+input.size(), result, options);
if((answer.ec != std::errc()) || ((result != 10000))) { std::cerr << "parsing failure\n"; return EXIT_FAILURE; }
std::cout << "parsed the number " << result << std::endl;
return EXIT_SUCCESS;
}
你也可以强制使用JSON格式(RFC 8259):
#include "fast_float/fast_float.h"
#include <iostream>
int main() {
const std::string input = "+.1"; // 不合法
double result;
fast_float::parse_options options{ fast_float::chars_format::json };
auto answer = fast_float::from_chars_advanced(input.data(), input.data()+input.size(), result, options);
if(answer.ec == std::errc()) { std::cerr << "should have failed\n"; return EXIT_FAILURE; }
return EXIT_SUCCESS;
}
默认情况下,JSON格式不允许 inf
:
#include "fast_float/fast_float.h"
#include <iostream>
int main() {
const std::string input = "inf"; // JSON中不合法
double result;
fast_float::parse_options options{ fast_float::chars_format::json };
auto answer = fast_float::from_chars_advanced(input.data(), input.data()+input.size(), result, options);
if(answer.ec == std::errc()) { std::cerr << "should have failed\n"; return EXIT_FAILURE; }
}
你可以使用非标准的 json_or_infnan
变体来允许它:
#include "fast_float/fast_float.h"
#include <iostream>
int main() {
const std::string input = "inf"; // JSON中不合法,但我们通过json_or_infnan允许它
double result;
fast_float::parse_options options{ fast_float::chars_format::json_or_infnan };
auto answer = fast_float::from_chars_advanced(input.data(), input.data()+input.size(), result, options);
if(answer.ec != std::errc() || (!std::isinf(result))) { std::cerr << "should have parsed infinity\n"; return EXIT_FAILURE; }
return EXIT_SUCCESS;
}
用户和相关工作
fast_float库是以下项目的一部分:
- GCC(从版本12开始):GCC中的
from_chars
函数依赖于fast_float。 - Chromium,Google Chrome和Microsoft Edge浏览器背后的引擎。
- WebKit,Safari(苹果的网络浏览器)背后的引擎。
- DuckDB
- Apache Arrow,将数字解析速度提高了两到三倍。
- Google Jsonnet
- ClickHouse
fastfloat算法是LLVM标准库的一部分。AdaCore有一个衍生实现。
fast_float库提供了与fast_double_parser库相似的性能,但使用了从头重新设计的更新算法,同时提供了更符合C++程序员期望的API。fast_double_parser库是Microsoft LightGBM机器学习框架的一部分。
参考文献
- Daniel Lemire,以每秒千兆字节的速度进行数字解析,软件:实践与经验 51 (8),2021。
- Noble Mushtak,Daniel Lemire,无需回退的快速数字解析,软件:实践与经验 53 (7),2023。
其他编程语言
- 有一个R语言绑定,名为
rcppfastfloat
。 - 有一个Rust版的fast_float库,名为
fast-float-rust
。 - 有一个Java版的fast_float库,名为
FastDoubleParser
。它被用于重要系统,如Jackson。 - 有一个C#版的fast_float库,名为
csFastFloat
。
它有多快?
在某些系统上,它可以以1 GB/s的速度解析随机浮点数。我们发现它通常比最好的可用竞争对手快两倍,比许多标准库实现快很多倍。
``` $ ./build/benchmarks/benchmark # 解析范围在[0,1)的随机整数 体积 = 2.09808 MB netlib : 271.18 MB/s (+/- 1.2 %) 12.93 Mfloat/s doubleconversion : 225.35 MB/s (+/- 1.2 %) 10.74 Mfloat/s strtod : 190.94 MB/s (+/- 1.6 %) 9.10 Mfloat/s abseil : 430.45 MB/s (+/- 2.2 %) 20.52 Mfloat/s fastfloat : 1042.38 MB/s (+/- 9.9 %) 49.68 Mfloat/s ```请访问 https://github.com/lemire/simple_fastfloat_benchmark 查看我们的基准测试代码。
视频
作为 CMake 依赖使用
本库设计为仅头文件。CMake 文件提供了 fast_float
目标,它只是指向 include
目录的指针。
如果您将 fast_float
仓库放入您的 CMake 项目中,您应该能够以这种方式使用它:
add_subdirectory(fast_float)
target_link_libraries(myprogram PUBLIC fast_float)
或者,如果您有足够新的 CMake 版本(至少 3.11 或更高),您可能希望自动获取依赖项:
FetchContent_Declare(
fast_float
GIT_REPOSITORY https://github.com/lemire/fast_float.git
GIT_TAG tags/v1.1.2
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(fast_float)
target_link_libraries(myprogram PUBLIC fast_float)
您应该更改 GIT_TAG
行,以获取您希望使用的版本。
作为单一头文件使用
如果需要,可以使用 script/amalgamate.py
脚本生成库的单一头文件版本。
只需从此仓库的根目录运行脚本即可。
如果需要,您可以按照命令行帮助中的说明自定义许可证类型和输出文件。
您可以直接下载自动生成的单一头文件:
https://github.com/fastfloat/fast_float/releases/download/v6.1.4/fast_float.h
RFC 7159
如果您需要支持 RFC 7159(JSON 标准),您可能需要考虑使用 fast_double_parser 库。
致谢
尽管这项工作受到许多不同人的启发,但它特别受益于与 Michael Eisel 的交流,他以其关键见解激发了原始研究,以及与 Nigel Tao 的交流,他提供了宝贵的反馈。Rémy Oudompheng 首次实现了我们在长位数情况下使用的快速路径。
该库包含了改编自 Google Wuffs(由 Nigel Tao 编写)的代码,该代码最初是在 Apache 2.0 许可下发布的。
许可证
根据 Apache License, Version 2.0、MIT license 或 BOOST license 三者之一授权。除非您另有明确声明,否则您有意提交以包含在本仓库中的任何贡献,如 Apache-2.0 许可中所定义,均应按上述方式三重许可,无任何附加条款或条件。