webentwicklung-frage-antwort-db.com.de

Schnelle Möglichkeit, Daten von einem std :: vector in eine Textdatei zu schreiben

Momentan schreibe ich eine Reihe von Doubles von einem Vektor in eine Textdatei wie diese:

std::ofstream fout;
fout.open("vector.txt");

for (l = 0; l < vector.size(); l++)
    fout << std::setprecision(10) << vector.at(l) << std::endl;

fout.close();

Aber es braucht viel Zeit, um fertig zu werden. Gibt es eine schnellere oder effizientere Möglichkeit, dies zu tun? Ich würde es gerne sehen und lernen.

54

Ihr Algorithmus besteht aus zwei Teilen:

  1. Serialisieren Sie doppelte Zahlen in einen Zeichenketten- oder Zeichenpuffer.

  2. Ergebnisse in eine Datei schreiben.

Der erste Punkt kann mit sprintf oder fmt verbessert werden (> 20%). Das zweite Element kann beschleunigt werden, indem die Ergebnisse in einem Puffer zwischengespeichert oder die Größe des Ausgabedateistream-Puffers erweitert werden, bevor die Ergebnisse in die Ausgabedatei geschrieben werden. Sie sollten std :: endl nicht verwenden, weil es ist viel langsamer als "\ n" . Wenn Sie es dennoch schneller machen möchten, schreiben Sie Ihre Daten im Binärformat. Unten finden Sie mein vollständiges Codebeispiel mit meinen Lösungsvorschlägen und einem von Edgar Rokyan. Ich habe auch Vorschläge von Ben Voigt und Matthieu M in den Testcode aufgenommen.

#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <vector>

// https://github.com/fmtlib/fmt
#include "fmt/format.h"

// http://uscilab.github.io/cereal/
#include "cereal/archives/binary.hpp"
#include "cereal/archives/json.hpp"
#include "cereal/archives/portable_binary.hpp"
#include "cereal/archives/xml.hpp"
#include "cereal/types/string.hpp"
#include "cereal/types/vector.hpp"

// https://github.com/DigitalInBlue/Celero
#include "celero/Celero.h"

template <typename T> const char* getFormattedString();
template<> const char* getFormattedString<double>(){return "%g\n";}
template<> const char* getFormattedString<float>(){return "%g\n";}
template<> const char* getFormattedString<int>(){return "%d\n";}
template<> const char* getFormattedString<size_t>(){return "%lu\n";}


namespace {
    constexpr size_t LEN = 32;

    template <typename T> std::vector<T> create_test_data(const size_t N) {
        std::vector<T> data(N);
        for (size_t idx = 0; idx < N; ++idx) {
            data[idx] = idx;
        }
        return data;
    }

    template <typename Iterator> auto toVectorOfChar(Iterator begin, Iterator end) {
        char aLine[LEN];
        std::vector<char> buffer;
        buffer.reserve(std::distance(begin, end) * LEN);
        const char* fmtStr = getFormattedString<typename std::iterator_traits<Iterator>::value_type>();
        std::for_each(begin, end, [&buffer, &aLine, &fmtStr](const auto value) {
            sprintf(aLine, fmtStr, value);
            for (size_t idx = 0; aLine[idx] != 0; ++idx) {
                buffer.Push_back(aLine[idx]);
            }
        });
        return buffer;
    }

    template <typename Iterator>
    auto toStringStream(Iterator begin, Iterator end, std::stringstream &buffer) {
        char aLine[LEN];
        const char* fmtStr = getFormattedString<typename std::iterator_traits<Iterator>::value_type>();
        std::for_each(begin, end, [&buffer, &aLine, &fmtStr](const auto value) {            
            sprintf(aLine, fmtStr, value);
            buffer << aLine;
        });
    }

    template <typename Iterator> auto toMemoryWriter(Iterator begin, Iterator end) {
        fmt::MemoryWriter writer;
        std::for_each(begin, end, [&writer](const auto value) { writer << value << "\n"; });
        return writer;
    }

    // A modified version of the original approach.
    template <typename Container>
    void original_approach(const Container &data, const std::string &fileName) {
        std::ofstream fout(fileName);
        for (size_t l = 0; l < data.size(); l++) {
            fout << data[l] << std::endl;
        }
        fout.close();
    }

    // Replace std::endl by "\n"
    template <typename Iterator>
    void improved_original_approach(Iterator begin, Iterator end, const std::string &fileName) {
        std::ofstream fout(fileName);
        const size_t len = std::distance(begin, end) * LEN;
        std::vector<char> buffer(len);
        fout.rdbuf()->pubsetbuf(buffer.data(), len);
        for (Iterator it = begin; it != end; ++it) {
            fout << *it << "\n";
        }
        fout.close();
    }

    //
    template <typename Iterator>
    void edgar_rokyan_solution(Iterator begin, Iterator end, const std::string &fileName) {
        std::ofstream fout(fileName);
        std::copy(begin, end, std::ostream_iterator<double>(fout, "\n"));
    }

    // Cache to a string stream before writing to the output file
    template <typename Iterator>
    void stringstream_approach(Iterator begin, Iterator end, const std::string &fileName) {
        std::stringstream buffer;
        for (Iterator it = begin; it != end; ++it) {
            buffer << *it << "\n";
        }

        // Now write to the output file.
        std::ofstream fout(fileName);
        fout << buffer.str();
        fout.close();
    }

    // Use sprintf
    template <typename Iterator>
    void sprintf_approach(Iterator begin, Iterator end, const std::string &fileName) {
        std::stringstream buffer;
        toStringStream(begin, end, buffer);
        std::ofstream fout(fileName);
        fout << buffer.str();
        fout.close();
    }

    // Use fmt::MemoryWriter (https://github.com/fmtlib/fmt)
    template <typename Iterator>
    void fmt_approach(Iterator begin, Iterator end, const std::string &fileName) {
        auto writer = toMemoryWriter(begin, end);
        std::ofstream fout(fileName);
        fout << writer.str();
        fout.close();
    }

    // Use std::vector<char>
    template <typename Iterator>
    void vector_of_char_approach(Iterator begin, Iterator end, const std::string &fileName) {
        std::vector<char> buffer = toVectorOfChar(begin, end);
        std::ofstream fout(fileName);
        fout << buffer.data();
        fout.close();
    }

    // Use cereal (http://uscilab.github.io/cereal/).
    template <typename Container, typename OArchive = cereal::BinaryOutputArchive>
    void use_cereal(Container &&data, const std::string &fileName) {
        std::stringstream buffer;
        {
            OArchive oar(buffer);
            oar(data);
        }

        std::ofstream fout(fileName);
        fout << buffer.str();
        fout.close();
    }
}

// Performance test input data.
constexpr int NumberOfSamples = 5;
constexpr int NumberOfIterations = 2;
constexpr int N = 3000000;
const auto double_data = create_test_data<double>(N);
const auto float_data = create_test_data<float>(N);
const auto int_data = create_test_data<int>(N);
const auto size_t_data = create_test_data<size_t>(N);

CELERO_MAIN

BASELINE(DoubleVector, original_approach, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("origsol.txt");
    original_approach(double_data, fileName);
}

BENCHMARK(DoubleVector, improved_original_approach, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("improvedsol.txt");
    improved_original_approach(double_data.cbegin(), double_data.cend(), fileName);
}

BENCHMARK(DoubleVector, edgar_rokyan_solution, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("edgar_rokyan_solution.txt");
    edgar_rokyan_solution(double_data.cbegin(), double_data.end(), fileName);
}

BENCHMARK(DoubleVector, stringstream_approach, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("stringstream.txt");
    stringstream_approach(double_data.cbegin(), double_data.cend(), fileName);
}

BENCHMARK(DoubleVector, sprintf_approach, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("sprintf.txt");
    sprintf_approach(double_data.cbegin(), double_data.cend(), fileName);
}

BENCHMARK(DoubleVector, fmt_approach, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("fmt.txt");
    fmt_approach(double_data.cbegin(), double_data.cend(), fileName);
}

BENCHMARK(DoubleVector, vector_of_char_approach, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("vector_of_char.txt");
    vector_of_char_approach(double_data.cbegin(), double_data.cend(), fileName);
}

BENCHMARK(DoubleVector, use_cereal, NumberOfSamples, NumberOfIterations) {
    const std::string fileName("cereal.bin");
    use_cereal(double_data, fileName);
}

// Benchmark double vector
BASELINE(DoubleVectorConversion, toStringStream, NumberOfSamples, NumberOfIterations) {
    std::stringstream output;
    toStringStream(double_data.cbegin(), double_data.cend(), output);
}

BENCHMARK(DoubleVectorConversion, toMemoryWriter, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toMemoryWriter(double_data.cbegin(), double_data.cend()));
}

BENCHMARK(DoubleVectorConversion, toVectorOfChar, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toVectorOfChar(double_data.cbegin(), double_data.cend()));
}

// Benchmark float vector
BASELINE(FloatVectorConversion, toStringStream, NumberOfSamples, NumberOfIterations) {
    std::stringstream output;
    toStringStream(float_data.cbegin(), float_data.cend(), output);
}

BENCHMARK(FloatVectorConversion, toMemoryWriter, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toMemoryWriter(float_data.cbegin(), float_data.cend()));
}

BENCHMARK(FloatVectorConversion, toVectorOfChar, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toVectorOfChar(float_data.cbegin(), float_data.cend()));
}

// Benchmark int vector
BASELINE(int_conversion, toStringStream, NumberOfSamples, NumberOfIterations) {
    std::stringstream output;
    toStringStream(int_data.cbegin(), int_data.cend(), output);
}

BENCHMARK(int_conversion, toMemoryWriter, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toMemoryWriter(int_data.cbegin(), int_data.cend()));
}

BENCHMARK(int_conversion, toVectorOfChar, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toVectorOfChar(int_data.cbegin(), int_data.cend()));
}

// Benchmark size_t vector
BASELINE(size_t_conversion, toStringStream, NumberOfSamples, NumberOfIterations) {
    std::stringstream output;
    toStringStream(size_t_data.cbegin(), size_t_data.cend(), output);
}

BENCHMARK(size_t_conversion, toMemoryWriter, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toMemoryWriter(size_t_data.cbegin(), size_t_data.cend()));
}

BENCHMARK(size_t_conversion, toVectorOfChar, NumberOfSamples, NumberOfIterations) {
    celero::DoNotOptimizeAway(toVectorOfChar(size_t_data.cbegin(), size_t_data.cend()));
}

Im Folgenden sind die Leistungsergebnisse aufgeführt, die in meiner Linux-Box mit dem Flag clang-3.9.1 und -O3 erzielt wurden. Ich benutze Celero , um alle Performance-Ergebnisse zu sammeln.

Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
DoubleVector    | original_approa | Null            |              10 |               4 |         1.00000 |   3650309.00000 |            0.27 | 
DoubleVector    | improved_Origin | Null            |              10 |               4 |         0.47828 |   1745855.00000 |            0.57 | 
DoubleVector    | edgar_rokyan_so | Null            |              10 |               4 |         0.45804 |   1672005.00000 |            0.60 | 
DoubleVector    | stringstream_ap | Null            |              10 |               4 |         0.41514 |   1515377.00000 |            0.66 | 
DoubleVector    | sprintf_approac | Null            |              10 |               4 |         0.35436 |   1293521.50000 |            0.77 | 
DoubleVector    | fmt_approach    | Null            |              10 |               4 |         0.34916 |   1274552.75000 |            0.78 | 
DoubleVector    | vector_of_char_ | Null            |              10 |               4 |         0.34366 |   1254462.00000 |            0.80 | 
DoubleVector    | use_cereal      | Null            |              10 |               4 |         0.04172 |    152291.25000 |            6.57 | 
Complete.

Ich vergleiche auch die Leistung von std :: stringstream, fmt :: MemoryWriter und std :: vector mit Algorithmen zur Konvertierung von Zahlen in Zeichenfolgen.

Timer resolution: 0.001000 us
-----------------------------------------------------------------------------------------------------------------------------------------------
     Group      |   Experiment    |   Prob. Space   |     Samples     |   Iterations    |    Baseline     |  us/Iteration   | Iterations/sec  | 
-----------------------------------------------------------------------------------------------------------------------------------------------
DoubleVectorCon | toStringStream  | Null            |              10 |               4 |         1.00000 |   1272667.00000 |            0.79 | 
FloatVectorConv | toStringStream  | Null            |              10 |               4 |         1.00000 |   1272573.75000 |            0.79 | 
int_conversion  | toStringStream  | Null            |              10 |               4 |         1.00000 |    248709.00000 |            4.02 | 
size_t_conversi | toStringStream  | Null            |              10 |               4 |         1.00000 |    252063.00000 |            3.97 | 
DoubleVectorCon | toMemoryWriter  | Null            |              10 |               4 |         0.98468 |   1253165.50000 |            0.80 | 
DoubleVectorCon | toVectorOfChar  | Null            |              10 |               4 |         0.97146 |   1236340.50000 |            0.81 | 
FloatVectorConv | toMemoryWriter  | Null            |              10 |               4 |         0.98419 |   1252454.25000 |            0.80 | 
FloatVectorConv | toVectorOfChar  | Null            |              10 |               4 |         0.97369 |   1239093.25000 |            0.81 | 
int_conversion  | toMemoryWriter  | Null            |              10 |               4 |         0.11741 |     29200.50000 |           34.25 | 
int_conversion  | toVectorOfChar  | Null            |              10 |               4 |         0.87105 |    216637.00000 |            4.62 | 
size_t_conversi | toMemoryWriter  | Null            |              10 |               4 |         0.13746 |     34649.50000 |           28.86 | 
size_t_conversi | toVectorOfChar  | Null            |              10 |               4 |         0.85345 |    215123.00000 |            4.65 | 
Complete.

Aus den obigen Tabellen können wir folgendes ersehen:

  1. Die Edgar Rokyan-Lösung ist 10% langsamer als die Stringstream-Lösung. Die Lösung, die fmt library verwendet, ist die beste für drei untersuchte Datentypen: double, int und size_t. Die sprintf + std :: vector-Lösung ist 1% schneller als die fmt -Lösung für doppelten Datentyp. Ich empfehle jedoch keine Lösungen, die sprintf für Produktionscode verwenden, da sie nicht elegant sind (immer noch im C-Stil geschrieben) und für verschiedene Datentypen wie int oder size_t nicht sofort funktionieren.

  2. Die Benchmark-Ergebnisse zeigen auch, dass fmt die überlegene Serialisierung des integralen Datentyps ist, da sie mindestens 7x schneller ist als andere Ansätze.

  3. Wir können diesen Algorithmus 10x beschleunigen, wenn wir das Binärformat verwenden. Dieser Ansatz ist erheblich schneller als das Schreiben in eine formatierte Textdatei, da nur Rohkopien aus dem Speicher in die Ausgabe geschrieben werden. Wenn Sie flexiblere und portablere Lösungen möchten, versuchen Sie Getreide oder Boost :: Serialisierung oder Protokollpuffer . Nach diese Leistungsstudie scheint Getreide am schnellsten zu sein.

34
hungptit
std::ofstream fout("vector.txt");
fout << std::setprecision(10);

for(auto const& x : vector)
    fout << x << '\n';

Alles, was ich geändert habe, hatte theoretisch eine schlechtere Leistung in Ihrer Version des Codes, aber das std::endl War der wahre Killer . std::vector::at (mit Begrenzungsüberprüfung, die Sie nicht benötigen) wäre die zweite, dann die Tatsache, dass Sie keine Iteratoren verwendet haben.

Warum standardmäßig einen std::ofstream Erstellen und dann open aufrufen, wenn Sie dies in einem Schritt tun können? Warum close aufrufen, wenn RAII (der Destruktor) das für Sie erledigt? Sie können auch anrufen

fout << std::setprecision(10)

nur einmal vor der Schleife.

Wie im folgenden Kommentar erwähnt, erzielen Sie mit for(auto x : vector) möglicherweise eine bessere Leistung, wenn Ihr Vektor Elemente eines fundamentalen Typs enthält. Messen Sie die Laufzeit/überprüfen Sie die Ausgabe der Baugruppe.


Nur um auf eine andere Sache hinzuweisen, die mir aufgefallen ist:

for(l = 0; l < vector.size(); l++)

Was ist das l? Warum sollte es außerhalb der Schleife deklariert werden? Es scheint, dass Sie es im äußeren Bereich nicht brauchen, also tun Sie es nicht. Und auch das Nachinkrement .

Das Ergebnis:

for(size_t l = 0; l < vector.size(); ++l)

Es tut mir leid, dass ich aus diesem Beitrag eine Codeüberprüfung gemacht habe.

72
LogicStuff

Mit Hilfe von Iteratoren und der Funktion vector können Sie auch den Inhalt eines beliebigen copy in übersichtlicher Form in die Datei ausgeben.

std::ofstream fout("vector.txt");
fout.precision(10);

std::copy(numbers.begin(), numbers.end(),
    std::ostream_iterator<double>(fout, "\n"));

Diese Lösung ist hinsichtlich der Ausführungszeit praktisch identisch mit der Lösung von LogicStuff. Aber es zeigt auch, wie man den Inhalt nur mit einer einzigen Funktion copy druckt, die, wie ich nehme, ziemlich gut aussieht.

21
Edgar Rokjān

OK, ich bin traurig, dass es drei Lösungen gibt, die versuchen, Ihnen einen Fisch zu geben, aber keine, die Ihnen beibringen, wie man fischt.

Wenn Sie ein Leistungsproblem haben, besteht die Lösung darin, einen Profiler zu verwenden und das Problem zu beheben, das der Profiler anzeigt.

Das Konvertieren von Double-to-String für 300.000 Doubles dauert auf Computern, die in den letzten 10 Jahren ausgeliefert wurden, keine 3 Minuten.

Das Schreiben von 3 MB Daten auf die Festplatte (eine durchschnittliche Größe von 300.000 Doppelten) dauert auf keinem Computer, der in den letzten 10 Jahren ausgeliefert wurde, 3 Minuten.

Wenn Sie dies profilieren, werden Sie wahrscheinlich feststellen, dass fout 300.000-mal gelöscht wird und dass das Löschen langsam ist, da es möglicherweise das Blockieren oder Halbblockieren von E/A beinhaltet. Daher müssen Sie das Blockieren von E/A vermeiden. Die typische Vorgehensweise besteht darin, alle E/A-Vorgänge in einem einzigen Puffer vorzubereiten (einen String-Stream zu erstellen, in diesen zu schreiben) und diesen Puffer dann auf einmal in eine physische Datei zu schreiben. Dies ist die Lösung, die hungptit beschreibt, außer ich denke, was fehlt, ist zu erklären, WARUM diese Lösung eine gute Lösung ist.

Oder anders ausgedrückt: Der Profiler teilt Ihnen mit, dass das Aufrufen von write () (unter Linux) oder WriteFile () (unter Windows) viel langsamer ist, als nur ein paar Bytes in einen Speicherpuffer zu kopieren, da es sich um einen Benutzer handelt/Kernel Level Übergang. Wenn std :: endl dies für jedes Double verursacht, werden Sie eine schlechte (langsame) Zeit haben. Ersetzen Sie es durch etwas, das nur im Benutzerraum verbleibt und Daten in den Arbeitsspeicher legt!

Wenn dies immer noch nicht schnell genug ist, kann es sein, dass die spezifisch genaue Version von Operator << () für Zeichenfolgen langsam ist oder unnötigen Overhead verursacht. In diesem Fall können Sie den Code möglicherweise noch weiter beschleunigen, indem Sie sprintf () oder eine andere potenziell schnellere Funktion verwenden, um Daten in den speicherinternen Puffer zu generieren, bevor Sie den gesamten Puffer auf einmal in eine Datei schreiben.

11
Jon Watte

Sie haben zwei Hauptengpässe in Ihrem Programm: Ausgabe und Formatierung von Text.

Um die Leistung zu steigern, sollten Sie die Datenmenge pro Anruf erhöhen. Beispielsweise ist eine Ausgabeübertragung von 500 Zeichen schneller als 500 Übertragungen von 1 Zeichen.

Meine Empfehlung ist, dass Sie die Daten in einen großen Puffer formatieren und dann den Puffer blockieren.

Hier ist ein Beispiel:

char buffer[1024 * 1024];
unsigned int buffer_index = 0;
const unsigned int size = my_vector.size();
for (unsigned int i = 0; i < size; ++i)
{
  signed int characters_formatted = snprintf(&buffer[buffer_index],
                                             (1024 * 1024) - buffer_index,
                                             "%.10f", my_vector[i]);
  if (characters_formatted > 0)
  {
      buffer_index += (unsigned int) characters_formatted;
  }
}
cout.write(&buffer[0], buffer_index);

Sie sollten zuerst versuchen, die Optimierungseinstellungen in Ihrem Compiler zu ändern, bevor Sie mit dem Code herumspielen.

5
Thomas Matthews

Hier ist eine etwas andere Lösung: Speichern Sie Ihre Doppel in binärer Form.

int fd = ::open("/path/to/the/file", O_WRONLY /* whatever permission */);
::write(fd, &vector[0], vector.size() * sizeof(vector[0]));

Da Sie erwähnt haben, dass Sie 300k Doubles haben, was 300k * 8 Bytes = 2,4M entspricht, können Sie alle in weniger als 0,1 Sekunden auf der lokalen Festplatte speichern. Der einzige Nachteil dieser Methode ist, dass gespeicherte Dateien nicht so lesbar sind wie Zeichenfolgendarstellungen. Ein HexEditor kann dieses Problem jedoch lösen.

Wenn Sie es robuster mögen, sind zahlreiche Serialisierungsbibliotheken/-tools online verfügbar. Sie bieten weitere Vorteile, z. B. sprachneutrale, maschinenunabhängige, flexible Komprimierungsalgorithmen usw. Dies sind die beiden, die ich normalerweise verwende:

2
Jason L.