用C ++编写二进制文件的速度非常快

时间:2012-07-19 15:18:23

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

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

任何想法如何加快我的写作速度?

12 个答案:

答案 0 :(得分:192)

这完成了这项工作:

#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年过去了。编译器,硬件,库和我的要求已经改变。这就是为什么我对代码进行了一些更改并进行了一些测量。

首先是代码:

#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(现在是我的要求之一)。 我让代码运行两个设置:

  • 笔记本电脑,酷睿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的值后,因为它们是明显的异常值): enter image description here enter image description here option1和option3都是我的SSD的最大值。我没想到会看到这一点,因为当时选项2曾经是我机器上最快的代码。

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

答案 1 :(得分:23)

按顺序尝试以下操作:

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

    注意:我注意到非常大的缓冲区倾向于降低性能。我注意到之前使用16-MiB缓冲区而不是512-KiB缓冲区的速度损失。

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

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

答案 2 :(得分:21)

我认为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从内存中的1个位置复制到另一个位置,看看它有多快。

修改

我将代码更改为使用较低级别的调用:
即没有缓冲。

#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();
}

答案 3 :(得分:12)

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

查看时间线:

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

&#39; F&#39;表示缓冲区填充的时间,并且&#39; W&#39;表示将缓冲区写入磁盘的时间。所以在将缓冲区写入文件之间浪费时间的问题。但是,通过在单独的线程上实现写入,您可以立即开始填充下一个缓冲区:

------------------------------------------------> (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 on主线程,该方法将返回False或将抛出异常。即使你在失败后提前写了一些数据,你停止填充缓冲区的时间并不重要 - 很可能文件会被破坏和无用。

答案 4 :(得分:10)

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

答案 5 :(得分: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的文件,将它存储在数据库中,并且文件甚至没有打开。所以解决方案是利用moemory map。我虽然在Python做到了。

答案 6 :(得分:6)

尝试使用open()/ write()/ close()API调用并尝试输出缓冲区大小。我的意思是不要通过整个&#34;很多字节&#34;缓冲一次,做几次写入(即TotalNumBytes / OutBufferSize)。 OutBufferSize可以从4096字节到兆字节。

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

无论如何,std :: ofstream是一个包装器,I / O操作可能会阻塞。请记住,遍历整个N-gigabyte阵列也需要一些时间。在编写一个小缓冲区时,它会进入缓存并运行得更快。

答案 7 :(得分:3)

尝试使用内存映射文件。

答案 8 :(得分:3)

如果您在资源管理器中将某些内容从磁盘A复制到磁盘B,则Windows会使用DMA。这意味着对于大多数复制过程而言,除了告诉磁盘控制器放置位置和从中获取数据之外,CPU基本上什么都不做,从而消除了整个链中的一个步骤,并且根本没有针对移动大量数据进行优化数据 - 我指的是硬件。

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

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

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

答案 9 :(得分:3)

fstream本身并不慢于C流,但它们使用更多CPU (特别是如果没有正确配置缓冲)。当CPU饱和时,它会限制I / O速率。

当未设置流缓冲区时,至少MSVC 2015实现将 1个字符一次复制到输出缓冲区(请参阅streambuf::xsputn)。所以确保设置流缓冲区(&gt; 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版本更慢。

答案 10 :(得分:1)

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

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

此外,在向文件写入大量数据时,逻辑扩展文件大小而不是物理上有时会更快,这是因为在逻辑上扩展文件时文件系统不会将新空间归零在写信之前。逻辑上扩展文件比实际需要更加智能,以防止大量文件扩展。 Windows上支持逻辑文件扩展,在XFS系统上使用SetFileValidData调用xfsctlXFS_IOC_RESVSP64

答案 11 :(得分:0)

我在win 7中的 GNU / Linux mingw 的gcc中编译我的程序并赢得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