关于C++:为什么std::fstreams这么慢?

Why are std::fstreams so slow?

我正在研究一个简单的解析器,当分析时,我发现瓶颈在…文件读取!我提取了非常简单的测试来比较fstreamsFILE*在读取大数据块时的性能:

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
#include <stdio.h>
#include <chrono>
#include <fstream>
#include <iostream>
#include <functional>

void measure(const std::string& test, std::function<void()> function)
{
    auto start_time = std::chrono::high_resolution_clock::now();

    function();

    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start_time);
    std::cout<<test<<""<<static_cast<double>(duration.count()) * 0.000001<<" ms"<<std::endl;
}

#define BUFFER_SIZE (1024 * 1024 * 1024)

int main(int argc, const char * argv[])
{
    auto buffer = new char[BUFFER_SIZE];
    memset(buffer, 123, BUFFER_SIZE);

    measure("FILE* write", [buffer]()
    {
        FILE* file = fopen("test_file_write","wb");
        fwrite(buffer, 1, BUFFER_SIZE, file);
        fclose(file);
    });
    measure("FILE* read", [buffer]()
    {
        FILE* file = fopen("test_file_read","rb");
        fread(buffer, 1, BUFFER_SIZE, file);
        fclose(file);
    });
    measure("fstream write", [buffer]()
    {
        std::ofstream stream("test_stream_write", std::ios::binary);
        stream.write(buffer, BUFFER_SIZE);
    });
    measure("fstream read", [buffer]()
    {
        std::ifstream stream("test_stream_read", std::ios::binary);
        stream.read(buffer, BUFFER_SIZE);
    });

    delete[] buffer;
}

在我的计算机上运行此代码的结果是:

1
2
3
4
FILE* write 1388.59 ms
FILE* read 1292.51 ms
fstream write 3105.38 ms
fstream read 3319.82 ms

fstream写/读比FILE*写/读慢2倍!这是在读取一大串数据时,没有任何解析或fstreams的其他特性。我在Mac OS、Intel i7 2.6GHz、16GB 1600 MHz RAM、SSD驱动器上运行代码。请注意,再次运行相同的代码,FILE* read的时间非常短(约200毫秒),可能是因为文件被缓存…这就是为什么打开阅读的文件不是用代码创建的。

为什么在使用fstream读取一个二进制数据块时,与FILE*相比,速度如此之慢?

编辑1:我更新了代码和时间。抱歉耽搁了!

编辑2:我添加了命令行和新结果(与以前的非常相似!)

1
2
3
4
5
6
$ clang++  main.cpp -std=c++11 -stdlib=libc++ -O3
$ ./a.out
FILE* write 1417.9 ms
FILE* read 1292.59 ms
fstream write 3214.02 ms
fstream read 3052.56 ms

根据第二次运行的结果:

1
2
3
4
5
$ ./a.out
FILE* write 1428.98 ms
FILE* read 196.902 ms
fstream write 3343.69 ms
fstream read 2285.93 ms

在读取FILE*stream时,文件似乎都会被缓存,因为时间会减少,两者的数量相同。

编辑3:我将代码简化为:

1
2
3
4
5
6
FILE* file = fopen("test_file_write","wb");
fwrite(buffer, 1, BUFFER_SIZE, file);
fclose(file);

std::ofstream stream("test_stream_write", std::ios::binary);
stream.write(buffer, BUFFER_SIZE);

启动了分析器。似乎streamxsputn函数中花费了大量时间,而实际的write调用具有相同的持续时间(应该是相同的函数…)

1
2
3
4
5
6
Running    Time     Self       Symbol Name
3266.0ms   66.9%    0,0        std::__1::basic_ostream<char, std::__1::char_traits<char> >::write(char const*, long)
3265.0ms   66.9%    2145,0          std::__1::basic_streambuf<char, std::__1::char_traits<char> >::xsputn(char const*, long)
1120.0ms   22.9%    7,0                 std::__1::basic_filebuf<char, std::__1::char_traits<char> >::overflow(int)
1112.0ms   22.7%    2,0                      fwrite
1127.0ms   23.0%    0,0        fwrite

出于某种原因,编辑4将此问题标记为重复。我想指出的是,我根本不使用printf,我只使用std::cout来写时间。read部分使用的文件是write部分的输出,用不同的名称复制以避免缓存


在Linux上,对于这一大数据集,fwrite的实现似乎效率更高,因为它使用write,而不是writev

我不知道为什么writevwrite慢得多,但这似乎就是区别所在。我完全没有看到真正的理由来解释为什么fstream需要在这种情况下使用这个构造。

这可以通过使用strace ./a.out很容易看出(其中a.out是测试这个的程序)。

输出:

Fstream:

1
2
3
4
5
6
7
clock_gettime(CLOCK_REALTIME, {1411978373, 114560081}) = 0
open("test", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
writev(3, [{NULL, 0}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1073741824}], 2) = 1073741824
close(3)                                = 0
clock_gettime(CLOCK_REALTIME, {1411978386, 376353883}) = 0
write(1,"fstream write 13261.8 ms
"
, 25fstream write 13261.8 ms) = 25

文件*:

1
2
3
4
5
6
clock_gettime(CLOCK_REALTIME, {1411978386, 930326134}) = 0
open("test", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3,"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 1073741824) = 1073741824
clock_gettime(CLOCK_REALTIME, {1411978388, 584197782}) = 0
write(1,"FILE* write 1653.87 ms
"
, 23FILE* write 1653.87 ms) = 23

我没有漂亮的固态硬盘驱动器,所以我的机器会慢一点-或者其他东西在我的情况下慢一点。

正如JanHudec所指出的,我误解了结果。我刚刚写的:

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <chrono>

void measure(const std::string& test, std::function<void()> function)
{
    auto start_time = std::chrono::high_resolution_clock::now();

    function();

    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start_time);
    std::cout<<test<<""<<static_cast<double>(duration.count()) * 0.000001<<" ms"<<std::endl;
}

#define BUFFER_SIZE (1024 * 1024 * 1024)


int main()
{
    auto buffer = new char[BUFFER_SIZE];
    memset(buffer, 0, BUFFER_SIZE);

    measure("writev", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY);
        struct iovec vec[] =
        {
            { NULL, 0 },
            { (void *)buffer, BUFFER_SIZE }
        };
        writev(fd, vec, sizeof(vec)/sizeof(vec[0]));
        close(fd);
    });

    measure("write", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY);
        write(fd, buffer, BUFFER_SIZE);
        close(fd);
    });
}

实际的fstream实现做了一些愚蠢的事情——可能以小块、某处、某种方式或类似的方式复制整个数据。我会进一步调查的。

两种情况下的结果几乎相同,并且比问题中的fstreamFILE*变体更快。

编辑:

在我的机器上,现在,如果您在写入后添加fclose(file),我的系统上fstreamFILE*的写入时间大致相同,大约需要13秒,使用老式旋转磁盘类型的驱动器(而不是SSD)写入1GB。

但是,我可以使用此代码更快地编写:

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
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <chrono>

void measure(const std::string& test, std::function<void()> function)
{
    auto start_time = std::chrono::high_resolution_clock::now();

    function();

    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start_time);
    std::cout<<test<<""<<static_cast<double>(duration.count()) * 0.000001<<" ms"<<std::endl;
}

#define BUFFER_SIZE (1024 * 1024 * 1024)


int main()
{
    auto buffer = new char[BUFFER_SIZE];
    memset(buffer, 0, BUFFER_SIZE);

    measure("writev", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY, 0660);
        struct iovec vec[] =
        {
            { NULL, 0 },
            { (void *)buffer, BUFFER_SIZE }
        };
        writev(fd, vec, sizeof(vec)/sizeof(vec[0]));
        close(fd);
    });

    measure("write", [buffer]()
    {
        int fd = open("test", O_CREAT|O_WRONLY, 0660);
        write(fd, buffer, BUFFER_SIZE);
        close(fd);
    });
}

给出约650-900 ms的时间。

我还可以编辑原始程序,为fwrite提供大约1000毫秒的时间-只需删除fclose

我还添加了这个方法:

1
2
3
4
5
6
measure("fstream write (new)", [buffer]()
{
    std::ofstream* stream = new std::ofstream("test", std::ios::binary);
    stream->write(buffer, BUFFER_SIZE);
    // Intentionally no delete.
});

然后这里也需要1000毫秒。

所以,我的结论是,有时候,关闭文件会使它刷新到磁盘上。在其他情况下,我不明白为什么…


tl;dr:在编写代码之前,请尝试将其添加到代码中:

1
2
3
const size_t bufsize = 256*1024;
char buf[bufsize];
mystream.rdbuf()->pubsetbuf(buf, bufsize);

使用fstream处理大型文件时,请确保使用流缓冲区。

相反,禁用流缓冲会显著降低性能。在没有设置缓冲区的情况下,MSVC实现至少一次将1个字符复制到filebuf(参见streambuf::xsputn()),这会使应用程序受到CPU限制,从而降低I/O速率。

注意:您可以在这里找到一个完整的示例应用程序。


与其他答案相反,大型文件读取的一个大问题来自于C标准库的缓冲。尝试使用低级别的read/write大区块(1024kb)调用,并查看性能跳跃。

C库的文件缓冲对于读取或写入小数据块(小于磁盘块大小)很有用。

在Windows上,在读取和写入原始视频流时,我的性能几乎提高了3倍。

我还使用本机OS(Win32)API调用打开了该文件,并告诉操作系统不要缓存该文件,因为这涉及到另一个副本。


流在mac、旧的实现或设置上以某种方式中断。

旧的安装程序可能会导致文件写入到exe目录和用户目录中的流中,除非您有2个磁盘或其他不同的设置,否则这不会有任何区别。

在我糟糕的远景里我得到正常缓冲+未缓存:C++ 201103文件*写入4756 ms文件*读取5007 msfstream写入5526 msfstream读取5728 ms

普通缓冲区+缓存:C++ 201103文件*写入4747 ms文件*读取454 msfstream写入5490 msfstream读取396 ms

大缓冲+缓存:C++ 201103第五运行:文件*写入4760 ms文件*读取446 msfstream写入5278 msfstream读取369 ms

这表明文件写入速度比fstream快,但读取速度比fstream慢。但所有的数字都在10%以内。

尝试向流中添加更多的缓冲,看看这是否有帮助。

1
2
3
const int MySize = 1024*1024;
char MrBuf[MySize];
stream.rdbuf()->pubsetbuf(MrBuf, MySize);

文件的等效值为

1
2
3
const int MySize = 1024*1024;
if (!setvbuf ( file , NULL , _IOFBF , MySize ))
    DieInDisgrace();


对谁感兴趣的旁注。主要关键字是Windows 2016 Server/CloseHandle。

在我们的应用程序中,我们在Win2016服务器上发现了一个严重的错误。

我们在每个Windows版本下的标准代码采用:(ms)

时间createfile/setfilepointer 1 writefile 0 closehandle 0

在Windows 2016上,我们得到:

时间createfile/setfilepointer 1 writefile 0 closehandle 275

时间随着文件的尺寸而增长,这是荒谬的。

经过大量调查(我们首先发现"closehandle"是罪魁祸首…)我们发现,在windows2016下ms在close函数中附加了一个"hook",该函数会触发"windows defender"扫描所有文件并阻止返回,直到完成。(换句话说,扫描是同步的,这是纯粹的疯狂)。

当我们在文件的"defender"中添加排除时,一切都正常。我认为是一个糟糕的设计,没有防病毒停止正常的文件活动在程序空间内扫描文件。(微软可以做到他们有能力做到。)