Szybki sposób zapisu danych ze std:: vector do pliku tekstowego

Obecnie zapisuję zbiór podwajaczy z wektora do pliku tekstowego w taki sposób:

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();
Ale to zajmuje dużo czasu. Czy istnieje szybszy lub skuteczniejszy sposób, aby to zrobić? Chciałbym to zobaczyć i się tego nauczyć.
Author: LogicStuff, 2016-09-28

6 answers

Twój algorytm ma dwie części:

  1. Serializuj podwójne liczby do bufora łańcuchowego lub znakowego.

  2. Zapisz wyniki do pliku.

Pierwszy element można ulepszyć (>20%) używając sprintf lub fmt. Drugi element może być przyspieszony przez buforowanie wyników do bufora lub rozszerzenie rozmiaru bufora strumienia pliku wyjściowego przed zapisaniem wyników do pliku wyjściowego. Nie powinieneś używać STD:: endl, ponieważ jest znacznie wolniejsze niż używanie "\n". Jeśli nadal chcesz zrobić to szybciej, Zapisz swoje dane w formacie binarnym. Poniżej znajduje się moja pełna próbka kodu, która zawiera moje proponowane rozwiązania i jeden z Edgara Rokyana. W kodzie testowym umieściłem również sugestie Bena Voigta i Matthieu M.

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

Poniżej są wyniki wydajności uzyskane w moim Linuksie przy użyciu flagi clang-3.9.1 i-O3. Używam Celero do zbierania wszystkich wyników.

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.

Porównuję również algorytmy konwersji numerycznej na ciąg znaków do porównaj wydajność STD:: stringstream, FMT:: MemoryWriter i std:: vector.

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.

Z powyższych tabel wynika, że:

  1. Roztwór Edgara Rokyana jest o 10% wolniejszy od roztworu stringstream. Rozwiązanie wykorzystujące bibliotekę fmt jest najlepsze dla trzech badanych typów danych, które są double, int i size_t. rozwiązanie sprintf + std::vector jest o 1% szybsze niż rozwiązanie FMT dla typu danych double. Nie polecam jednak rozwiązań wykorzystujących sprintf dla kodu produkcyjnego, ponieważ nie są eleganckie (nadal napisane w stylu C) i nie działają po wyjęciu z pudełka dla różnych typów danych, takich jak int lub size_t.

  2. Wyniki benchmarku pokazują również, że fmt jest superrior integral data type serialization, ponieważ jest co najmniej 7x szybszy niż inne podejścia.

  3. Możemy przyspieszyć ten algorytm 10x, jeśli użyjemy formatu binarnego. Takie podejście jest znacznie szybsze niż pisanie do sformatowanego tekstu pliku, ponieważ wykonujemy tylko surową kopię z pamięci na wyjście. Jeśli chcesz mieć bardziej elastyczne i przenośne rozwiązania, spróbuj cereal lub boost::serialization lub protocol-buffer. Według to badanie wydajności płatki wydają się być najszybsze.

 33
Author: hungptit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:16:53
std::ofstream fout("vector.txt");
fout << std::setprecision(10);

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

Wszystko co zmieniłem miało teoretycznie gorszą wydajność w Twojej wersji kodu, ale std::endl był prawdziwym zabójcą . std::vector::at (z sprawdzaniem granic, których nie potrzebujesz) byłby drugi, a następnie fakt, że nie korzystałeś z iteratorów.

Dlaczego default-construct a std::ofstream a następnie call open, skoro można to zrobić w jednym kroku? Po co dzwonić, skoro RAII (Destruktor) zajmie się tym za Ciebie? Możesz również zadzwonić

fout << std::setprecision(10)

Tylko raz, zanim pętla.

Jak wspomniano w komentarzu poniżej, jeśli twój wektor składa się z elementów fundamentalnych, możesz uzyskać lepszą wydajność z for(auto x : vector). Zmierz czas pracy / sprawdź wyjście montażowe.


Żeby zaznaczyć jeszcze jedną rzecz, która wpadła mi w oczy, to:

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

Co to jest l? Dlaczego deklarujesz to poza pętlą? Wygląda na to, że nie potrzebujesz go w zewnętrznym zakresie, więc nie rób tego.

The wynik:

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

Przepraszam za zrobienie recenzji kodu z tego posta.

 72
Author: LogicStuff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:16:53

Można również użyć dość zgrabnej formy wyprowadzania zawartości dowolnego vector do pliku, przy pomocy iteratorów i funkcji copy.

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

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

Rozwiązanie to jest praktycznie takie samo z rozwiązaniem LogicStuff pod względem czasu wykonania. Ale ilustruje również, jak wydrukować zawartość za pomocą jednej funkcji copy, która, jak przypuszczam, wygląda całkiem dobrze.

 21
Author: Edgar Rokjān,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-17 21:27:50

OK, jestem smutny, że istnieją trzy rozwiązania, które próbują dać ci rybę, ale nie ma rozwiązania, które próbuje nauczyć cię łowić.

Gdy masz problem z wydajnością, rozwiązaniem jest użycie profilera i naprawienie dowolnego problemu, który pokazuje profiler.

Konwersja double-to-string dla 300,000 sobowtórów nie zajmie 3 minut na żadnym komputerze, który został wysłany w ciągu ostatnich 10 lat.

Zapisanie 3 MB danych na dysk (średni rozmiar 300 000) nie spowoduje poświęć 3 minuty na dowolnym komputerze, który został wysłany w ciągu ostatnich 10 lat.

Jeśli to profilujesz, domyślam się, że zauważysz, że fout jest spłukiwany 300 000 razy, a spłukiwanie jest powolne, ponieważ może wiązać się z blokowaniem lub półblokowaniem We/Wy.dlatego musisz unikać blokowania We/Wy. typowym sposobem jest przygotowanie wszystkich We/Wy do pojedynczego bufora (Utwórz strumień stringstream, Zapisz do niego), a następnie zapisanie tego bufora do fizycznego pliku za jednym razem. To jest rozwiązanie hungptit opisuje, ale myślę, że brakuje wyjaśnienia, dlaczego to rozwiązanie jest dobrym rozwiązaniem.

Lub inaczej mówiąc: profiler powie Ci, że wywołanie write() (w Linuksie) lub WriteFile() (w Windows) jest o wiele wolniejsze niż zwykłe skopiowanie kilku bajtów do bufora pamięci, ponieważ jest to przejście na poziomie użytkownik/jądro. Jeśli std:: endl spowoduje to dla każdego podwajania, będziesz miał zły (wolny) czas. Zastąp go czymś, co po prostu pozostaje w przestrzeni użytkownika i umieszcza dane w pamięci RAM!

Jeśli to wciąż nie jest wystarczająco szybkie, może się zdarzyć, że wersja operatora o określonej precyzji

 11
Author: Jon Watte,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-29 03:41:41

Masz dwa główne wąskie gardła w programie: wyjście i formatowanie tekstu.

Aby zwiększyć wydajność, należy zwiększyć ilość danych wyjściowych na połączenie. Na przykład 1 Transfer wyjściowy 500 znaków jest szybszy niż 500 transferów 1 znaku.

Zalecam sformatowanie danych do dużego bufora, a następnie zablokowanie zapisu bufora.

Oto przykład:

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);

Powinieneś najpierw spróbować zmienić ustawienia optymalizacji w kompilatorze zanim pogmatwasz kod.

 5
Author: Thomas Matthews,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-28 17:40:03

Oto nieco inne rozwiązanie: Zapisz swoje sobowtóry w postaci binarnej.

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

Skoro wspomniałeś, że masz 300K Dubli, co odpowiada 300k * 8 bajtów = 2,4 m, Możesz zapisać je wszystkie na dysku lokalnym w mniej niż 0,1 sekundy . Jedyną wadą tej metody jest to, że zapisany plik nie jest tak czytelny jak reprezentacja łańcuchowa, ale HexEditor może rozwiązać ten problem.

Jeśli wolisz bardziej solidny sposób, istnieje wiele bibliotek serializacyjnych / narzędzi dostępny on-line. Zapewniają one więcej korzyści, takich jak neutralny językowo, niezależny od maszyny, elastyczny algorytm kompresji itp. Są to dwa, których zwykle używam:

 2
Author: Jason L.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-28 18:46:54