用C ++快速编写二进制文件


240

我试图将大量数据写入我的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可以将其他SSD上的文件复制到此SSD的速度介于150MB / s和200MB / s之间。因此至少快7倍。这就是为什么我认为我应该能够走得更快。

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


11
您的计时结果是否排除了填充a []所需的时间?
catchmeifyoutry 2012年

7
我实际上已经完成了此任务。使用简单,fwrite()我可以获得大约80%的峰值写入速度。只有有了FILE_FLAG_NO_BUFFERING我,我才能获得最大速度。
Mysticial

10
我不确定将您的文件写入与SSD到SSD复制进行比较是否公平。可能是SSD到SSD的工作水平较低,避免使用C ++库,或者使用直接内存访问(DMA)。复制某些内容与将任意值写入随机访问文件不同。
伊戈尔·F

4
@IgorF .:那是错误的猜测;这是一个完全公平的比较(如果没有别的,则支持文件写入)。在Windows中跨驱动器复制只是读写操作。下面没有什么幻想/复杂/不同的事情。
user541686

5
@MaximYegorushkin:链接还是没有发生。:P
user541686

Answers:


231

做到了这一点(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 ++版本7.2.0,带有-std = c ++ 11 -march = native -O3
  • 台式机,Core i7,SSD,Windows 10,带有/ Ox / Ob2 / Oi / Ot / GT / GL / Gy的Visual Studio 2017版本15.3.1

进行了以下测量(在 删除1MB的值之后,因为它们是明显的离群值): option1和option3都使我的SSD最大化。我没想到会看到这种情况,因为option2曾经是当时旧计算机上最快的代码。在此处输入图片说明 在此处输入图片说明

TL; DR:我的测量表明使用std::fstreamFILE


8
+1是的,这是我尝试的第一件事。FILE*比溪流快 我不会期望有如此大的差异,因为无论如何它都应该绑定I / O。
Mysticial

12
我们是否可以得出结论,C风格的I / O(奇怪地)比C ++流快得多?
SChepurin 2012年

21
@SChepurin:如果您正在做修脚,可能不是。如果您很实用,可能是的。:)
user541686

10
您能否解释一下(对于像我这样的C ++笨蛋)这两种方法之间的区别,为什么这种方法比原始方法快得多?
Mike Chamberlain 2012年

11
前置ios::sync_with_stdio(false);对流代码有什么影响吗?我只是很好奇这条线与不使用这条线之间有很大的区别,但是我没有足够快的磁盘来检查极端情况。如果有任何真正的区别。
Artur Czajka,2012年

24

按顺序尝试以下操作:

  • 较小的缓冲区大小。一次写入〜2 MiB可能是一个好的开始。在我的最后一台笔记本电脑上,〜512 KiB是最佳选择,但我尚未在SSD上进行测试。

    注意:我注意到很大的缓冲区会降低性能。我注意到以前使用16-MiB缓冲区而不是512-KiB缓冲区会造成速度损失。

  • 使用_open(或_topen如果您希望Windows正确使用)打开文件,然后使用_write。这可能会避免很多缓冲,但是并不确定。

  • 使用Windows特定的功能,例如CreateFileWriteFile。这样可以避免在标准库中进行任何缓冲。


查看在线发布的所有基准测试结果。您需要队列深度为32或更大的4kB写入,或者需要512K或更高的写入,才能获得各种体面的吞吐量。
Ben Voigt 2012年

@BenVoigt:是的,这与我说512 KiB是我的最佳选择有关。:)
user541686

是。根据我的经验,较小的缓冲区大小通常是最佳的。例外是在使用时FILE_FLAG_NO_BUFFERING-较大的缓冲区往往会更好。因为我认为FILE_FLAG_NO_BUFFERING是DMA。
Mysticial

22

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

另请注意:

  • SSD驱动器填满时会“趋向于”变慢(降低传输速率)。
  • SSD驱动器趋于老化(由于无法工作的位)时,它们“趋向于”减慢速度(降低传输速率)。

我看到代码在63秒内运行。
因此,传输速率为:260M / s(我的SSD看上去比您的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),因此使用OS的文件机制进行传输。

虽然操作系统不需要做任何假设。它可以分辨出所涉及的驱动器类型,并使用最佳技术来传输数据。在这种情况下,直接进行内存到内存的传输。尝试编写一个程序,将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();
}

5
我没有投票,但是您的缓冲区大小太小。我使用OP正在使用的相同512 MB缓冲区完成此操作,使用流获得20 MB / s的速度,而使用获得90 MB / s FILE*
Mysticial

也可以使用fwrite(a,sizeof(unsigned long long),size,stream); 而不是fwrite(a,1,size * sizeof(unsigned long long),pFile); 给我220MB / s,每次写入有64MB的块。
Dominic Hofer

2
@Mysticial:令我惊讶的是缓冲区大小有所不同(尽管我相信您)。当您进行大量小写操作时,缓冲区非常有用,这样底层设备就不会被很多请求所困扰。但是,当您编写大量块时,(在阻塞设备上)进行读/写操作时不需要缓冲区。因此,数据应直接传递到基础设备(因此绕过缓冲区)。尽管如果您看到差异,这将与此矛盾并令我感到奇怪,为什么写入实际上实际上使用了缓冲区。
马丁·约克

2
最好的解决方案不是增加缓冲区大小,而是删除缓冲区并使写入直接将数据传递到基础设备。
马丁·约克

1
@Mysticial:1)没有小块=>它总是足够大(在此示例中)。在这种情况下,块为512M 2)这是SSD驱动器(我的和OP),因此都没有关系。我已经更新了答案。
马丁·约克

13

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

看一下时间线:

------------------------------------------------>
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-填充第二缓冲区
W-将第一缓冲区写入文件
w-将第二缓冲区写入文件
_-等待操作完成

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

并且缓冲区大小必须是磁盘群集大小的倍数。否则,将单个缓冲区写入两个相邻的磁盘群集将导致性能下降。

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

错误处理。
虽然代码开始填充第二个缓冲区,并且第一个缓冲区正在单独的线程上写入,但是由于某种原因写入失败,但主线程应意识到该失败。

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

假设CSequentialStreamWriter的接口具有Write函数返回bool或引发异常,因此在单独的线程上发生错误,则必须记住该状态,因此,下次在主线程上调用Write或Finilize时,该方法将返回错误或将引发异常。即使您在故障发生后提前写了一些数据,在哪一点停止填充缓冲区也并不重要,这很可能是文件已损坏且无用。


3
执行I / O与计算并行是一个好主意,但是在Windows上,您不应该使用线程来完成它。而是使用“重叠的I / O”,它在I / O调用期间不会阻塞您的线程之一。这意味着您几乎不必担心线程同步(只是不必访问使用了活动I / O操作的缓冲区)。
Ben Voigt,2015年

11

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


1
@nalply请记住,这仍然是一个有效,有效且有趣的解决方案。
Yam Marcovic 2012年

关于mmap的利弊的stackoverflow.com/a/2895799/220060。尤其要注意“对于纯顺序访问文件,它也不总是总是更好的解决方案。”另外stackoverflow.com/questions/726471,它有效地表明,在32位系统上,您被限制为2或... 3 GB。-顺便说一下,不是我否决了这个答案。
2012年

8

您可以使用它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


实际上,FILE*使用相同的512 MB缓冲区直接等效于原始代码将获得全速。您当前的缓冲区太小。
Mysticial

1
@Mysticial但这只是一个例子。
cybertextron 2012年

在大多数系统中,这2对应于标准错误,但仍建议您使用STDERR_FILENO而不是2。另一个重要的问题是,当收到中断信号时,您可能会遇到的一个可能的错误是EINTR,这不是真正的错误,您只需重试即可。
Peyman

6

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

另一种尝试-使用WinAPI OpenFile / CreateFile并使用此MSDN文章来关闭缓冲(FILE_FLAG_NO_BUFFERING)。而在这个WriteFile的MSDN文章()演示了如何获取的块大小的驱动器知道最佳的缓冲区大小。

无论如何,std :: ofstream是包装器,并且I / O操作上可能存在阻塞。请记住,遍历整个N GB阵列也需要一些时间。当您编写一个小的缓冲区时,它会进入高速缓存并更快地工作。


5

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

当未设置流缓冲区时,至少MSVC 2015实现一次1个char复制到输出缓冲区(请参阅参考资料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使用的CPU数量多8倍)。因此,可以想象,如果磁盘速度更快,fstream写入速度将比stdio版本更快。


3

尝试使用内存映射文件。


@Mehrdad,但是为什么呢?因为这是依赖平台的解决方案?
qehgt 2012年

3
不,因为要进行快速的顺序文件写入,您需要一次写入大量数据。(例如,2-MiB块可能是一个不错的起点。)内存映射文件不允许您控制粒度,因此无论内存管理器决定为您预取/缓冲任何内容,您都将受到摆布。总的来说,我从来没有见过它们能像ReadFile按顺序访问这样的普通读/写一样有效,尽管对于随机访问它们可能会更好。
user541686

但是,例如,操作系统将内存映射文件用于分页。我认为这是一种高度优化的(就速度而言)读取/写入数据的方式。
qehgt 2012年

7
@Mysticial:人们“知道”很多完全是错误的事情
Ben Voigt 2012年

1
@qehgt:如果有的话,对于随机访问,分页比顺序访问要优化得多。读取1页数据比单次操作读取1 MB数据要慢得多。
user541686

3

如果在资源管理器中将某些内容从磁盘A复制到磁盘B,则Windows将使用DMA。这意味着在大多数复制过程中,CPU基本上不会做任何事情,只是告诉磁盘控制器从何处放置数据以及从中获取数据,从而消除了整个链中的整个步骤,并且该步骤根本没有针对大量移动进行优化数据-我的意思是硬件。

什么做涉及到CPU很多。我想将您指向“一些计算以填充[[]”部分。我认为这是必不可少的。生成a [],然后从a []复制到输出缓冲区(这就是fstream :: write的作用),然后再次生成,依此类推。

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

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

1

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

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

另外,在向文件中写入大量数据时,有时逻辑上扩展文件大小要比物理扩展快一些,这是因为逻辑上扩展文件时,文件系统在写入文件之前不会将新空间归零。在逻辑上扩展文件的数量超过防止大量文件扩展的实际所需,这也是很明智的。逻辑文件扩展是通过调用支持在Windows SetFileValidDataxfsctlXFS_IOC_RESVSP64上XFS系统。


0

即时编译我的程序在海湾合作委员会的GNU / LinuxMinGW的在Win 7和Win XP和良好的工作

您可以使用我的程序并创建80 GB文件,只需将第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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.