Ich versuche große Datenmengen auf meine SSD (Solid State Drive) zu schreiben. Und mit riesigen Mengen meine ich 80GB.
Ich habe im Internet nach Lösungen gesucht, aber das Beste, was mir eingefallen ist, war Folgendes:
#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();
}
Kompiliert mit Visual Studio 2010 und vollständigen Optimierungen und ausgeführt unter Windows 7 erreicht dieses Programm eine maximale Geschwindigkeit von ca. 20 MB/s. Was mich wirklich stört, ist, dass Windows Dateien von einer anderen SSD mit einer Geschwindigkeit zwischen 150 MB/s und 200 MB/s auf diese SSD kopieren kann. Also mindestens 7 mal schneller. Deshalb denke ich, ich sollte in der Lage sein, schneller zu fahren.
Irgendwelche Ideen, wie ich mein Schreiben beschleunigen kann?
Das hat die Arbeit gemacht:
#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;
}
Ich habe gerade 8 GB in 36 Sekunden gemessen, was ungefähr 220 MB/s entspricht, und ich denke, das schöpft meine SSD aus. Erwähnenswert ist auch, dass der Code in der Frage einen Kern zu 100% verwendete, während dieser Code nur 2-5% verwendet.
Vielen Dank an alle.
Update : 5 Jahre sind vergangen. Compiler, Hardware, Bibliotheken und meine Anforderungen haben sich geändert. Aus diesem Grund habe ich einige Änderungen am Code vorgenommen und einige Messungen durchgeführt.
Zuerst den Code hoch:
#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;
}
Jetzt kompiliert der Code mit Visual Studio 2017 und g ++ 7.2.0 (was jetzt eine meiner Anforderungen ist). Ich habe den Code mit zwei Setups laufen lassen:
Welches ergab die folgenden Messungen (nach Abzug der Werte für 1 MB, weil sie offensichtliche Ausreißer waren): Beide Male, Option1 und Option3, maximieren meine SSD. Ich hatte nicht damit gerechnet, da Option2 damals der schnellste Code auf meinem Computer war.
TL; DR : Meine Messungen weisen darauf hin, dass std::fstream
Über FILE
verwendet wird.
Versuchen Sie Folgendes, um:
Kleinere Puffergröße. Das gleichzeitige Schreiben von ~ 2 MiB könnte ein guter Anfang sein. Auf meinem letzten Laptop war ~ 512 KiB der Sweet Spot, aber ich habe es noch nicht auf meiner SSD getestet.
Hinweis: Ich habe festgestellt, dass sehr große Puffer dazu neigen, die Leistung zu verringern. Ich habe zuvor Geschwindigkeitsverluste bei der Verwendung von 16-MiB-Puffern anstelle von 512-KiB-Puffern festgestellt.
Verwenden _open
(oder _topen
Wenn Sie Windows-korrekt sein möchten), um die Datei zu öffnen, verwenden Sie _write
. Dies wird wahrscheinlich viel Pufferung vermeiden, aber es ist nicht sicher.
Verwendung von Windows-spezifischen Funktionen wie CreateFile
und WriteFile
. Dadurch wird eine Pufferung in der Standardbibliothek vermieden.
Ich sehe keinen Unterschied zwischen std :: stream/FILE/device. Zwischen puffern und nicht puffern.
Beachten Sie auch:
Ich sehe den Code in 63 Sekunden ausgeführt.
Also eine Übertragungsrate von: 260M/s (meine SSD sieht etwas schneller aus als deine).
64 * 1024 * 1024 * 8 /*sizeof(unsigned long long) */ * 32 /*Chunks*/
= 16G
= 16G/63 = 260M/s
Ich bekomme keine Erhöhung, wenn ich von std :: fstream zu FILE * wechsle.
#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);
}
Der C++ - Stream arbeitet also so schnell, wie es die zugrunde liegende Bibliothek zulässt.
Ich halte es jedoch für unfair, das Betriebssystem mit einer Anwendung zu vergleichen, die auf dem Betriebssystem aufbaut. Die Anwendung kann keine Annahmen treffen (sie weiß nicht, dass es sich bei den Laufwerken um SSD handelt) und verwendet daher die Dateimechanismen des Betriebssystems für die Übertragung.
Dabei muss das Betriebssystem keine Annahmen treffen. Es kann die Typen der beteiligten Laufwerke erkennen und die optimale Technik für die Übertragung der Daten verwenden. In diesem Fall erfolgt eine direkte Übertragung von Speicher zu Speicher. Versuchen Sie, ein Programm zu schreiben, das 80 GB von einem Speicherort in einen anderen kopiert, und sehen Sie, wie schnell das ist.
Ich habe meinen Code geändert, um die niedrigeren Aufrufe zu verwenden:
dh keine Pufferung.
#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);
}
Dies machte keinen Unterschied.
NOTE : Mein Laufwerk ist ein SSD-Laufwerk, wenn Sie ein normales Laufwerk haben Möglicherweise besteht ein Unterschied zwischen den beiden oben genannten Techniken. Aber wie ich erwartet hatte, macht das Nicht-Puffern und Puffern (wenn große Blöcke geschrieben werden, die größer als die Puffergröße sind) keinen Unterschied.
Haben Sie die schnellste Methode zum Kopieren von Dateien in C++ ausprobiert?
int main()
{
std::ifstream input("input");
std::ofstream output("ouptut");
output << input.rdbuf();
}
Die beste Lösung ist die Implementierung eines asynchronen Schreibvorgangs mit doppelter Pufferung.
Schauen Sie sich die Zeitleiste an:
------------------------------------------------>
FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|
Das 'F' steht für die Zeit zum Füllen des Puffers und das 'W' für die Zeit zum Schreiben des Puffers auf die Festplatte. Also das Problem beim Verschwenden von Zeit zwischen dem Schreiben von Puffern in eine Datei. Wenn Sie jedoch das Schreiben in einem separaten Thread implementieren, können Sie den nächsten Puffer sofort wie folgt füllen:
------------------------------------------------> (main thread, fills buffers)
FF|ff______|FF______|ff______|________|
------------------------------------------------> (writer thread)
|WWWWWWWW|wwwwwwww|WWWWWWWW|wwwwwwww|
F - 1. Puffer füllen
f - Füllen des 2. Puffers
W - Schreiben des ersten Puffers in die Datei
w - Schreiben des 2. Puffers in die Datei
_ - Warten Sie, bis der Vorgang abgeschlossen ist
Dieser Ansatz mit Pufferaustausch ist sehr nützlich, wenn das Füllen eines Puffers eine komplexere Berechnung erfordert (daher mehr Zeit). Ich implementiere immer eine CSequentialStreamWriter-Klasse, die asynchrones Schreiben verbirgt, sodass die Schnittstelle für den Endbenutzer nur Schreibfunktionen hat.
Die Puffergröße muss ein Vielfaches der Festplattenclustergröße sein. Andernfalls wird die Leistung beeinträchtigt, wenn Sie einen einzelnen Puffer in zwei benachbarte Festplattencluster schreiben.
Schreiben des letzten Puffers.
Wenn Sie die Write-Funktion zum letzten Mal aufrufen, müssen Sie sicherstellen, dass der aktuelle Puffer auch auf die Festplatte geschrieben wird. Daher sollte CSequentialStreamWriter eine separate Methode haben, zum Beispiel Finalize (final buffer flush), mit der der letzte Teil der Daten auf die Festplatte geschrieben werden soll.
Fehlerbehandlung.
Während der Code den 2. Puffer füllt und der 1. in einen separaten Thread geschrieben wird, das Schreiben jedoch aus irgendeinem Grund fehlschlägt, sollte der Haupt-Thread diesen Fehler kennen.
------------------------------------------------> (main thread, fills buffers)
FF|fX|
------------------------------------------------> (writer thread)
__|X|
Angenommen, die Schnittstelle eines CSequentialStreamWriter hat die Funktion Write gibt bool zurück oder löst eine Ausnahme aus. Wenn also ein Fehler in einem separaten Thread auftritt, müssen Sie sich diesen Status merken. Wenn Sie also das nächste Mal Write oder Finilize im Hauptthread aufrufen, wird die Methode zurückgegeben Falsch oder löst eine Ausnahme aus. Dabei spielt es keine Rolle, an welchem Punkt Sie aufgehört haben, einen Puffer zu füllen, auch wenn Sie nach dem Fehler einige Daten vorher geschrieben haben - höchstwahrscheinlich wäre die Datei beschädigt und unbrauchbar.
Ich würde vorschlagen, es mit Dateizuordnung zu versuchen. Ich habe mmap
in der Vergangenheit in einer UNIX-Umgebung verwendet und war beeindruckt von der hohen Leistung, die ich erzielen konnte
Könnten Sie FILE*
stattdessen und messen Sie die Leistung, die Sie gewonnen haben? Eine Reihe von Optionen ist die Verwendung von fwrite/write
anstelle von 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;
}
Wenn Sie sich für write
entscheiden, versuchen Sie etwas Ähnliches:
#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;
}
Ich würde Ihnen auch raten, memory map
. Das könnte deine Antwort sein. Einmal musste ich eine 20GB-Datei in einer anderen verarbeiten, um sie in der Datenbank zu speichern und die Datei nicht einmal zu öffnen. Also die Lösung, um Moemory Map zu nutzen. Ich habe das allerdings in Python
gemacht.
Versuchen Sie, die API-Aufrufe open ()/write ()/close () zu verwenden, und experimentieren Sie mit der Ausgabepuffergröße. Ich meine, übergebe nicht den gesamten "Many-Many-Bytes" -Puffer auf einmal, sondern schreibe ein paar Mal (d. H. TotalNumBytes/OutBufferSize). OutBufferSize kann zwischen 4096 Byte und Megabyte liegen.
Ein weiterer Versuch: Verwenden Sie WinAPI OpenFile/CreateFile und dieser MSDN-Artikel , um die Pufferung zu deaktivieren (FILE_FLAG_NO_BUFFERING). Und dieser MSDN-Artikel zu WriteFile () zeigt, wie die Blockgröße für das Laufwerk ermittelt wird, um die optimale Puffergröße zu ermitteln.
Wie auch immer, std :: ofstream ist ein Wrapper und blockiert möglicherweise E/A-Vorgänge. Beachten Sie, dass das Durchlaufen des gesamten N-Gigabyte-Arrays auch einige Zeit in Anspruch nimmt. Während Sie einen kleinen Puffer schreiben, gelangt dieser in den Cache und arbeitet schneller.
Wenn Sie im Explorer etwas von Datenträger A auf Datenträger B kopieren, verwendet Windows DMA. Das bedeutet, dass die CPU für den größten Teil des Kopiervorgangs nichts anderes tut, als dem Plattencontroller mitzuteilen, wo er Daten ablegen und von wo er sie abrufen soll, wodurch ein ganzer Schritt in der Kette entfällt und der überhaupt nicht für das Verschieben großer Mengen optimiert ist von Daten - und ich meine Hardware.
Was Sie tun, hat viel mit der CPU zu tun. Ich möchte Sie auf den Teil "Einige Berechnungen zum Füllen eines []" verweisen. Was ich für wesentlich halte. Sie generieren ein [], kopieren dann von einem [] in einen Ausgabepuffer (das macht fstream :: write) und generieren dann erneut usw.
Was ist zu tun? Multithreading! (Ich hoffe du hast einen Multi-Core Prozessor)
fstream
s sind an sich nicht langsamer als C-Streams, verwenden jedochmehr CPU(insbesondere, wenn die Pufferung nicht ordnungsgemäß konfiguriert ist). Wenn eine CPU gesättigt ist, wird die E/A-Rate begrenzt.
Mindestens die MSVC 2015-Implementierung kopiert1 Zeichen gleichzeitigin den Ausgabepuffer, wenn kein Stream-Puffer festgelegt ist (siehe streambuf::xsputn
). Also stellen Sie sicher, dass Sie einen Stream Buffer (> 0) setzen.
Ich kann mit fstream
eine Schreibgeschwindigkeit von 1500 MB/s (die volle Geschwindigkeit meiner M.2-SSD) erreichen, indem ich diesen Code verwende:
#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";
}
Ich habe diesen Code auf anderen Plattformen (Ubuntu, FreeBSD) ausprobiert und festgestellt, dass es keine Unterschiede in der E/A-Rate gibt, aber eine Differenz vonCPU-Auslastungvon ungefähr 8: 1 (fstream
verwendet8-mal mehr CPU). Man kann sich also vorstellen, wenn ich eine schnellere Festplatte hätte, würde der Schreibvorgang mit fstream
schneller verlangsamen als mit stdio
.
Versuchen Sie, Dateien mit Speicherzuordnung zu verwenden.
Wenn Sie schnell in Dateistreams schreiben möchten, können Sie den Lesepuffer für Streams vergrößern:
wfstream f;
const size_t nBufferSize = 16184;
wchar_t buffer[nBufferSize];
f.rdbuf()->pubsetbuf(buffer, nBufferSize);
Wenn Sie viele Daten in Dateien schreiben, ist es manchmal schneller, die Dateigröße logisch anstatt physisch zu erweitern, da das Dateisystem beim logischen Erweitern einer Datei den neuen Speicherplatz vor dem Schreiben nicht auf Null setzt dazu. Es ist auch sinnvoll, die Datei logisch mehr zu erweitern, als Sie tatsächlich benötigen, um viele Dateierweiterungen zu vermeiden. Die logische Dateierweiterung wird unter Windows durch Aufrufen von SetFileValidData
oder xfsctl
mit XFS_IOC_RESVSP64
Auf XFS-Systemen unterstützt.
ich kompiliere mein Programm in gcc in GNU/Linux und mingw in win 7 und Win XP und hat gut funktioniert
sie können mein Programm verwenden und zum Erstellen einer 80-GB-Datei einfach die Zeile 33 in ändern
makeFile("Text.txt",1024,8192000);
wenn Sie das Programm beenden, wird die Datei zerstört. Überprüfen Sie die Datei, wenn sie ausgeführt wird
um das gewünschte Programm zu haben, ändern Sie einfach das Programm
das erste ist das Windows-Programm und das zweite ist für GNU/Linux