提问人:Huy Le 提问时间:8/2/2023 最后编辑:Huy Le 更新时间:8/2/2023 访问量:97
C++ 基本浮点解析代码与 std::atof 结果略有不同
C++ basic float parsing code slightly different result from std::atof
问:
#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
#include <vector>
#include <iomanip>
#include <cmath>
using namespace std;
#define LIKELY(expr) (__builtin_expect(!!(expr), 1))
#define UNLIKELY(expr) (__builtin_expect(!!(expr), 0))
bool abscmp(double a, double b)
{
if (isnan(a) && isnan(b)) return true;
if (isnan(a) ^ isnan(b)) return false;
return a == b;
}
template <typename T>
inline __attribute__((always_inline)) T ParseFloat(const char *a) {
static constexpr T multers[] = {
0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001, 0.0000001, 0.00000001, 0.000000001, 0.0000000001, 0.00000000001,
0.000000000001, 0.0000000000001, 0.00000000000001, 0.000000000000001, 0.0000000000000001, 0.00000000000000001
};
static_assert(std::is_floating_point_v<T>);
int i = (a[0] == '-') | (a[0] == '+');
T res = 0.0;
int sign = 1 - 2 * (a[0] == '-');
if (UNLIKELY(!a[0])) return NAN;
while (a[i] && a[i] != '.') {
if (UNLIKELY(a[i] < '0' || a[i] > '9')) {
return NAN;
}
res = res * static_cast<T>(10.0) + a[i] - '0';
i++;
}
if (LIKELY(a[i] != '\0')) {
i++;
int j = i;
//T mult = 0.1;
while (a[i]) {
if (UNLIKELY(a[i] < '0' || a[i] > '9')) {
return NAN;
}
res = res + (a[i] - '0') * multers[i - j];
// res = res + (a[i] - '0') * mult;
// mult *= 0.1;
i++;
}
}
return res * sign;
}
int main()
{
string inputs[] = {
"31.0911863667",
"30.9500",
"225.1293333333",
"16.4850",
"29.0507297346",
"147.9440517474",
"28.8500",
"213.4600",
"212.9105553333",
"199.1553333333",
"19.5884123000",
"3092458.37500000000"
};
int n = sizeof(inputs) / sizeof(inputs[0]);
for (int i = 0; i < n; i++) {
float res1 = std::atof(inputs[i].c_str());
float res2 = ParseFloat<double>(inputs[i].c_str());
if (!abscmp(res1, res2)) {
cout << std::fixed << std::setprecision(20) << "CompareConvert " << res1 << " " << res2 << " " << std::string(inputs[i]) << std::endl;
} else {
cout << std::fixed << std::setprecision(20) << "Correct " << res1 << std::endl;
}
}
}
我正在编写一个简单的解析器(具有完全有效性检查),因为它太慢了(在我的测试输入中平均快 3.2 倍 - 解析 CSV 文件的 GB)。公式非常简单,.但它给出的结果略有不同。std::atof
ParseFast
res = res * 10 + (a[i] - '0');
我知道这是因为 IEEE-754 浮点的局限性。但是有没有便宜的方法可以给出完全相同的结果?我需要它们完全相同,因为它正在与使用 sha256sum 而不是ParseFast
std::atof
fabs(a - b) < epsilon
运行命令:gcc 10.2.0g++ -o main main.cpp -O3 -std=c++17
编辑:解释为什么最后一个输入是错误的:小数部分正好介于 2 个可能的值和 之间。但是使用这种解析方法,在数字处,中间结果将是四舍五入。除此之外,仍然会给.0.375
0.5
0.25
.3
3092458.0 + 0.3 == 3092458.25
0.075
3092458.25
答:
我建议使用 fast_float 库 (https://github.com/fastfloat/fast_float)。据称它比(glibc 只是一个简单的包装器)快 4 到 10 倍。strtod
atod
strtod
评论
正如您正确注意到的那样,问题在于中间结果已经不精确,这加起来了。例如,中间存储已经不精确了,即使它可能后面跟着 ,这将使它成为 ,这将是完全可表示的。.2
5
.25
您需要将浮点数的小数部分累加为整数(仍然是浮点数,但没有小数部分),然后在末尾除以一次以调整指数:
// ...
if (LIKELY(a[i] != '\0')) {
T fraction = 0; // fractional part as an integer
T power = 1; // turns to 1, 10, 100, 1000, ... each loop iteration
i++;
while (a[i]) {
if (UNLIKELY(a[i] < '0' || a[i] > '9')) {
return NAN;
}
// note: additional logic is required to make sure that trailing
// zeros in the fraction cannot decrease the precision
power *= T{10};
fraction = fraction * T{10} + (a[i] - '0');
i++;
}
// note: 'power' can also be obtained from a look-up table like in
// your original code.
// Benchmark to make sure that it's actually faster to use a table.
res += fraction / power; // perform one division in the end, e.g. 375 / 100
}
return std::copysign(res, sign); // note: prefer copysign over multiplication
}
即使有这些更改,@cpplearner建议使用第三方库也可能更好。标准库函数类似也可能不提供最佳性能,并且性能会因标准库而异。std::strtof
std::to_chars
虽然您的解决方案在某些平台上可能更快,但在浮点乘法和除法成本更高的平台上,它的性能可能会差得多。浮点数很难。
评论
fast_float
power
fraction/power
评论
std::string
a[i] < '0' || a[i] > '9'
!std::isdigit(a[i])
unsigned
const char*
!std::isdigit(a[i])
int
unsigned
int
std::atof
返回 的类型为 。您正在用于内部计算。你可能想先比较一下,看看你有多接近。乍一看,我会说这可能是有问题的。正如您可能知道的那样,在二进制中不能清晰地表示,因此这不仅是一个直接可能的问题,而且当您这样做时也会变得复杂double
float
ParseFloat<double>
mult
0.1
mult *= 0.1
atof
atof
atof()
0.1