web-dev-qa-db-ger.com

Schreiben einer Binärdatei in C ++ sehr schnell

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?

217
Dominic Hofer

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:

  • Laptop, Core i7, SSD, Ubuntu 16.04, g ++ Version 7.2.0 mit -std = c ++ 11 -march = native -O3
  • Desktop, Core i7, SSD, Windows 10, Visual Studio 2017 Version 15.3.1 mit/Ox/Ob2/Oi/Ot/GT/GL/Gy

Welches ergab die folgenden Messungen (nach Abzug der Werte für 1 MB, weil sie offensichtliche Ausreißer waren): enter image description hereenter image description here 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.

197
Dominic Hofer

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.

23
Mehrdad

Ich sehe keinen Unterschied zwischen std :: stream/FILE/device. Zwischen puffern und nicht puffern.

Beachten Sie auch:

  • SSD-Laufwerke "neigen" zum Verlangsamen (niedrigere Übertragungsraten), wenn sie voll werden.
  • SSD-Laufwerke "neigen" dazu, langsamer zu werden (niedrigere Übertragungsraten), wenn sie älter werden (wegen nicht funktionierender Bits).

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.

Bearbeiten

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.

Bearbeiten 2:

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();
}
21
Martin York

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.

12
HandMadeOX

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

10
Ralph

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.

8
cybertextron

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.

6
Viktor Latypov

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)

  • gabel.
  • Verwenden Sie einen Thread, um [] Daten zu generieren
  • Verwenden Sie die andere, um Daten von einem [] auf die Festplatte zu schreiben
  • Sie benötigen zwei Arrays a1 [] und a2 [] und wechseln zwischen ihnen
  • Sie benötigen eine Synchronisierung zwischen Ihren Threads (Semaphoren, Nachrichtenwarteschlange usw.).
  • Verwenden Sie ungepufferte Funktionen auf niedrigerer Ebene, wie die von Mehrdad erwähnte Funktion WriteFile
3
dualed

fstreams 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.

3
rustyx

Versuchen Sie, Dateien mit Speicherzuordnung zu verwenden.

3
qehgt

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.

1
user152949

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

http://mustafajf.persiangig.com/Projects/File/WinFile.cpp

http://mustafajf.persiangig.com/Projects/File/File.cpp

0
MostafaJF