关于性能:非常快速地用C ++编写二进制文件

Writing a binary file in C++ very fast

我正在尝试将大量数据写入我的SSD(固态硬盘)。我指的是大量的80GB。

我浏览网页寻找解决方案,但我想到的最好办法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <fstream>
const unsigned long long size = 64ULL*1024ULL*1024ULL;
unsigned long long a[size];
int main()
{
    std::fstream myfile;
    myfile = std::fstream("file.binary", std::ios::out | std::ios::binary);
    //Here would be some error handling
    for(int i = 0; i < 32; ++i){
        //Some calculations to fill a[]
        myfile.write((char*)&a,size*sizeof(unsigned long long));
    }
    myfile.close();
}

使用Visual Studio 2010和完全优化进行编译,并在Windows7下运行,该程序最大速度约为20MB/s。真正让我困扰的是,Windows可以以150MB/s到200MB/s的速度将文件从其他SSD复制到该SSD。因此,速度至少快7倍。这就是为什么我认为我应该能走得更快。

有什么办法可以加快我的写作速度吗?


这样做的目的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
const unsigned long long size = 8ULL*1024ULL*1024ULL;
unsigned long long a[size];

int main()
{
    FILE* pFile;
    pFile = fopen("file.binary","wb");
    for (unsigned long long j = 0; j < 1024; ++j){
        //Some calculations to fill a[]
        fwrite(a, 1, size*sizeof(unsigned long long), pFile);
    }
    fclose(pFile);
    return 0;
}

我刚刚在36秒内完成了8GB的计时,大约是220MB/s,我想这会使我的SSD最大化。同样值得注意的是,问题中的代码使用一个核心100%,而此代码只使用2-5%。

非常感谢大家。

更新:5年过去了。编译器、硬件、库和我的需求都发生了变化。这就是为什么我对代码做了一些修改并做了一些测量。

首先是代码:

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
#include <fstream>
#include <chrono>
#include <vector>
#include <cstdint>
#include <numeric>
#include <random>
#include
#include <iostream>
#include <cassert>

std::vector<uint64_t> GenerateData(std::size_t bytes)
{
    assert(bytes % sizeof(uint64_t) == 0);
    std::vector<uint64_t> data(bytes / sizeof(uint64_t));
    std::iota(data.begin(), data.end(), 0);
    std::shuffle(data.begin(), data.end(), std::mt19937{ std::random_device{}() });
    return data;
}

long long option_1(std::size_t bytes)
{
    std::vector<uint64_t> data = GenerateData(bytes);

    auto startTime = std::chrono::high_resolution_clock::now();
    auto myfile = std::fstream("file.binary", std::ios::out | std::ios::binary);
    myfile.write((char*)&data[0], bytes);
    myfile.close();
    auto endTime = std::chrono::high_resolution_clock::now();

    return std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
}

long long option_2(std::size_t bytes)
{
    std::vector<uint64_t> data = GenerateData(bytes);

    auto startTime = std::chrono::high_resolution_clock::now();
    FILE* file = fopen("file.binary","wb");
    fwrite(&data[0], 1, bytes, file);
    fclose(file);
    auto endTime = std::chrono::high_resolution_clock::now();

    return std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
}

long long option_3(std::size_t bytes)
{
    std::vector<uint64_t> data = GenerateData(bytes);

    std::ios_base::sync_with_stdio(false);
    auto startTime = std::chrono::high_resolution_clock::now();
    auto myfile = std::fstream("file.binary", std::ios::out | std::ios::binary);
    myfile.write((char*)&data[0], bytes);
    myfile.close();
    auto endTime = std::chrono::high_resolution_clock::now();

    return std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
}

int main()
{
    const std::size_t kB = 1024;
    const std::size_t MB = 1024 * kB;
    const std::size_t GB = 1024 * MB;

    for (std::size_t size = 1 * MB; size <= 4 * GB; size *= 2) std::cout <<"option1," << size / MB <<"MB:" << option_1(size) <<"ms" << std::endl;
    for (std::size_t size = 1 * MB; size <= 4 * GB; size *= 2) std::cout <<"option2," << size / MB <<"MB:" << option_2(size) <<"ms" << std::endl;
    for (std::size_t size = 1 * MB; size <= 4 * GB; size *= 2) std::cout <<"option3," << size / MB <<"MB:" << option_3(size) <<"ms" << std::endl;

    return 0;
}

现在,代码与Visual Studio 2017和G++7.2.0(现在是我的要求之一)一起编译。我使用两种设置运行代码:

  • 笔记本电脑,核心I7,SSD,Ubuntu 16.04,G+版本7.2.0,-STD=C++ 11 -三月=原生-O3
  • 桌面、Core i7、SSD、Windows 10、Visual Studio 2017版本15.3.1和/ox/ob2/oi/ot/gt/gl/gy

它给出了以下测量值(在丢弃1MB的值后,因为它们是明显的异常值):enter image description hereenter image description here选项1和选项3都是我的SSD的最大输出。我没想到会出现这种情况,因为选项2曾经是我机器上最快的代码。

tl;dr:我的测量结果表明在FILE上使用std::fstream


按顺序尝试以下操作:

  • 较小的缓冲区大小。一次写2兆字节可能是个好的开始。在我上一台笔记本电脑上,~512 kib是最佳选择,但我还没有在固态硬盘上测试过。

    注意:我注意到非常大的缓冲区会降低性能。我注意到以前使用16 mib缓冲区而不是512 kib缓冲区会造成速度损失。

  • 使用_open_topen打开文件,然后使用_write。这可能会避免大量的缓冲,但不一定。

  • 使用特定于Windows的函数,如CreateFileWriteFile。这将避免标准库中的任何缓冲。


我看不到std::stream/file/device之间的区别。在缓冲和非缓冲之间。

还要注意:

  • SSD驱动器在充满时"趋向"减慢(传输速率较低)。
  • 当SSD驱动器变老(由于非工作位)时,它们"趋向"减慢(传输速率较低)。

我看到代码在63秒内运行。因此传输速率为:260m/s(我的SSD看起来比您的稍快)。

1
2
3
4
64 * 1024 * 1024 * 8 /*sizeof(unsigned long long) */ * 32 /*Chunks*/

= 16G
= 16G/63 = 260M/s

从std::fstream移到file*不会增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

using namespace std;

int main()
{

    FILE* stream = fopen("binary","w");

    for(int loop=0;loop < 32;++loop)
    {
         fwrite(a, sizeof(unsigned long long), size, stream);
    }
    fclose(stream);

}

因此,C++流的工作速度与底层库所允许的速度一样快。

但我认为将操作系统与构建在操作系统之上的应用程序进行比较是不公平的。应用程序不能做任何假设(它不知道驱动器是SSD),因此使用操作系统的文件机制进行传输。

而操作系统不需要做任何假设。它可以区分所涉及的驱动器类型,并使用最佳技术传输数据。在这种情况下,直接内存到内存的传输。尝试编写一个程序,将80G从内存中的一个位置复制到另一个位置,看看速度有多快。

编辑

我更改了代码以使用低级调用:没有缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <fcntl.h>
#include <unistd.h>


const unsigned long long size = 64ULL*1024ULL*1024ULL;
unsigned long long a[size];
int main()
{
    int data = open("test", O_WRONLY | O_CREAT, 0777);
    for(int loop = 0; loop < 32; ++loop)
    {  
        write(data, a, size * sizeof(unsigned long long));
    }  
    close(data);
}

这并没有什么不同。

注意:我的驱动器是一个SSD驱动器如果您有一个正常的驱动器,您可能会看到上面两种技术之间的差异。但正如我预期的那样,非缓冲和缓冲(当写入大于缓冲区大小的大块时)没有什么区别。

编辑2:

你尝试过最快的C++文件复制方法吗?

1
2
3
4
5
6
7
int main()
{
    std::ifstream  input("input");
    std::ofstream  output("ouptut");

    output << input.rdbuf();
}


最好的解决方案是使用双缓冲实现异步写入。

看看时间线:

1
2
------------------------------------------------>
FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|

"f"表示缓冲区填充时间,"w"表示将缓冲区写入磁盘的时间。因此,在将缓冲区写入文件之间浪费时间的问题。但是,通过在单独的线程上实现写操作,可以立即开始填充下一个缓冲区,如下所示:

1
2
3
4
------------------------------------------------> (main thread, fills buffers)
FF|ff______|FF______|ff______|________|
------------------------------------------------> (writer thread)
  |WWWWWWWW|wwwwwwww|WWWWWWWW|wwwwwwww|

F-填充第一个缓冲区
F-填充第二个缓冲区
w-将第一个缓冲区写入文件
w-将第二个缓冲区写入文件
_-操作完成时等待

当填充缓冲区需要更复杂的计算(因此需要更多的时间)时,使用缓冲区交换的这种方法非常有用。我总是实现一个CSSequentialStreamWriter类,它将异步写入隐藏在内部,因此对于最终用户,该接口只有写入函数。

缓冲区大小必须是磁盘群集大小的倍数。否则,您将向两个相邻的磁盘集群写入一个缓冲区,从而导致性能低下。

正在写入最后一个缓冲区。最后一次调用写函数时,必须确保当前正在填充的缓冲区也应该写入磁盘。因此,CSSequentialStreamWriter应该有一个单独的方法,比如Finalize(最终缓冲区刷新),它应该将数据的最后一部分写入磁盘。

错误处理。当代码开始填充第二个缓冲区时,第一个缓冲区被写到一个单独的线程上,但是由于某种原因写失败了,主线程应该意识到这个失败。

1
2
3
4
------------------------------------------------> (main thread, fills buffers)
FF|fX|
------------------------------------------------> (writer thread)
__|X|

假设CSSequentialStreamWriter的接口具有write函数,返回bool或引发异常,因此在单独的线程上有错误,您必须记住该状态,因此下次在主线程上调用write或finize时,该方法将返回false或引发异常。在什么时候停止填充缓冲区并不重要,即使在失败之后提前写了一些数据——很有可能文件会被损坏并且无用。


我建议尝试文件映射。我以前在Unix环境中使用过mmap,我对自己能够实现的高性能印象深刻。


你能用FILE*来代替,并衡量你所取得的成绩吗?有两种选择是使用fwrite/write而不是fstream

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main ()
{
  FILE * pFile;
  char buffer[] = { 'x' , 'y' , 'z' };
  pFile = fopen ("myfile.bin" ,"w+b" );
  fwrite (buffer , 1 , sizeof(buffer) , pFile );
  fclose (pFile);
  return 0;
}

如果您决定使用write,请尝试类似的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
    int filedesc = open("testfile.txt", O_WRONLY | O_APPEND);

    if (filedesc < 0) {
        return -1;
    }

    if (write(filedesc,"This will be output to testfile.txt
"
, 36) != 36) {
        write(2,"There was an error writing to testfile.txt
"
, 43);
        return -1;
    }

    return 0;
}

我也建议你调查一下memory map。那可能是你的答案。有一次,我不得不在另一个数据库中处理一个20GB的文件,而这个文件甚至没有打开。因此,利用moemory图的解决方案。不过,我是在江户城做的。


尝试使用open()/write()/close()API调用并尝试输出缓冲区大小。我的意思是不要一次传递整个"许多字节"缓冲区,进行几次写入(即totalNumBytes/outBufferSize)。OutBufferSize可以是4096字节到兆字节。

另一个尝试-使用winapi openfile/createfile并使用此msdn文章关闭缓冲(file_flag_no_buffering)。这个关于writefile()的msdn文章展示了如何获取块大小,以便让驱动器知道最佳缓冲区大小。

总之,std::ofstream是一个包装器,可能会阻塞I/O操作。记住,遍历整个n-gigabyte数组也需要一些时间。当您在写一个小的缓冲区时,它会到达缓存并工作得更快。


fstreams本身并不比C流慢,但它们使用更多的CPU(特别是在缓冲配置不正确的情况下)。当CPU饱和时,它会限制I/O速率。

当没有设置流缓冲时,至少MSVC 2015实现一次将1个字符复制到输出缓冲区(参见streambuf::xsputn)。因此,请确保设置流缓冲区(>0)。

使用此代码,我可以获得1500 MB/s的写入速度(我的M.2 SSD的全速),使用fstream

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
#include <iostream>
#include <fstream>
#include <chrono>
#include <memory>
#include <stdio.h>
#ifdef __linux__
#include <unistd.h>
#endif
using namespace std;
using namespace std::chrono;
const size_t sz = 512 * 1024 * 1024;
const int numiter = 20;
const size_t bufsize = 1024 * 1024;
int main(int argc, char**argv)
{
  unique_ptr<char[]> data(new char[sz]);
  unique_ptr<char[]> buf(new char[bufsize]);
  for (size_t p = 0; p < sz; p += 16) {
    memcpy(&data[p],"BINARY.DATA.....", 16);
  }
  unlink("file.binary");
  int64_t total = 0;
  if (argc < 2 || strcmp(argv[1],"fopen") != 0) {
    cout <<"fstream mode
"
;
    ofstream myfile("file.binary", ios::out | ios::binary);
    if (!myfile) {
      cerr <<"open failed
"
; return 1;
    }
    myfile.rdbuf()->pubsetbuf(buf.get(), bufsize); // IMPORTANT
    for (int i = 0; i < numiter; ++i) {
      auto tm1 = high_resolution_clock::now();
      myfile.write(data.get(), sz);
      if (!myfile)
        cerr <<"write failed
"
;
      auto tm = (duration_cast<milliseconds>(high_resolution_clock::now() - tm1).count());
      cout << tm <<" ms
"
;
      total += tm;
    }
    myfile.close();
  }
  else {
    cout <<"fopen mode
"
;
    FILE* pFile = fopen("file.binary","wb");
    if (!pFile) {
      cerr <<"open failed
"
; return 1;
    }
    setvbuf(pFile, buf.get(), _IOFBF, bufsize); // NOT important
    auto tm1 = high_resolution_clock::now();
    for (int i = 0; i < numiter; ++i) {
      auto tm1 = high_resolution_clock::now();
      if (fwrite(data.get(), sz, 1, pFile) != 1)
        cerr <<"write failed
"
;
      auto tm = (duration_cast<milliseconds>(high_resolution_clock::now() - tm1).count());
      cout << tm <<" ms
"
;
      total += tm;
    }
    fclose(pFile);
    auto tm2 = high_resolution_clock::now();
  }
  cout <<"Total:" << total <<" ms," << (sz*numiter * 1000 / (1024.0 * 1024 * total)) <<" MB/s
"
;
}

我在其他平台(Ubuntu、FreeBsd)上尝试了这段代码,没有发现I/O速率的差异,但是CPU使用率的差异约为8:1(fstream使用了8倍的CPU)。所以可以想象,如果我有一个更快的磁盘,fstream的写入速度会比stdio版本更快。


如果在资源管理器中将某些内容从磁盘A复制到磁盘B,Windows将使用DMA。这意味着在大多数复制过程中,CPU除了告诉磁盘控制器放在哪里,从哪里获取数据之外,基本上什么也不做,消除了链中的整个步骤,以及一个根本不适合移动大量数据的步骤——我的意思是硬件。

你所做的工作涉及CPU很多。我想给你指出"一些计算来填充一个[]部分"。我认为这是必要的。生成一个[],然后从一个[]复制到一个输出缓冲区(这就是fstream::write所做的),然后再次生成,等等。

怎么办?多线程!(我希望你有一个多核处理器)

  • 叉子。
  • 使用一个线程生成[]数据
  • 另一个用于将数据从[]写入磁盘
  • 您需要两个数组a1[]和a2[]并在它们之间切换
  • 您将需要在线程之间进行某种同步(信号量、消息队列等)。
  • 使用低级的、无缓冲的函数,如mehrdad提到的writefile函数

尝试使用内存映射文件。


如果要快速写入文件流,则可以使流的读取缓冲区变大:

1
2
3
4
wfstream f;
const size_t nBufferSize = 16184;
wchar_t buffer[nBufferSize];
f.rdbuf()->pubsetbuf(buffer, nBufferSize);

此外,在向文件写入大量数据时,逻辑扩展文件大小而不是物理扩展文件大小有时会更快,这是因为在逻辑扩展文件时,文件系统在写入文件之前不会将新的空间清空。在逻辑上扩展文件比防止大量文件扩展实际需要的还要多也是明智的。通过在XFS系统上使用XFS_IOC_RESVSP64调用SetFileValidDataxfsctl在Windows上支持逻辑文件扩展。


我在gcc中用gnu/linux编译程序,在win 7和win xp中用mingw编译程序,效果很好

您可以使用我的程序创建一个80 GB的文件,只需将第33行更改为

1
makeFile("Text.txt",1024,8192000);

退出程序时,文件将被销毁,然后在文件运行时进行检查。

有你想要的程序就改变程序

第一个是Windows程序,第二个是GNU/Linux程序

http://mustafajf.persiangig.com/projects/file/winfile.cpp

http://mustafajf.persiangig.com/projects/file/file.cpp