我正在尝试将大量数据写入我的 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 倍。这就是为什么我认为我应该能够走得更快。
有什么想法可以加快写作速度吗?
fwrite()
,我可以获得大约 80% 的峰值写入速度。只有使用 FILE_FLAG_NO_BUFFERING
,我才能获得最大速度。
这完成了工作(在 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
https://i.stack.imgur.com/cOdNE.jpg
TL;DR:我的测量结果表明使用 std::fstream
而不是 FILE
。
按顺序尝试以下操作:
较小的缓冲区大小。一次写入约 2 MiB 可能是一个好的开始。在我的最后一台笔记本电脑上,~512 KiB 是最佳选择,但我还没有在我的 SSD 上进行测试。注意:我注意到非常大的缓冲区往往会降低性能。我注意到以前使用 16-MiB 缓冲区而不是 512-KiB 缓冲区会造成速度损失。
使用 _open(或 _topen,如果你想是 Windows 正确的)打开文件,然后使用 _write。这可能会避免大量缓冲,但不确定。
使用特定于 Windows 的函数,如 CreateFile 和 WriteFile。这将避免标准库中的任何缓冲。
FILE_FLAG_NO_BUFFERING
- 其中较大的缓冲区往往更好。因为我认为 FILE_FLAG_NO_BUFFERING
几乎是 DMA。
我认为 std::stream/FILE/device 之间没有区别。在缓冲和非缓冲之间。
另请注意:
SSD 驱动器在装满时“倾向于”减慢速度(较低的传输速率)。
随着 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();
}
FILE*
。
最好的解决方案是使用双缓冲实现异步写入。
看时间线:
------------------------------------------------>
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 或将抛出异常。即使您在失败后提前写入了一些数据,您在什么时候停止填充缓冲区也无关紧要 - 文件很可能已损坏且无用。
我建议尝试 file mapping。我过去在 UNIX 环境中使用过mmap
,我对可以实现的高性能印象深刻
您能否改用 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*
等效项获得了全速。您当前的缓冲区太小。
2
对应于标准错误,但仍然建议您使用 STDERR_FILENO
而不是 2
。另一个重要问题是,当您收到中断信号时,您可能会遇到的一个错误是 EINTR,这不是真正的错误,您应该再试一次。
fstream
本身并不比 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
版本慢。
尝试使用 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 千兆字节数组也需要一些时间。当您写入一个小缓冲区时,它会进入缓存并且工作得更快。
如果您在资源管理器中将某些内容从磁盘 A 复制到磁盘 B,Windows 会使用 DMA。这意味着对于大多数复制过程,CPU 基本上只会告诉磁盘控制器将数据放在哪里,从哪里获取数据,从而消除链中的整个步骤,以及根本没有针对移动大量数据进行优化的步骤数据-我的意思是硬件。
你所做的事情涉及到 CPU 很多。我想向您指出“填充 [] 的一些计算”部分。我认为这是必不可少的。您生成 a[],然后从 a[] 复制到输出缓冲区(这就是 fstream::write 所做的),然后再次生成,等等。
该怎么办?多线程! (我希望你有一个多核处理器)
叉子。
使用一个线程生成一个[]数据
使用 other 将数据从 a[] 写入磁盘
您将需要两个数组 a1[] 和 a2[] 并在它们之间切换
您将需要在线程(信号量、消息队列等)之间进行某种同步
使用较低级别的无缓冲函数,例如 Mehrdad 提到的 WriteFile 函数
尝试使用内存映射文件。
ReadFile
的普通读/写以及顺序访问那样有效,尽管对于随机访问它们可能会更好。
如果您想快速写入文件流,则可以使流读取缓冲区更大:
wfstream f;
const size_t nBufferSize = 16184;
wchar_t buffer[nBufferSize];
f.rdbuf()->pubsetbuf(buffer, nBufferSize);
此外,当将大量数据写入文件时,逻辑上扩展文件大小有时比物理上更快,这是因为在逻辑上扩展文件时,文件系统不会在写入之前将新空间归零给它。在逻辑上扩展文件比您实际需要的更多以防止大量文件扩展也是明智的。通过在 XFS 系统上使用 XFS_IOC_RESVSP64
调用 SetFileValidData
或 xfsctl
在 Windows 上支持逻辑文件扩展。
我在 GNU/Linux 中的 gcc 和 win 7 和 win xp 中的 mingw 中编译我的程序并且运行良好
您可以使用我的程序并创建一个 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
不定期副业成功案例分享
FILE*
比流更快。我没想到会有这样的差异,因为无论如何它“应该”是 I/O 绑定的。ios::sync_with_stdio(false);
对带有流的代码有什么影响吗?我只是好奇使用这条线与不使用这条线有多大区别,但我没有足够快的磁盘来检查角落案例。如果有任何真正的区别。