如何写一个大缓冲区到一个二进制文件在c++,快速?

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

我在网上寻找解决方案,但我想到的最好的办法是:

#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倍。这就是为什么我认为我应该能跑得更快。

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

245840 次浏览

尝试使用open()/write()/close() API调用并试验输出缓冲区的大小。我的意思是不要一次传递整个“多-多-字节”缓冲区,做几次写入(即TotalNumBytes / OutBufferSize)。OutBufferSize可以从4096字节到兆字节。

另一个尝试——使用WinAPI OpenFile/CreateFile并使用这篇MSDN文章来关闭缓冲(FILE_FLAG_NO_BUFFERING)。而这篇关于WriteFile()的MSDN文章显示了如何获取驱动器的块大小以了解最佳缓冲区大小。

不管怎样,std::ofstream是一个包装器,可能会阻塞I/O操作。请记住,遍历整个n gb数组也需要一些时间。当您写入一个小缓冲区时,它会更快地到达缓存并工作。

尝试使用内存映射文件。

你可以使用FILE*代替,并衡量你所获得的性能吗? 有几个选项是使用fwrite/write而不是fstream:

#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,试试类似的方法:

#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\n", 36) != 36) {
write(2, "There was an error writing to testfile.txt\n", 43);
return -1;
}


return 0;
}

我还建议你看看memory map。这可能就是你的答案。有一次我不得不处理一个20GB的文件,把它存储在数据库中,而这个文件甚至没有打开。因此,解决方案是利用内存映射。我在Python中做过。

按顺序尝试以下方法:

  • 较小的缓冲区大小。一次写~ 2mib可能是一个好的开始。在我的上一台笔记本电脑上,大约512 KiB是最好的,但我还没有在我的SSD上测试过。

    我注意到非常大的缓冲区倾向于减少性能。我注意到以前使用16-MiB缓冲区而不是512-KiB缓冲区时速度下降

  • 使用_open(或_topen如果你想要正确的windows)来打开文件,然后使用_write。这将可能避免大量缓冲,但这并不一定。

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

我看到std::stream/FILE/device之间没有区别。

还要注意:

  • SSD驱动器"tend"在填充时减慢(较低的传输速率)。
  • SSD驱动器"tend"当它们变老时(因为没有工作位)减慢(较低的传输速率)。

我看到代码在63秒内运行。
因此传输速率为:260米/秒(我的SSD看起来比你的快一点)

64 * 1024 * 1024 * 8 /*sizeof(unsigned long long) */ * 32 /*Chunks*/


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

我从std::fstream移动到FILE*没有增加。

#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从内存的一个位置复制到另一个位置,看看有多快。

编辑

我改变了我的代码,以使用较低级别的调用:

#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++复制文件的最快方法吗

int main()
{
std::ifstream  input("input");
std::ofstream  output("ouptut");


output << input.rdbuf();
}

这是成功的(在2012年):

#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年过去了,现在是2017年。编译器、硬件、库和我的需求都发生了变化。这就是为什么我对代码做了一些更改,并做了一些新的测量。

首先是代码:

#include <fstream>
#include <chrono>
#include <vector>
#include <cstdint>
#include <numeric>
#include <random>
#include <algorithm>
#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(新需求)编译。 我用两个设置运行代码:

  • 笔记本电脑,Core i7, SSD, Ubuntu 16.04, g++ Version 7.2.0带有-std=c++11 -march=native -O3
  • 桌面,Core i7, SSD, Windows 10, Visual Studio 2017 Version 15.3.1,带有/Ox /Ob2 /Oi /Ot /GT /GL /Gy
它给出了以下测量值(在抛弃1MB的值后,因为它们是明显的异常值): enter image description here enter image description here 两次option1和option3都将耗尽我的SSD。我不希望看到这个,因为option2曾经是我的旧机器上最快的代码

博士TL;:我的测量表明使用std::fstream而不是FILE

如果你在资源管理器中将一些东西从磁盘A复制到磁盘B, Windows使用DMA。这意味着对于大多数的复制过程,CPU基本上什么也不做,只是告诉磁盘控制器在哪里放置数据,并从哪里获取数据,从而消除了整个链中的一个步骤,而且这个步骤根本不适合移动大量数据(我指的是硬件)。

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

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

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

我建议试试文件映射。我过去在UNIX环境中使用__abc0,我对我可以实现的高性能印象深刻

我在gcc编译我的程序在GNU / Linuxmingw在win7和winxp和工作良好

你可以用我的程序创建一个80gb的文件,只需要把第33行改为

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

当退出程序时,文件将被销毁,然后检查文件时,它正在运行

要得到你想要的程序,只需改变程序

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

http://mustafajf.persiangig.com/Projects/File/WinFile.cpp

http://mustafajf.persiangig.com/Projects/File/File.cpp

如果你想快速写入文件流,那么你可以让stream读缓冲区更大:

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

同样,当向文件写入大量数据时,在逻辑上扩展文件大小有时比物理扩展文件大小更快,这是因为当逻辑扩展一个文件时,文件系统在写入之前不会将新空间归零。明智的做法是在逻辑上对文件进行比实际需要更多的扩展,以防止大量的文件扩展。逻辑文件扩展名在Windows上通过在XFS系统上调用SetFileValidDataxfsctlXFS_IOC_RESVSP64来支持。

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

看看时间轴:

------------------------------------------------>
FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|

“F”表示填充缓冲区的时间,“W”表示将缓冲区写入磁盘的时间。所以问题是在写缓冲区到文件之间浪费时间。然而,通过在一个单独的线程上实现写入,你可以像这样立即开始填充下一个缓冲区:

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

F -填充第一个缓冲区
F -填充第2缓冲区
写入第一个缓冲区到文件
写入第二个缓冲区到文件
_ - wait while operation completed

.操作结束 当填充缓冲区需要更复杂的计算(因此需要更多时间)时,这种使用缓冲区交换的方法非常有用。 我总是实现一个CSequentialStreamWriter类,它隐藏了异步写入,所以对于最终用户来说,接口只有写入函数

缓冲区大小必须是磁盘集群大小的倍数。否则,通过将一个缓冲区写入两个相邻的磁盘集群,您将最终获得较差的性能。

正在写入最后一个缓冲区 当您最后一次调用Write函数时,必须确保当前正在被填充的缓冲区也应该写入磁盘。因此CSequentialStreamWriter应该有一个单独的方法,比如Finalize(最后的缓冲区刷新),它应该把最后一部分数据写入磁盘 < br > < p >错误处理。 当代码开始填充第二个缓冲区时,第一个缓冲区正在另一个线程上写入,但由于某种原因写入失败了,主线程应该知道这个失败

------------------------------------------------> (main thread, fills buffers)
FF|fX|
------------------------------------------------> (writer thread)
__|X|

让我们假设CSequentialStreamWriter的接口有Write函数返回bool值或抛出异常,因此在一个单独的线程上有一个错误,你必须记住那个状态,所以下次你在主线程上调用Write或finalize时,该方法将返回False或抛出异常。在什么时候停止填充缓冲区并不重要,即使在失败后提前写入了一些数据,文件很可能会损坏并且无用。

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

至少MSVC 2015实现在流缓冲区未设置时将每次1个字符复制到输出缓冲区(参见streambuf::xsputn)。所以确保设置一个流缓冲区(>0)

我可以用这个代码用fstream获得1500MB/s的写速度(我的M.2 SSD的全速):

#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\n";
ofstream myfile("file.binary", ios::out | ios::binary);
if (!myfile) {
cerr << "open failed\n"; 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\n";
auto tm = (duration_cast<milliseconds>(high_resolution_clock::now() - tm1).count());
cout << tm << " ms\n";
total += tm;
}
myfile.close();
}
else {
cout << "fopen mode\n";
FILE* pFile = fopen("file.binary", "wb");
if (!pFile) {
cerr << "open failed\n"; 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\n";
auto tm = (duration_cast<milliseconds>(high_resolution_clock::now() - tm1).count());
cout << tm << " ms\n";
total += tm;
}
fclose(pFile);
auto tm2 = high_resolution_clock::now();
}
cout << "Total: " << total << " ms, " << (sz*numiter * 1000 / (1024.0 * 1024 * total)) << " MB/s\n";
}

我在其他平台(Ubuntu, FreeBSD)上尝试了这段代码,并注意到没有I/O率差异,但CPU使用率的差异约为8:1 (fstream使用8倍的CPU)。因此,可以想象,如果我有一个更快的磁盘,fstream写将比stdio版本更快地慢下来。