如何在 C++ 中快速解析空格分隔的浮点数?
Posted
技术标签:
【中文标题】如何在 C++ 中快速解析空格分隔的浮点数?【英文标题】:How to parse space-separated floats in C++ quickly? 【发布时间】:2013-07-04 08:10:03 【问题描述】:我有一个包含数百万行的文件,每行有 3 个浮点数,以空格分隔。读取文件需要很多时间,所以我尝试使用内存映射文件读取它们,结果发现问题不在于 IO 的速度,而在于解析的速度。
我当前的解析是获取流(称为文件)并执行以下操作
float x,y,z;
file >> x >> y >> z;
Stack Overflow 中有人推荐使用 Boost.Spirit,但我找不到任何简单的教程来解释如何使用它。
我正在尝试找到一种简单有效的方法来解析如下所示的行:
"134.32 3545.87 3425"
我将非常感谢一些帮助。本来想用strtok来拆分的,但是不知道怎么把字符串转成浮点数,也不太确定是不是最好的办法。
我不介意解决方案是否是 Boost。我不介意这不是有史以来最有效的解决方案,但我确信它可以将速度提高一倍。
提前致谢。
【问题讨论】:
如果您如此关心速度,为什么不切换到二进制文件格式? 你试过只用fscanf
吗?
我无法切换到二进制格式,因为这是我的输入。
@alfC 看看这个帖子:***.com/questions/9371238/…
我庆祝我作为北极熊的回归,为您带来了 float3 文件解析器的综合基准......结果令人惊讶(至少对我而言)***.com/a/17479702/85371
【参考方案1】:
更新
由于 Spirit X3 可用于测试,我已经更新了基准测试。同时,我使用Nonius 来获得统计上合理的基准。
以下所有图表均可用interactive online
使用的基准 CMake 项目 + 测试数据在 github 上:https://github.com/sehe/bench_float_parsing
总结:
Spirit 解析器是最快的。如果可以使用 C++14,可以考虑实验版 Spirit X3:
以上是使用内存映射文件的措施。使用 iostreams 会更慢,
但没有 scanf
使用 C/POSIX FILE*
函数调用那么慢:
以下是旧答案的部分内容
我实现了 Spirit 版本,并与其他建议的答案进行了比较。
这是我的结果,所有测试都在相同的输入主体上运行(
input.txt
的 515Mb)。请参阅下文了解具体规格。(挂钟时间,以秒为单位,平均运行 2 次以上)
令我惊讶的是,Boost Spirit 速度最快,而且最优雅:
处理/报告错误 支持 +/-Inf 和 NaN 以及可变空格 检测输入结束完全没有问题(与其他 mmap 答案相反)看起来不错:
bool ok = phrase_parse(f,l, // source iterators (double_ > double_ > double_) % eol, // grammar blank, // skipper data); // output attribute
请注意,
boost::spirit::istreambuf_iterator
的速度要慢得多(15 秒以上)。我希望这会有所帮助!基准详情
所有解析完成到
vector
的struct float3 float x,y,z;
。使用生成输入文件
od -f -A none --width=12 /dev/urandom | head -n 11000000
这会生成一个 515Mb 的文件,其中包含类似的数据
-2627.0056 -1.967235e-12 -2.2784738e+33 -1.0664798e-27 -4.6421956e-23 -6.917859e+20 -1.1080849e+36 2.8909405e-33 1.7888695e-12 -7.1663235e+33 -1.0840628e+36 1.5343362e-12 -3.1773715e-17 -6.3655537e-22 -8.797282e+31 9.781095e+19 1.7378472e-37 63825084 -1.2139188e+09 -5.2464635e-05 -2.1235992e-38 3.0109424e+08 5.3939846e+30 -6.6146894e-20
编译程序使用:
g++ -std=c++0x -g -O3 -isystem -march=native test.cpp -o test -lboost_filesystem -lboost_iostreams
使用测量挂钟时间
time ./test < input.txt
环境:
Linux 桌面 4.2.0-42-generic #49-Ubuntu SMP x86_64 Intel(R) Core(TM) i7-3770K CPU @ 3.50GHz 32GiB 内存完整代码
旧基准的完整代码在edit history of this post,最新版本是on github
【讨论】:
@LightnessRacesinOrbit 为什么会这样?挂钟时间是相关度量(当然,“挂钟”是比喻性的语言,以确保您理解我们的意思是总经过时间,而不是系统时间或 CPU 时间。这是基准术语。)感觉免费改进基准演示! @sehe:我将“挂墙时间”读为经过的系统时间。我想您故意使用它而不是 CPU 时间来测量 I/O 活动以及其他所有内容,但是您也在测量其他进程使用的时间。 @sehe:你实际跑了多少次?估计不止2个?!尽管输入和时间范围相对较大,但要获得良好的基准。 (请注意,我觉得这个答案很有趣,不要质疑其结果的精神 [sic]!) @LightnessRacesinOrbit 我想我最终至少运行了 50 次(每个场景超过 10 次)。是的,我现在睡眠不足。我只是为实际结果表平均了 2 个数字。并不是说运行之间存在任何显着性偏差......【参考方案2】:如果转化是瓶颈(这很有可能), 你应该从使用不同的可能性开始 标准。从逻辑上讲,人们会期望它们非常接近, 但实际上,它们并不总是:
你已经确定std::ifstream
太慢了。
将内存映射数据转换为std::istringstream
几乎可以肯定不是一个好的解决方案;你首先必须
创建一个字符串,它将复制所有数据。
写自己的streambuf
直接从内存中读取,
无需复制(或使用已弃用的std::istrstream
)
可能是一个解决方案,尽管如果问题真的是
转换...这仍然使用相同的转换例程。
您可以随时在内存映射上尝试fscanf
或scanf
溪流。根据实现,它们可能会更快
比各种istream
实现。
使用strtod
可能比其中任何一个都快。没必要
为此进行标记:strtod
跳过前导空格
(包括'\n'
),并且有一个输出参数
未读取的第一个字符的地址。结束条件是
有点棘手,你的循环可能看起来有点像:
如果这些都不够快,您将不得不考虑
实际数据。它可能有某种额外的
约束,这意味着您可以编写
一个比更一般的转换程序更快的转换程序;
例如strtod
必须同时处理固定和科学,它
即使有 17 个有效数字,也必须 100% 准确。
它还必须是特定于语言环境的。所有这些都添加了
复杂性,这意味着添加了要执行的代码。但请注意:
编写一个有效且正确的转换例程,即使对于
一组受限制的输入,是不平凡的;你真的必须
知道你在做什么。
编辑:
出于好奇,我进行了一些测试。除了
前面提到的解决方案,我写了一个简单的自定义转换器,
它只处理固定点(不科学),最多
小数点后五位,小数点前的值
必须适合int
:
double
convert( char const* source, char const** endPtr )
char* end;
int left = strtol( source, &end, 10 );
double results = left;
if ( *end == '.' )
char* start = end + 1;
int right = strtol( start, &end, 10 );
static double const fracMult[]
= 0.0, 0.1, 0.01, 0.001, 0.0001, 0.00001 ;
results += right * fracMult[ end - start ];
if ( endPtr != nullptr )
*endPtr = end;
return results;
(如果你真的使用这个,你肯定应该添加一些错误 处理。这只是为了实验而快速敲掉的 目的,读取我生成的测试文件,什么都没有 否则。)
接口与strtod
一模一样,为了简化编码。
我在两个环境中运行了基准测试(在不同的机器上, 所以任何时间的绝对值都不相关)。我拿到 结果如下:
在 Windows 7 下,使用 VC 11 (/O2) 编译:
Testing Using fstream directly (5 iterations)...
6.3528e+006 microseconds per iteration
Testing Using fscan directly (5 iterations)...
685800 microseconds per iteration
Testing Using strtod (5 iterations)...
597000 microseconds per iteration
Testing Using manual (5 iterations)...
269600 microseconds per iteration
Linux 2.6.18 下,使用 g++ 4.4.2 (-O2, IIRC) 编译:
Testing Using fstream directly (5 iterations)...
784000 microseconds per iteration
Testing Using fscanf directly (5 iterations)...
526000 microseconds per iteration
Testing Using strtod (5 iterations)...
382000 microseconds per iteration
Testing Using strtof (5 iterations)...
360000 microseconds per iteration
Testing Using manual (5 iterations)...
186000 microseconds per iteration
在所有情况下,我正在阅读 554000 行,每行随机包含 3 行
在[0...10000)
范围内生成浮点数。
最引人注目的是两者之间的巨大差异
Windows下的fstream
和fscan
(以及比较小的
fscan
和 strtod
之间的区别)。第二件事是
简单的自定义转换功能获得了多少,关于
两个平台。必要的错误处理会减慢它
有一点,但差别还是很大的。我期望
一些改进,因为它不能处理很多事情
标准转换例程(如科学格式,
非常非常小的数字,Inf 和 NaN,i18n 等),但不是这个
很多。
【讨论】:
这个答案给了我最好的表现,所以我把它标记为接受的答案。我觉得有义务说杰夫福斯特的回答也很有帮助 @OopsUser,那么多少行/三胞胎的速度是多少?>>
需要 9 秒,fscanf
需要 4.5 秒,strtod
需要多长时间?
令我惊讶的是,我发现 Spirit 在解析内存映射数据时要快得多。我将它与这种方法以及我的答案中的 fscanf 解决方案一起进行了基准测试。
我刚刚编辑了我的答案以包含我自己的基准测试结果。
下面更“完整”的高速 str->float 例程。 crack_atof
: ***.com/a/59013147/1087626【参考方案3】:
在开始之前,请确认这是您的应用程序的慢速部分,并围绕它获取测试工具,以便您可以衡量改进。
boost::spirit
在我看来,这将是矫枉过正。试试fscanf
FILE* f = fopen("yourfile");
if (NULL == f)
printf("Failed to open 'yourfile'");
return;
float x,y,z;
int nItemsRead = fscanf(f,"%f %f %f\n", &x, &y, &z);
if (3 != nItemsRead)
printf("Oh dear, items aren't in the right format.\n");
return;
【讨论】:
抱歉这个菜鸟问题,但是我如何循环文件,我可以做类似 while(!f.eof()) 的事情吗? 在回复初学者时不能省略错误处理。 @OopsUser:不,这是个坏主意。更好的主意是首先检查您的读取是否有效(读取三个浮点数)。如果没有,有两个可能的原因:格式错误或 EOF。只有在那个时候你才应该检查f.eof()
非常感谢我当前的代码在 4.5 秒内读取了一个包含 554,000 个点(行)的 15 MB 文件,而不是原始解析的 9 秒。如果我只使用 ifstream 然后使用 file.getLine(),那么我只需要 0.9 秒,所以大部分速度仍然在解析
@OopsUser 有效地解析双打显然不简单,而且需要时间。请记住,来自文件的>>
和fscanf
都必须处理科学格式和固定格式,并且两者都对语言环境敏感——fscanf
和>>
具有如此不同性能的主要原因是因为 C++ 语言环境有效使用要尴尬得多。 (尴尬,但并非不可能。但大多数实现似乎满足于使用最明显的解决方案,即使它明显慢得多。)【参考方案4】:
我会查看此相关帖子Using ifstream to read floats 或How do I tokenize a string in C++,尤其是与 C++ 字符串工具包库相关的帖子。我使用过 C strtok、C++ 流、Boost 标记器,其中最好的是 C++ String Toolkit Library。
【讨论】:
【参考方案5】:编辑:对于那些担心crack_atof 无法以任何方式验证的人,请参阅底部关于Ryu 的cmets。
这里有一个更完整的(尽管按照任何标准都不是“官方的”)高速字符串转双例程,因为好的 C++17 from_chars()
解决方案只适用于 MSVC(不是 clang 或 gcc)。
认识crack_atof
https://gist.github.com/oschonrock/a410d4bec6ec1ccc5a3009f0907b3d15
不是我的工作,我只是稍微重构了一下。并更改了签名。代码很容易理解,很明显为什么它很快。而且速度非常快,请在此处查看基准:
https://www.codeproject.com/Articles/1130262/Cplusplus-string-view-Conversion-to-Integral-Types
我用 11,000,000 行 3 个浮点数运行它(csv 中的 15 位精度,这很重要!)。在我老旧的第二代酷睿 i7 2600 上,它的运行时间为 1.327 秒。 Kubuntu 19.04 上的编译器 clang V8.0.0 -O2。
下面的完整代码。我正在使用 mmap,因为 str->float 不再是唯一的瓶颈,这要归功于crack_atof。我已经将 mmap 的东西包装到一个类中,以确保地图的 RAII 版本。
#include <iomanip>
#include <iostream>
// for mmap:
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
class MemoryMappedFile
public:
MemoryMappedFile(const char* filename)
int fd = open(filename, O_RDONLY);
if (fd == -1) throw std::logic_error("MemoryMappedFile: couldn't open file.");
// obtain file size
struct stat sb;
if (fstat(fd, &sb) == -1) throw std::logic_error("MemoryMappedFile: cannot stat file size");
m_filesize = sb.st_size;
m_map = static_cast<const char*>(mmap(NULL, m_filesize, PROT_READ, MAP_PRIVATE, fd, 0u));
if (m_map == MAP_FAILED) throw std::logic_error("MemoryMappedFile: cannot map file");
~MemoryMappedFile()
if (munmap(static_cast<void*>(const_cast<char*>(m_map)), m_filesize) == -1)
std::cerr << "Warnng: MemoryMappedFile: error in destructor during `munmap()`\n";
const char* start() const return m_map;
const char* end() const return m_map + m_filesize;
private:
size_t m_filesize = 0;
const char* m_map = nullptr;
;
// high speed str -> double parser
double pow10(int n)
double ret = 1.0;
double r = 10.0;
if (n < 0)
n = -n;
r = 0.1;
while (n)
if (n & 1)
ret *= r;
r *= r;
n >>= 1;
return ret;
double crack_atof(const char* start, const char* const end)
if (!start || !end || end <= start)
return 0;
int sign = 1;
double int_part = 0.0;
double frac_part = 0.0;
bool has_frac = false;
bool has_exp = false;
// +/- sign
if (*start == '-')
++start;
sign = -1;
else if (*start == '+')
++start;
while (start != end)
if (*start >= '0' && *start <= '9')
int_part = int_part * 10 + (*start - '0');
else if (*start == '.')
has_frac = true;
++start;
break;
else if (*start == 'e')
has_exp = true;
++start;
break;
else
return sign * int_part;
++start;
if (has_frac)
double frac_exp = 0.1;
while (start != end)
if (*start >= '0' && *start <= '9')
frac_part += frac_exp * (*start - '0');
frac_exp *= 0.1;
else if (*start == 'e')
has_exp = true;
++start;
break;
else
return sign * (int_part + frac_part);
++start;
// parsing exponent part
double exp_part = 1.0;
if (start != end && has_exp)
int exp_sign = 1;
if (*start == '-')
exp_sign = -1;
++start;
else if (*start == '+')
++start;
int e = 0;
while (start != end && *start >= '0' && *start <= '9')
e = e * 10 + *start - '0';
++start;
exp_part = pow10(exp_sign * e);
return sign * (int_part + frac_part) * exp_part;
int main()
MemoryMappedFile map = MemoryMappedFile("FloatDataset.csv");
const char* curr = map.start();
const char* start = map.start();
const char* const end = map.end();
uintmax_t lines_n = 0;
int cnt = 0;
double sum = 0.0;
while (curr && curr != end)
if (*curr == ',' || *curr == '\n')
// std::string fieldstr(start, curr);
// double field = std::stod(fieldstr);
// m_numLines = 11000000 cnt=33000000 sum=16498294753551.9
// real 5.998s
double field = crack_atof(start, curr);
// m_numLines = 11000000 cnt=33000000 sum=16498294753551.9
// real 1.327s
sum += field;
++cnt;
if (*curr == '\n') lines_n++;
curr++;
start = curr;
else
++curr;
std::cout << std::setprecision(15) << "m_numLines = " << lines_n << " cnt=" << cnt
<< " sum=" << sum << "\n";
代码也在 github gist 上:
https://gist.github.com/oschonrock/67fc870ba067ebf0f369897a9d52c2dd
【讨论】:
crack_atof
似乎没有在任何地方测试准确性和边缘情况。我不愿意在生产中使用它。
@EmileCormier 没错,我同意 但是我们现在有了 Ryu:github.com/ulfjack/ryu 广受赞誉的 Double =>String 部分已被采用到 <charconv>to_chars
的 MSVC 实现中。 String => Double 解析仍然较新(于 2019 年 12 月首次提交),但这更容易,我希望这将成熟并迅速得到验证。 ——我已经在使用它了。我的库中有一个包装器,它采用 string_view 并将 <charconv>to|from_chars
用于 ints/ 对于双打,它直接将 ryu 用于 clang/ggc 和 MSVC 的标准实现,
@EmileCormier 我只是用 Ryu 而不是 crack_atof
重新运行了上面的代码。它没有那么快(但可能已经像你说的那样更正确了)。 1.995 秒。
感谢您让我了解 Ryu!我一直想使用from_chars
,但它在 Clang/GCC 上尚不可用。 Ryu 应该在此期间作为一个不错的后备。【参考方案6】:
一个具体的解决方案是在问题上投入更多的核心,产生多个线程。 如果瓶颈只是 CPU,您可以通过生成两个线程(在多核 CPU 上)将运行时间减半
其他一些提示:
尽量避免解析库中的函数,例如 boost 和/或 std。它们因错误检查条件而臃肿,并且大部分处理时间都花在了这些检查上。只需进行几次转换,它们就可以了,但在处理数百万个值时却惨遭失败。如果您已经知道您的数据格式正确,您可以编写(或查找)一个自定义优化的 C 函数,该函数只进行数据转换
使用一个大的内存缓冲区(比如说 10 MB),您可以在其中加载文件的块并在那里进行转换
divide et impera:将您的问题拆分为更简单的问题:预处理您的文件,使其成为单行单浮点数,用“.”分隔每一行字符并转换整数而不是浮点数,然后将两个整数合并以创建浮点数
【讨论】:
他说解析是瓶颈,不是IO访问。 不解析0.4秒读取25万行,解析4.5秒。我使用了 boost 映射文件,他想尽可能快地读取它们。 使用我最快的解决方案,我已经达到了 11,000,000 行的 3.18 秒。 62x 的速度差异当然可能完全取决于我的计算机速度更快...... :)【参考方案7】:我相信字符串处理中最重要的一条规则是“一次只读取一次,一次一个字符”。我认为它总是更简单、更快、更可靠。
我制作了简单的基准程序来展示它是多么简单。我的测试表明这段代码的运行速度比strtod
版本快40%。
#include <iostream>
#include <sstream>
#include <iomanip>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <sys/time.h>
using namespace std;
string test_generate(size_t n)
srand((unsigned)time(0));
double sum = 0.0;
ostringstream os;
os << std::fixed;
for (size_t i=0; i<n; ++i)
unsigned u = rand();
int w = 0;
if (u > UINT_MAX/2)
w = - (u - UINT_MAX/2);
else
w = + (u - UINT_MAX/2);
double f = w / 1000.0;
sum += f;
os << f;
os << " ";
printf("generated %f\n", sum);
return os.str();
void read_float_ss(const string& in)
double sum = 0.0;
const char* begin = in.c_str();
char* end = NULL;
errno = 0;
double f = strtod( begin, &end );
sum += f;
while ( errno == 0 && end != begin )
begin = end;
f = strtod( begin, &end );
sum += f;
printf("scanned %f\n", sum);
double scan_float(const char* str, size_t& off, size_t len)
static const double bases[13] =
0.0, 10.0, 100.0, 1000.0, 10000.0,
100000.0, 1000000.0, 10000000.0, 100000000.0,
1000000000.0, 10000000000.0, 100000000000.0, 1000000000000.0,
;
bool begin = false;
bool fail = false;
bool minus = false;
int pfrac = 0;
double dec = 0.0;
double frac = 0.0;
for (; !fail && off<len; ++off)
char c = str[off];
if (c == '+')
if (!begin)
begin = true;
else
fail = true;
else if (c == '-')
if (!begin)
begin = true;
else
fail = true;
minus = true;
else if (c == '.')
if (!begin)
begin = true;
else if (pfrac)
fail = true;
pfrac = 1;
else if (c >= '0' && c <= '9')
if (!begin)
begin = true;
if (pfrac == 0)
dec *= 10;
dec += c - '0';
else if (pfrac < 13)
frac += (c - '0') / bases[pfrac];
++pfrac;
else
break;
if (!fail)
double f = dec + frac;
if (minus)
f = -f;
return f;
return 0.0;
void read_float_direct(const string& in)
double sum = 0.0;
size_t len = in.length();
const char* str = in.c_str();
for (size_t i=0; i<len; ++i)
double f = scan_float(str, i, len);
sum += f;
printf("scanned %f\n", sum);
int main()
const int n = 1000000;
printf("count = %d\n", n);
string in = test_generate(n);
struct timeval t1;
gettimeofday(&t1, 0);
printf("scan start\n");
read_float_ss(in);
struct timeval t2;
gettimeofday(&t2, 0);
double elapsed = (t2.tv_sec - t1.tv_sec) * 1000000.0;
elapsed += (t2.tv_usec - t1.tv_usec) / 1000.0;
printf("elapsed %.2fms\n", elapsed);
struct timeval t1;
gettimeofday(&t1, 0);
printf("scan start\n");
read_float_direct(in);
struct timeval t2;
gettimeofday(&t2, 0);
double elapsed = (t2.tv_sec - t1.tv_sec) * 1000000.0;
elapsed += (t2.tv_usec - t1.tv_usec) / 1000.0;
printf("elapsed %.2fms\n", elapsed);
return 0;
以下是 i7 Mac Book Pro 的控制台输出(在 XCode 4.6 中编译)。
count = 1000000
generated -1073202156466.638184
scan start
scanned -1073202156466.638184
elapsed 83.34ms
scan start
scanned -1073202156466.638184
elapsed 53.50ms
【讨论】:
这不解析指数(314e-2
例如),不解析 NaN 或无穷大,不处理空格(甚至不处理指定的换行符)。我不确定我是否相信 scan_float
从这个起点解析准确的结果。
我跑了 my benchmark,纠正了不支持的输入位 sed -i 's/e[-+][0-9][0-9]//g'
和 sed -i 's/nan/0.0/g'
并调整代码以匹配其余的基准(即解析空白...)。我得到了大约 1.84 秒。请注意,输入实际上减少到 408Mb(从 515Mb,减少了 21%)。对此进行补偿将给 2.32 秒
当然,这比 Spirit 版本要快一些,但仅快了约 25%(或半 GiB 输入时约 0.9 秒...)。不足以保证所显示的限制,IMO。 完全披露:我用来测量此代码的程序:http://ideone.com/yFBlpF /cc @OopsUser【参考方案8】:
使用 C 将是最快的解决方案。 使用 使用strtok
拆分成token,然后strtof
转换为float。或者,如果您知道确切的格式,请使用 fscanf
。
【讨论】:
使用strtok
不会解决任何问题(如果你直接访问内存映射数据,你不能使用它,因为数据将被只读)。以上是关于如何在 C++ 中快速解析空格分隔的浮点数?的主要内容,如果未能解决你的问题,请参考以下文章