55 votes

Une façon rapide d'écrire les données d'un std::vector vers un fichier texte

Actuellement, j'écris un ensemble de doubles d'un vecteur vers un fichier texte comme ceci :

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 à terminer. Existe-t-il un moyen plus rapide ou plus efficace de le faire ? J'aimerais bien la voir et l'apprendre.

72voto

LogicStuff Points 10924
std::ofstream fout("vector.txt");
fout << std::setprecision(10);

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

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

Pourquoi construire par défaut un std::ofstream et ensuite appeler open alors que 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 composé d'éléments de type fondamental, vous pourriez obtenir de meilleures performances avec for(auto x : vector) . Mesurez le temps de fonctionnement / contrôlez la sortie de l'assemblage.


Juste pour signaler 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 en dehors de la boucle ? Il semble que vous n'en ayez pas besoin dans la portée externe, alors ne le déclarez pas. Et aussi le post-incrément .

Le résultat :

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

Je suis désolé d'avoir fait une revue de code de ce post.

34voto

hungptit Points 762

Votre algorithme comporte deux parties :

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

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

Le premier point 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 du 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 il est beaucoup plus lent que l'utilisation de " \n " . Si vous voulez encore accélérer le processus, écrivez vos données au format binaire. Vous trouverez ci-dessous mon exemple de code complet qui inclut mes solutions proposées et celle 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 clang-3.9.1 et le drapeau -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.

Je fais également un benchmark des algorithmes de conversion numérique-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.

D'après les tableaux ci-dessus, nous pouvons voir que :

  1. La solution d'Edgar Rokyan est 10% plus lente que la solution stringstream. La solution qui utilise fmt est la meilleure pour les trois types de données étudiés, à savoir double, int et size_t. La solution sprintf + std::vector est 1% plus rapide que la solution fmt pour les données de type 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 dans le style C) et ne fonctionnent pas d'emblée pour différents types de données tels que int ou size_t.

  2. Les résultats de l'évaluation comparative montrent également que fmt est le superrior de la sérialisation intégrale des types de données puisqu'il est au moins 7x plus rapide que les autres approches.

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

21voto

Edgar Rokyan Points 1335

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

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 elle illustre également la manière d'imprimer le contenu avec une seule commande copy qui, comme je le suppose, se présente plutôt bien.

12voto

Jon Watte Points 2065

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 avez un problème de performance, la solution consiste à utiliser un profileur et à corriger le problème que le profileur révèle.

La conversion de double en chaîne de caractères pour 300 000 doubles ne prendra pas 3 minutes sur n'importe quel 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 doubles) ne prendra pas 3 minutes sur n'importe quel ordinateur livré au cours des 10 dernières années.

Si vous établissez ce profil, je pense que vous constaterez que fout est vidé 300 000 fois, et que la vidange est lente, car elle peut impliquer des E/S bloquantes ou semi-bloquantes. Vous devez donc éviter les E/S bloquantes. La façon typique de faire cela est de préparer toutes vos E/S vers un seul tampon (créer un stringstream, écrire dans celui-ci) et ensuite écrire ce tampon dans un fichier physique en une seule fois. C'est la solution décrite par hungptit, sauf que je pense que ce qui manque est d'expliquer POURQUOI cette solution est une bonne solution.

Ou, pour le dire autrement : Ce que le profileur vous dira, c'est que l'appel à write() (sous Linux) ou WriteFile() (sous Windows) est beaucoup plus lent que la simple copie de quelques octets dans un tampon mémoire, car il s'agit d'une transition entre le niveau utilisateur et le niveau noyau. Si std::endl provoque cette transition pour chaque double, vous allez passer un mauvais moment (lent). Remplacez-le par quelque chose qui reste simplement dans l'espace utilisateur et met les données dans la RAM !

Si cela n'est toujours pas assez rapide, il se peut que la version à précision spécifique de l'opérateur<<() sur les chaînes de caractères soit lente ou implique une surcharge inutile. Si c'est le cas, vous pouvez accélérer 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 d'écrire l'intégralité du tampon dans un fichier en une seule fois.

5voto

Thomas Matthews Points 19838

Votre programme comporte deux principaux goulets d'étranglement : l'édition et le formatage du texte.

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

Je vous recommande de formater les données dans un grand tampon, puis d'écrire en bloc dans le 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 modifier le code.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X