关于解析:如何快速解析C++中的空间分离浮点?

How to parse space-separated floats in C++ quickly?

我有一个文件,有数百万行,每行有3个由空格分隔的浮点数。读取文件需要很多时间,所以我尝试使用内存映射文件来读取它们,结果发现问题不在于IO的速度,而在于解析的速度。

我当前的分析是获取流(称为文件)并执行以下操作

1
2
float x,y,z;
file >> x >> y >> z;

堆栈溢出中有人建议使用boost.spirit,但我找不到任何简单的教程来解释如何使用它。

我正试图找到一种简单有效的方法来解析如下所示的行:

1
"134.32 3545.87 3425"

我会非常感谢你的帮助。我想用strtok来拆分它,但我不知道如何将字符串转换为float,我也不确定这是最好的方法。

我不介意解决方案是否会得到提高。我不介意它是否是有史以来最有效的解决方案,但我相信它有可能加倍的速度。

事先谢谢。


UPDATE

Since Spirit X3 is available for testing, I've updated the benchmarks. Meanwhile I've used Nonius to get statistically sound benchmarks.

All charts below are available interactive online

Benchmark CMake project + testdata used is on github: https://github.com/sehe/bench_float_parsing

enter image description here

总结:

Spirit解析器速度最快。如果你可以使用C++ 14考虑实验版本精神X3:

enter image description here

以上是使用内存映射文件的度量。使用iostreams时,通过板的速度会变慢,

enter image description here

但速度不如使用c/posix FILE*函数调用的scanf慢:

enter image description here

下面是旧答案的一部分

I implemented the Spirit version, and ran a benchmark comparing to the other suggested answers.

Here's my results, all tests run on the same body of input (515Mb of input.txt). See below for exact specs.



(wall clock time in seconds, average of 2+ runs)

To my own surprise, Boost Spirit turns out to be fastest, and most elegant:

  • handles/reports errors
  • supports +/-Inf and NaN and variable whitespace
  • no problems at all detecting the end of input (as opposed to the other mmap answer)
  • looks nice:

    1
    2
    3
    4
    bool ok = phrase_parse(f,l,               // source iterators
         (double_ > double_ > double_) % eol, // grammar
         blank,                               // skipper
         data);                               // output attribute

Note that boost::spirit::istreambuf_iterator was unspeakably much slower (15s+). I hope this helps!

Benchmark details

All parsing done into vector of struct float3 { float x,y,z; }.

Generate input file using

1
od -f -A none --width=12 /dev/urandom | head -n 11000000

This results in a 515Mb file containing data like

1
2
3
4
5
6
7
8
     -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

Compile the program using:

1
g++ -std=c++0x -g -O3 -isystem -march=native test.cpp -o test -lboost_filesystem -lboost_iostreams

Measure wall clock time using

1
time ./test < input.txt

环境:

  • Linux桌面4.2.0-42-generic 49 Ubuntu SMP x86
  • Intel(R)Core(tm)i7-3770k [email protected]
  • 32 GIB RAM

全码

旧基准的完整代码在本文的编辑历史中,最新版本在Github上


如果转换是瓶颈(这是很可能的),您应该从使用标准。从逻辑上讲,我们希望他们非常接近,但实际上,它们并不总是:

  • 您已经确定std::ifstream太慢了。

  • 将内存映射数据转换为std::istringstream。几乎肯定不是一个好的解决办法;你必须首先创建一个字符串,该字符串将复制所有数据。

  • 编写自己的streambuf直接从内存中读取,不复制(或使用不推荐使用的std::istrstream)可能是个解决方案,但如果问题确实是转换…这仍然使用相同的转换例程。

  • 您可以在映射的内存上尝试fscanfscanf。溪流。根据实现情况,它们可能更快比各种各样的istream实现还要多。

  • 使用strtod可能比任何一种方法都快。不需要为此,标记化:strtod跳过了前导空格(包括'
    '
    ,并有一个out参数,它将未读取第一个字符的地址。最终条件是有点棘手,您的循环可能看起来有点像:

1
2
3
4
5
6
7
8
9
10
11
12
    char* begin;    //  Set to point to the mmap'ed data...
                    //  You'll also have to arrange for a '\0'
                    //  to follow the data.  This is probably
                    //  the most difficult issue.
    char* end;
    errno = 0;
    double tmp = strtod( begin, &end );
    while ( errno == 0 && end != begin ) {
        //  do whatever with tmp...
        begin = end;
        tmp = strtod( begin, &end );
    }

如果这些都不够快,你就得考虑实际数据。它可能还有一些附加的约束,这意味着您可以编写比一般程序更快的转换程序;例如,strtod必须处理固定的和科学的,并且必须100%准确,即使有17个有效数字。它还必须是特定于区域设置的。所有这些都已添加复杂性,这意味着要执行添加的代码。但要当心:编写一个有效和正确的转换例程,即使是一组有限的输入是非常重要的;您确实需要知道你在做什么。

编辑:

只是出于好奇,我做了一些测试。除了在前面提到的解决方案中,我编写了一个简单的自定义转换器,只处理固定点(不科学),最多小数点后五位,小数点前的值必须适合int

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)编译:

1
2
3
4
5
6
7
8
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)编译:

1
2
3
4
5
6
7
8
9
10
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)范围内。

最引人注目的是窗户下的fstreamfscan(和相对较小的fscanstrtod之间的差异。第二件事是简单的自定义转换函数在两个平台。必要的错误处理会减慢速度有点,但差别仍然很大。我期待有些改进,因为它处理不了很多事情,标准转换例程可以(如科学格式,非常非常小的数字、inf和nan、i18n等),但不是这个很多。


在开始之前,请确认这是应用程序中速度较慢的部分,并在其周围安装一个测试工具,以便您可以度量改进。

在我看来,江户十一〔0〕会因此而被过度杀戮。试试fscanf

1
2
3
4
5
6
7
8
9
10
11
12
13
FILE* f = fopen("yourfile");
if (NULL == f) {
   printf("Failed to open 'yourfile'");
   return;
}
float x,y,z;
int nItemsRead = fscanf(f,"%f %f %f
"
, &x, &y, &z);
if (3 != nItemsRead) {
   printf("Oh dear, items aren't in the right format.
"
);
   return;
}


我会使用IFString查看这个相关帖子来读取浮点,或者如何在C++中标记字符串,特别是与C++ String工具包库相关的帖子。我已经使用了C Strtok、C++流、Boost Tokisher,其中最适合的是C++String工具包库。


我认为字符串处理中最重要的规则是"一次只读取一个字符"。我想,它总是更简单、更快、更可靠。

我做了一个简单的基准测试程序来展示它有多简单。我的测试表明这个代码比strtod版本快40%。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#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
"
, 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
"
, 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
"
, sum);
}

int main()
{
    const int n = 1000000;
    printf("count = %d
"
, n);

    string in = test_generate(n);    
    {
        struct timeval t1;
        gettimeofday(&t1, 0);
        printf("scan start
"
);

        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
"
, elapsed);
    }

    {
        struct timeval t1;
        gettimeofday(&t1, 0);
        printf("scan start
"
);

        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
"
, elapsed);
    }
    return 0;
}

下面是i7 mac book pro(在xcode 4.6中编译)的控制台输出。

1
2
3
4
5
6
7
8
count = 1000000
generated -1073202156466.638184
scan start
scanned -1073202156466.638184
elapsed 83.34ms
scan start
scanned -1073202156466.638184
elapsed 53.50ms


一个棘手的解决方案是向问题中抛出更多的内核,从而产生多个线程。如果瓶颈仅仅是CPU,则可以通过生成两个线程(在多核CPU上)将运行时间减半。

其他一些提示:

  • 尽量避免从库中分析函数,例如boost和/或std。它们会因错误检查条件而膨胀,并且大部分处理时间都花在这些检查上。对于几个转换来说,它们是好的,但在处理数百万个值时却惨遭失败。如果您已经知道您的数据格式良好,您可以编写(或查找)一个自定义优化的C函数,它只执行数据转换。

  • 使用一个大的内存缓冲区(比如10兆字节),在缓冲区中加载文件块并在其中进行转换。

  • Divide et Impera:将问题拆分为更小更简单的问题:预处理文件,使其成为单行单浮点数,用"."字符拆分每行,并转换整数而不是浮点数,然后合并两个整数以创建浮点数。


使用C将是最快的解决方案。split into tokens using strtokand thenconvert to float with strtof.或者,如果您知道确切的格式,请使用fscanf