Быстрый способ записи данных из std::vector в текстовый файл

В настоящее время я пишу набор удвоений из вектора в текстовый файл следующим образом:

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

Но это занимает много времени, чтобы закончить. Есть ли более быстрый или более эффективный способ сделать это? Мне бы хотелось увидеть и изучить его.

Ответ 1

Ваш алгоритм состоит из двух частей:

  • Сериализуйте двойные числа в строковый или символьный буфер.

  • Запись результатов в файл.

Первый элемент можно улучшить ( > 20%) с помощью sprintf или fmt. Второй элемент может ускоряться путем кэширования результатов в буфер или расширения размера буфера потока выходных файлов, прежде чем записывать результаты в выходной файл. Вы не должны использовать std:: endl, потому что он намного медленнее, чем использование "\n" . Если вы все еще хотите сделать это быстрее, тогда напишите свои данные в двоичном формате. Ниже приведен мой полный образец кода, который включает в себя мои предлагаемые решения и один от Эдгара Рокьяна. Я также включил предложения Ben Voigt и 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()));
}

Ниже приведены результаты производительности, полученные в моем ящике Linux с использованием флагов clang-3.9.1 и -O3. Я использую Celero, чтобы собрать все результаты производительности.

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.

Я также сравниваю числовые алгоритмы преобразования строк, чтобы сравнить производительность std:: stringstream, fmt:: MemoryWriter и 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.

Из приведенных выше таблиц видно, что:

  • Решение Эдгара Рокьяна на 10% медленнее, чем решение для струнного потока. Решение, использующее библиотеку fmt, является лучшим для трех изученных типов данных, которые являются double, int и size_t. Решение sprintf + std::vector на 1% быстрее, чем решение fmt для двойного типа данных. Тем не менее, я не рекомендую решения, которые используют sprintf для производственного кода, потому что они не изящны (все еще написаны в стиле C) и не работают из коробки для разных типов данных, таких как int или size_t.

  • Результаты тестов также показывают, что fmt - это сериализация типа интегрального интегратора, поскольку она не менее чем на 7 раз быстрее, чем другие подходы.

  • Мы можем ускорить этот алгоритм 10x, если мы используем двоичный формат. Этот подход значительно быстрее, чем запись в форматированный текстовый файл, потому что мы делаем только исходную копию из памяти на выходе. Если вы хотите иметь более гибкие и портативные решения, попробуйте cereal или boost:: serialization или protocol-buffer. Согласно это исследование эффективности, зерно кажется самым быстрым.

Ответ 2

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

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

Все, что я изменил, имело теоретически худшую производительность в вашей версии кода, но std::endl был настоящим убийцей. std::vector::at (с проверкой границ, которая вам не нужна) была бы второй, а затем фактом, что вы не использовали итераторы.

Почему default-construct a std::ofstream, а затем вызывается open, когда вы можете сделать это за один шаг? Зачем звонить close, когда RAII (деструктор) заботится о нем для вас? Вы также можете позвонить

fout << std::setprecision(10)

только один раз, перед циклом.

Как отмечено в комментарии ниже, если ваш вектор имеет элементы фундаментального типа, вы можете получить лучшую производительность с помощью for(auto x : vector). Измерьте время работы/проверьте выход сборки.


Чтобы указать на другое, что поймало мои глаза, это:

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

Что это за l? Зачем объявлять его за пределами цикла? Кажется, вам это не нужно во внешнем масштабе, так что нет. А также пост-инкремент.

Результат:

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

Прошу прощения за отзыв кода из этой публикации.

Ответ 3

Вы также можете использовать довольно аккуратную форму вывода содержимого любого vector в файл с помощью итераторов и copy.

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

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

Эти решения практически совпадают с решением LogicStuff с точки зрения времени выполнения. Но это также иллюстрирует, как печатать содержимое только с помощью одной функции copy, которая, как я полагаю, выглядит довольно хорошо.

Ответ 4

Хорошо, мне грустно, что есть три решения, которые пытаются дать вам рыбу, но не решение, которое пытается научить вас, как ловить рыбу.

При возникновении проблемы с производительностью решение заключается в использовании профайлера и исправлении любой проблемы, которую показывает профайлер.

Преобразование double-to-string для 300 000 двухлокальных номеров не займет 3 минуты на любом компьютере, который отправлен за последние 10 лет.

Написание 3 МБ данных на диск (средний размер 300 000 удваивается) не займет 3 минуты на любом компьютере, который отправлен за последние 10 лет.

Если вы прокомментируете это, я предполагаю, что вы обнаружите, что fout получает покраснение 300 000 раз, и эта промывка идет медленно, потому что это может быть связано с блокировкой или полублокировкой ввода-вывода. Таким образом, вам необходимо избегать блокировки ввода-вывода. Типичный способ сделать это - подготовить все ваши операции ввода-вывода к одному буферу (создать строковый поток, записать на него), а затем записать этот буфер в физический файл за один раз. Это решение hungptit описывает, за исключением того, что я думаю, что недостающее объясняет, почему это решение является хорошим решением.

Или, скажем так: то, что профайлер вам скажет, заключается в том, что вызов write() (в Linux) или WriteFile() (в Windows) намного медленнее, чем просто копирование нескольких байтов в буфер памяти, потому что это переход на уровне пользователя/ядра. Если std:: endl вызывает это для каждого двойника, у вас будет плохое (медленное) время. Замените его чем-то, что просто остается в пространстве пользователя и помещает данные в ОЗУ!

Если это еще не достаточно быстро, может оказаться, что версия оператора < <() для строки с заданной точностью медленна или включает ненужные служебные данные. Если это так, вы можете еще больше ускорить работу кода, используя sprintf() или другую потенциально более быструю функцию для генерации данных в буфере в памяти, прежде чем вы, наконец, напишите весь буфер в файл за один раз.

Ответ 5

У вас есть два основных узких места в вашей программе: вывод и форматирование текста.

Чтобы повысить производительность, вы захотите увеличить объем вывода данных за один вызов. Например, 1 выходная передача 500 символов быстрее 500 передач 1 символа.

Моя рекомендация заключается в том, что вы форматируете данные в большой буфер, затем блокируете запись буфера.

Вот пример:

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

Сначала вы должны попробовать изменить параметры оптимизации в своем компиляторе, прежде чем возиться с кодом.

Ответ 6

Вот несколько другое решение: сохраните двойники в двоичной форме.

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

Поскольку вы упомянули, что у вас есть 300k удваивается, что равно 300k * 8 байт = 2.4M, вы можете сохранить их все в локальном диске в менее 0,1 секунды. Единственный недостаток этого метода - сохраненный файл не так читается, как строковое представление, но HexEditor может решить эту проблему.

Если вы предпочитаете более надежный способ, в сети доступно множество библиотек/инструментов для сериализации. Они обеспечивают больше преимуществ, таких как нейтральный по отношению к языку, независимый от машины, гибкий алгоритм сжатия и т.д. Это те два, которые я обычно использую: