web-dev-qa-db-fra.com

Un moyen rapide d'écrire des données d'un std :: vector dans un fichier texte

J'écris actuellement un ensemble de doubles d'un vecteur dans un fichier texte comme celui-ci:

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

Mais cela prend beaucoup de temps pour finir. Existe-t-il un moyen plus rapide ou plus efficace de le faire? J'adorerais le voir et l'apprendre.

54
Diego Fernando Pava

Votre algorithme comprend deux parties:

  1. Sérialiser des nombres doubles dans une chaîne ou un tampon de caractères.

  2. Écrivez les résultats dans un fichier.

Le premier élément peut être amélioré (> 20%) en utilisant sprintf ou fmt . Le deuxième élément peut être accéléré en mettant en cache les résultats dans un tampon ou en étendant la taille de la mémoire tampon du flux du fichier de sortie avant d'écrire les résultats dans le fichier de sortie. Vous ne devez pas utiliser std :: endl car c'est beaucoup plus lent que d'utiliser "\ n" . Si vous voulez toujours l'accélérer, écrivez vos données au format binaire. Ci-dessous est mon exemple de code complet qui comprend mes solutions proposées et une d'Edgar Rokyan. J'ai également inclus les suggestions de Ben Voigt et Matthieu M dans le code de test.

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

Voici les résultats de performance obtenus dans ma boîte Linux en utilisant les indicateurs clang-3.9.1 et -O3. J'utilise Celero pour collecter tous les résultats de performance.

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.

J'ai également testé des algorithmes de conversion numérique en chaîne pour comparer les performances de std :: stringstream, fmt :: MemoryWriter et 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.

Les tableaux ci-dessus montrent que:

  1. La solution Edgar Rokyan est 10% plus lente que la solution stringstream. La solution qui utilise la bibliothèque fmt est la meilleure pour trois types de données étudiés qui sont double, int et size_t. La solution sprintf + std :: vector est 1% plus rapide que la solution fmt pour le type de données double. Cependant, je ne recommande pas les solutions qui utilisent sprintf pour le code de production car elles ne sont pas élégantes (toujours écrites en style C) et ne fonctionnent pas prédéfinies pour différents types de données tels que int ou size_t.

  2. Les résultats de référence montrent également que fmt est la sérialisation du type de données intégral supérieur, car elle est au moins 7 fois plus rapide que les autres approches.

  3. Nous pouvons accélérer cet algorithme 10x si nous utilisons le format binaire. Cette approche est beaucoup plus rapide que d'écrire dans un fichier texte formaté car nous ne faisons qu'une copie brute de la mémoire vers la sortie. Si vous voulez des solutions plus flexibles et portables, essayez cereal ou boost :: serialization ou protocol-buffer . Selon cette étude de performance les céréales semblent être les plus rapides.

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

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

Tout ce que j'ai changé avait des performances théoriquement pires dans votre version du code, mais le std::endl Était le vrai tueur . std::vector::at (avec vérification des limites, dont vous n'avez pas besoin) serait le deuxième, puis le fait que vous n'utilisiez pas d'itérateurs.

Pourquoi construire par défaut un std::ofstream Puis appeler open, quand vous pouvez le faire en une seule étape? Pourquoi appeler close quand RAII (le destructeur) s'en occupe pour vous? Vous pouvez également appeler

fout << std::setprecision(10)

juste une fois, avant la boucle.

Comme indiqué dans le commentaire ci-dessous, si votre vecteur est d'éléments de type fondamental, vous pourriez obtenir de meilleures performances avec for(auto x : vector). Mesurer le temps de fonctionnement/inspecter la sortie de l'assemblage.


Juste pour souligner une autre chose qui a attiré mon attention, ceci:

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

Qu'est-ce que c'est l? Pourquoi le déclarer hors de la boucle? Il semble que vous n'en ayez pas besoin dans le cadre extérieur, alors ne le faites pas. Et aussi le post-incrémentation .

Le résultat:

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

Je suis désolé d'avoir révisé le code de ce post.

72
LogicStuff

Vous pouvez également utiliser une forme plutôt soignée de sortie du contenu de n'importe quel vector dans le fichier, à l'aide d'itérateurs et de la fonction copy.

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

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

Cette solution est pratiquement la même que celle de LogicStuff en termes de temps d'exécution. Mais il illustre également comment imprimer le contenu avec une seule fonction copy qui, comme je suppose, semble assez bien.

21
Edgar Rokjān

OK, je suis triste qu'il y ait trois solutions qui tentent de vous donner un poisson, mais aucune solution qui tente de vous apprendre à pêcher.

Lorsque vous rencontrez un problème de performances, la solution consiste à utiliser un profileur et à résoudre tout problème que le profileur affiche.

La conversion du double en chaîne pour 300 000 doubles ne prendra pas 3 minutes sur un ordinateur livré au cours des 10 dernières années.

L'écriture de 3 Mo de données sur le disque (une taille moyenne de 300 000 doublons) ne prendra pas 3 minutes sur un ordinateur livré au cours des 10 dernières années.

Si vous décrivez cela, je suppose que vous constaterez que le fout est vidangé 300 000 fois et que le vidage est lent, car cela peut impliquer le blocage ou le semi-blocage des E/S. Ainsi, vous devez éviter les E/S bloquantes. La façon typique de le faire est de préparer toutes vos E/S dans un seul tampon (créer un flux de chaînes, d'écrire dans celui-ci), puis d'écrire ce tampon dans un fichier physique en une seule fois. C'est la solution que hungptit décrit, sauf que je pense que ce qui manque explique pourquoi cette solution est une bonne solution.

Ou, pour le dire autrement: Ce que le profileur vous dira, c'est que l'appel de write () (sous Linux) ou WriteFile () (sous Windows) est beaucoup plus lent que de simplement copier quelques octets dans une mémoire tampon, car c'est un utilisateur/transition au niveau du noyau. Si std :: endl fait que cela se produise pour chaque double, vous allez avoir un mauvais moment (lent). Remplacez-le par quelque chose qui reste dans l'espace utilisateur et place les données dans la RAM!

Si ce n'est pas encore assez rapide, il se peut que la version à précision spécifique de l'opérateur << () sur les chaînes soit lente ou implique une surcharge inutile. Si c'est le cas, vous pourrez peut-être accélérer davantage le code en utilisant sprintf () ou une autre fonction potentiellement plus rapide pour générer des données dans le tampon en mémoire, avant de finalement écrire le tampon entier dans un fichier en une seule fois.

11
Jon Watte

Vous avez deux principaux goulots d'étranglement dans votre programme: la sortie et la mise en forme du texte.

Pour augmenter les performances, vous souhaiterez augmenter la quantité de données en sortie par appel. Par exemple, 1 transfert de sortie de 500 caractères est plus rapide que 500 transferts de 1 caractère.

Ma recommandation est de formater les données dans un grand tampon, puis de bloquer l'écriture du tampon.

Voici un exemple:

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

Vous devez d'abord essayer de modifier les paramètres d'optimisation dans votre compilateur avant de jouer avec le code.

5
Thomas Matthews

Voici une solution légèrement différente: enregistrez vos doubles sous forme binaire.

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

Puisque vous avez mentionné que vous disposez de 300k doubles, ce qui équivaut à 300k * 8 octets = 2,4 Mo, vous pouvez tous les enregistrer dans un fichier disque local en en moins de 0,1 seconde . Le seul inconvénient de cette méthode est que le fichier enregistré n'est pas aussi lisible que la représentation sous forme de chaîne, mais un HexEditor peut résoudre ce problème.

Si vous préférez une méthode plus robuste, de nombreuses bibliothèques/outils de sérialisation sont disponibles en ligne. Ils offrent plus d'avantages, tels qu'un algorithme de compression flexible, indépendant du langage, indépendant de la machine, etc. Ce sont les deux que j'utilise habituellement:

2
Jason L.