【问题标题】:How to write a large buffer into a binary file in C++, fast?如何快速将大缓冲区写入 C++ 中的二进制文件?
【发布时间】:2012-07-18 19:43:39
【问题描述】:

我正在尝试将大量数据写入我的 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 倍。这就是为什么我认为我应该能够走得更快。

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

【问题讨论】:

  • 您的计时结果是否排除了计算填充 a[] 所需的时间?
  • 这个任务我其实已经做过了。使用简单的fwrite(),我可以获得大约 80% 的峰值写入速度。只有FILE_FLAG_NO_BUFFERING 才能达到最高速度。
  • 我不确定将您的文件写入与 SSD 到 SSD 的复制进行比较是否公平。 SSD 到 SSD 很可能在较低级别上工作,避免使用 C++ 库或使用直接内存访问 (DMA)。复制某些内容与将任意值写入随机访问文件不同。
  • @IgorF.:这只是错误的猜测;这是一个完全公平的比较(如果没有别的,有利于文件写入)。在 Windows 中跨驱动器复制只是读写;下面没有任何花哨/复杂/不同的事情。
  • @MaximYegorushkin:链接或者它没有发生。 :P

标签: c++ performance optimization file-io io


【解决方案1】:

这完成了工作(在 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、Visual Studio 2017 版本 15.3.1 和 /Ox /Ob2 /Oi /Ot /GT /GL /Gy

它给出了以下测量值(在放弃 1MB 的值之后,因为它们是明显的异常值): 选项 1 和选项 3 都最大限度地利用了我的 SSD。我没想到会出现这种情况,因为当时 option2 曾经是我旧机器上最快的代码。

TL;DR:我的测量结果表明使用std::fstream 而不是FILE

【讨论】:

  • +1 是的,这是我尝试的第一件事。 FILE* 比流更快。我没想到会有这样的差异,因为它“应该”是 I/O 绑定的。
  • 我们能否得出结论,C 风格的 I/O 比 C++ 流(奇怪地)快得多?
  • @SChepurin:如果你是迂腐的,可能不是。如果你是实际的,可能是的。 :)
  • 您能否解释一下(对于像我这样的 C++ 笨蛋)这两种方法之间的区别,以及为什么这种方法比原来的工作得快得多?
  • 前置ios::sync_with_stdio(false); 对带有流的代码有什么影响吗?我只是好奇使用这条线与不使用这条线有多大区别,但我没有足够快的磁盘来检查角落案例。如果有任何真正的区别。
【解决方案2】:

按顺序尝试以下操作:

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

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

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

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

【讨论】:

  • 检查在线发布的任何基准测试结果。您需要队列深度为 32 或更大的 4kB 写入,或者 512K 或更高的写入,以获得任何类型的体面吞吐量。
  • @BenVoigt:是的,这与我说 512 KiB 对我来说是最佳点有关。 :)
  • 是的。根据我的经验,较小的缓冲区大小通常是最佳的。例外情况是当您使用 FILE_FLAG_NO_BUFFERING 时 - 较大的缓冲区往往会更好。因为我认为FILE_FLAG_NO_BUFFERING 几乎是 DMA。
【解决方案3】:

我认为 std::stream/FILE/device 之间没有区别。 在缓冲和非缓冲之间。

另请注意:

  • SSD 驱动器“趋于”在装满时变慢(传输速率降低)。
  • SSD 驱动器“趋于”随着它们变老(由于非工作位)变慢(传输速率降低)。

我看到代码在 63 秒内运行。
因此传输速率为:260M/s(我的 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();
}

【讨论】:

  • 我没有投反对票,但你的缓冲区太小了。我使用 OP 正在使用的相同 512 MB 缓冲区进行此操作,我得到 20 MB/s 的流和 90 MB/s 的 FILE*
  • 你也可以使用 fwrite(a, sizeof(unsigned long long), size, stream);而不是 fwrite(a, 1, size*sizeof(unsigned long long), pFile);给我 220MB/s 的速度,每次写入 64MB 的块。
  • @Mysticial:缓冲区大小的影响让我感到惊讶(尽管我相信你)。当您有大量小写入时,缓冲区很有用,这样底层设备就不会受到许多请求的困扰。但是当你写大块时,写/读时不需要缓冲区(在阻塞设备上)。因此,数据应直接传递到底层设备(从而绕过缓冲区)。虽然如果您看到差异,这将与此相矛盾,并且让我想知道为什么写入实际上完全使用缓冲区。
  • 最好的解决方案是增加缓冲区大小,而是移除缓冲区并让写入将数据直接传递到底层设备。
  • @Mysticial: 1) 没有小块 => 它总是足够大(在这个例子中)。在这种情况下,块是 512M 2)这是一个 SSD 驱动器(我的和 OP 都是),所以这些都不相关。我已经更新了我的答案。
【解决方案4】:

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

看时间线:

------------------------------------------------>
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 函数。

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

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

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

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

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

【讨论】:

  • 执行 I/O 与计算并行是一个非常好的主意,但在 Windows 上您不应该使用线程来完成它。相反,请使用“重叠 I/O”,它不会在 I/O 调用期间阻塞您的线程之一。这意味着您几乎不必担心线程同步(只是不要访问具有活动 I/O 操作的缓冲区)。
【解决方案5】:

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

【讨论】:

  • @nalply 记住它仍然是一个有效、高效且有趣的解决方案。
  • stackoverflow.com/a/2895799/220060 关于 mmap 的优缺点。特别注意“对于文件的纯顺序访问,它也不总是更好的解决方案 [...]”另外stackoverflow.com/questions/726471,它实际上表示在 32 位系统上,您被限制为 2 或 3 GB。 - 顺便说一句,不是我否决了这个答案。
【解决方案6】:

您能否改用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 中做到的。

【讨论】:

  • 实际上,使用相同 512 MB 缓冲区的原始代码的直接FILE* 等价物可以获得全速。您当前的缓冲区太小。
  • @Mysticial 但这只是一个例子。
  • 在大多数系统中,2 对应于标准错误,但仍然建议您使用 STDERR_FILENO 而不是 2。另一个重要的问题是,当您收到中断信号时,您可能会遇到的一个错误是 EINTR,这不是真正的错误,您应该再试一次。
【解决方案7】:

fstreams 本身并不比 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 版本慢。

【讨论】:

    【解决方案8】:

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

    另一个尝试 - 使用 WinAPI OpenFile/CreateFile 并使用 this MSDN article 关闭缓冲 (FILE_FLAG_NO_BUFFERING)。 this MSDN article on WriteFile() 展示了如何获取驱动器的块大小以了解最佳缓冲区大小。

    无论如何,std::ofstream 是一个包装器,可能会阻塞 I/O 操作。请记住,遍历整个 N 千兆字节数组也需要一些时间。当您写入一个小缓冲区时,它会进入缓存并且工作得更快。

    【讨论】:

      【解决方案9】:

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

      所做的事情对 CPU 的影响很大。 我想向您指出“填充 [] 的一些计算”部分。我认为这是必不可少的。您生成 a[],然后从 a[] 复制到输出缓冲区(这就是 fstream::write 所做的),然后再次生成,等等。

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

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

      【讨论】:

        【解决方案10】:

        尝试使用内存映射文件。

        【讨论】:

        • @Mehrdad 但为什么呢?因为它是一个依赖于平台的解决方案?
        • 不...这是因为为了进行快速的顺序文件写入,您需要一次写入大量数据。 (比如说,2-MiB 的块可能是一个很好的起点。)内存映射文件不允许您控制粒度,因此您受内存管理器决定为您预取/缓冲的任何东西的摆布。总的来说,我从来没有见过它们像使用ReadFile 和顺序访问这样的正常读/写一样有效,尽管对于随机访问它们可能会更好。
        • 但是内存映射文件被操作系统用于分页,例如。我认为这是一种高度优化的(在速度方面)读/写数据的方式。
        • @Mysticial:人们“知道”很多完全错误的事情。
        • @qehgt:如果有的话,分页比顺序访问更适合随机访问。读取 1 页数据比在单个操作中读取 1 MB 数据慢得多
        【解决方案11】:

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

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

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

        【讨论】:

          【解决方案12】:

          我在 GNU/Linuxmingw 中的 gcc 中编译我的程序,在 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

          【讨论】:

            猜你喜欢
            • 2012-10-03
            • 1970-01-01
            • 2019-08-17
            • 2011-07-05
            • 1970-01-01
            • 2014-11-21
            • 1970-01-01
            • 1970-01-01
            • 2015-08-20
            相关资源
            最近更新 更多