c++ - sirve - Manera rápida de escribir datos desde un std:: vector a un archivo de texto
manejo de archivos en c++ fstream (6)
Aquí hay una solución ligeramente diferente: guarde sus dobles en forma binaria.
int fd = ::open("/path/to/the/file", O_WRONLY /* whatever permission */);
::write(fd, &vector[0], vector.size() * sizeof(vector[0]));
Como mencionó que tiene 300k dobles, lo que equivale a 300k * 8 bytes = 2.4M, puede guardarlos en un archivo de disco local en menos de 0.1 segundo . El único inconveniente de este método es que el archivo guardado no es tan legible como la representación de cadena, pero un HexEditor puede resolver ese problema.
Si prefiere una forma más robusta, hay muchas bibliotecas / herramientas de serialización disponibles en línea. Proporcionan más beneficios, como un algoritmo de compresión flexible, independiente del lenguaje, independiente de la máquina, etc. Esos son los dos que suelo usar:
Actualmente escribo un conjunto de dobles de un vector a un archivo de texto como este:
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();
Pero esto está tomando mucho tiempo para terminar. ¿Hay una manera más rápida o más eficiente de hacer esto? Me encantaría verlo y aprenderlo.
De acuerdo, me entristece que haya tres soluciones que intenten darte un pez, pero ninguna solución que intente enseñarte a pescar.
Cuando tiene un problema de rendimiento, la solución es utilizar un generador de perfiles y solucionar cualquier problema que muestre.
La conversión de doble cadena a 300,000 dobles no tomará 3 minutos en cualquier computadora que se haya enviado en los últimos 10 años.
Escribir 3 MB de datos en el disco (un tamaño promedio de 300,000 dobles) no tomará 3 minutos en cualquier computadora que se haya enviado en los últimos 10 años.
Si hace un perfil de esto, supongo que encontrará que la descarga se enjuaga 300,000 veces, y que el enjuague es lento, porque puede involucrar el bloqueo o semi-bloqueo de E / S. Por lo tanto, debe evitar el bloqueo de E / S. La forma típica de hacerlo es preparar todas sus E / S en un único búfer (crear un flujo de cadena, escribir en eso) y luego escribir ese búfer en un archivo físico de una sola vez. Esta es la solución que describe hungptit, excepto que creo que lo que falta es explicar POR QUÉ esa solución es una buena solución.
O, para decirlo de otra manera: lo que le dirá el generador de perfiles es que llamar a write () (en Linux) o WriteFile () (en Windows) es mucho más lento que simplemente copiar unos pocos bytes en un búfer de memoria, porque es un usuario / transición de nivel de kernel. Si std :: endl hace que esto suceda para cada doble, tendrás un mal tiempo (lento). ¡Reemplácelo con algo que simplemente permanezca en el espacio del usuario y ponga datos en la RAM!
Si todavía no es lo suficientemente rápido, puede ser que la versión de precisión específica del operador << () en las cadenas sea lenta o implique una sobrecarga innecesaria. Si es así, puede acelerar aún más el código utilizando sprintf () o alguna otra función potencialmente más rápida para generar datos en el búfer en memoria, antes de finalmente escribir todo el búfer en un archivo de una sola vez.
Su algoritmo tiene dos partes:
-
Serializar números dobles a una cadena o buffer de caracteres.
-
Escribir resultados en un archivo.
El primer elemento se puede mejorar (> 20%) usando sprintf o fmt . El segundo elemento puede acelerarse almacenando en caché los resultados en un búfer o extendiendo el tamaño del búfer de la secuencia del archivo de salida antes de escribir los resultados en el archivo de salida. No debe usar std :: endl porque es mucho más lento que usar "/ n" . Si todavía quiere hacerlo más rápido, escriba sus datos en formato binario. A continuación se muestra mi código de muestra completo que incluye mis soluciones propuestas y una de Edgar Rokyan. También incluí sugerencias de Ben Voigt y Matthieu M en el código de prueba.
#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()));
}
A continuación se muestran los resultados de rendimiento obtenidos en mi cuadro de Linux usando el indicador clang-3.9.1 y -O3. Yo uso Celero para recopilar todos los resultados de rendimiento.
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.
También comparo los algoritmos de conversión de números a cadenas para comparar el rendimiento de std :: stringstream, fmt :: MemoryWriter y 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.
De las tablas anteriores podemos ver que:
-
La solución de Edgar Rokyan es un 10% más lenta que la solución de flujo de cadena. La solución que usa la biblioteca fmt es la mejor para tres tipos de datos estudiados que son double, int y size_t. La solución sprintf + std :: vector es 1% más rápida que la solución fmt para el tipo de datos doble. Sin embargo, no recomiendo soluciones que usen sprintf para el código de producción porque no son elegantes (todavía están escritas en estilo C) y no funcionan de manera predeterminada para diferentes tipos de datos como int o size_t.
-
Los resultados de referencia también muestran que fmt es la serialización de tipo de datos integral superior ya que es al menos 7 veces más rápido que otros enfoques.
-
Podemos acelerar este algoritmo 10 veces si usamos el formato binario. Este enfoque es significativamente más rápido que escribir en un archivo de texto formateado porque solo hacemos una copia en bruto de la memoria a la salida. Si desea tener soluciones más flexibles y portátiles, pruebe cereal o boost::serialization o protocol-buffer . Según este estudio de rendimiento, el cereal parece ser el más rápido.
También puede usar una forma bastante ordenada de salida de contenido de cualquier
vector
en el archivo, con la ayuda de iteradores y la función de
copy
.
std::ofstream fout("vector.txt");
fout.precision(10);
std::copy(numbers.begin(), numbers.end(),
std::ostream_iterator<double>(fout, "/n"));
Esta solución es prácticamente la misma con la solución de LogicStuff en términos de tiempo de ejecución.
Pero también ilustra cómo imprimir los contenidos con una sola función de
copy
que, como supongo, se ve bastante bien.
Tiene dos cuellos de botella principales en su programa: salida y formato de texto.
Para aumentar el rendimiento, querrá aumentar la cantidad de salida de datos por llamada. Por ejemplo, 1 transferencia de salida de 500 caracteres es más rápida que 500 transferencias de 1 carácter.
Mi recomendación es que formatee los datos en un búfer grande, luego bloquee la escritura del búfer.
Aquí hay un ejemplo:
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);
Primero debe intentar cambiar la configuración de optimización en su compilador antes de jugar con el código.
std::ofstream fout("vector.txt");
fout << std::setprecision(10);
for(auto const& x : vector)
fout << x << ''/n'';
Todo lo que cambié tenía un rendimiento teóricamente peor en su versión del código, pero
std::endl
fue el verdadero asesino
.
std::vector::at
(con comprobación de límites, que no necesita) sería el segundo, luego el hecho de que no utilizó iteradores.
¿Por qué construir por defecto un
std::ofstream
y luego llamar a
open
, cuando puedes hacerlo en un solo paso?
¿Por qué llamar
close
cuando RAII (el destructor) se encarga de ti?
Tambien puedes llamar
fout << std::setprecision(10)
solo una vez, antes del bucle.
Como se señala en el comentario a continuación, si su vector es de elementos de tipo fundamental, es posible que obtenga un mejor rendimiento con
for(auto x : vector)
.
Mida el tiempo de funcionamiento / inspeccione la salida del ensamblaje.
Solo para señalar otra cosa que me llamó la atención, esto:
for(l = 0; l < vector.size(); l++)
¿Qué es esto? ¿Por qué declararlo fuera del circuito? Parece que no lo necesitas en el ámbito externo, así que no lo necesites. Y también el post-increment .
El resultado:
for(size_t l = 0; l < vector.size(); ++l)
Lo siento por hacer una revisión del código de esta publicación.