将数据从 std::vector 写入文本文件的快速方法
Fast way to write data from a std::vector to a text file
我目前将一组双打从矢量写入文本文件,如下所示:
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();
但这需要很长时间才能完成。有没有更快或更有效的方法来做到这一点?我很乐意看到并学习它。
std::ofstream fout("vector.txt");
fout << std::setprecision(10);
for(auto const& x : vector)
fout << x << '\n';
我更改的所有内容理论上在您的代码版本中性能更差,但 std::endl
was the real killer. std::vector::at
(具有边界检查,您不需要)将是第二个,那么您没有这样做的事实使用迭代器。
为什么默认构造一个 std::ofstream
然后调用 open
,当你可以一步完成时?当 RAII(析构函数)为您处理时,为什么要调用 close
?您也可以拨打
fout << std::setprecision(10)
只有一次,在循环之前。
如以下评论所述,如果您的矢量是基本类型的元素,则 for(auto x : vector)
可能会获得更好的性能。测量 运行 时间/检查程序集输出。
只是指出另一件引起我注意的事情,这个:
for(l = 0; l < vector.size(); l++)
这是什么l
?为什么要在循环外声明呢?看起来你在外部范围内不需要它,所以不要。还有 post-increment.
结果:
for(size_t l = 0; l < vector.size(); ++l)
我很抱歉为此进行代码审查 post。
您的程序有两个主要瓶颈:输出和格式化文本。
要提高性能,您需要增加每次调用的数据输出量。例如,500 个字符的 1 次输出传输比 1 个字符的 500 次传输更快。
我的建议是将数据格式化为大缓冲区,然后块写入缓冲区。
这是一个例子:
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);
您应该先尝试更改编译器中的优化设置,然后再修改代码。
在迭代器和 copy
函数的帮助下,您还可以使用一种相当简洁的形式将任何 vector
的内容输出到文件中。
std::ofstream fout("vector.txt");
fout.precision(10);
std::copy(numbers.begin(), numbers.end(),
std::ostream_iterator<double>(fout, "\n"));
这个解决方案在执行时间上与LogicStuff的解决方案几乎相同。但它也说明了如何仅使用单个 copy
函数打印内容,我想这看起来很不错。
这里有一个稍微不同的解决方案:以二进制形式保存你的双打。
int fd = ::open("/path/to/the/file", O_WRONLY /* whatever permission */);
::write(fd, &vector[0], vector.size() * sizeof(vector[0]));
既然你提到你有300k double,等于300k * 8 bytes = 2.4M,你可以在不到0.1秒的时间内将它们全部保存到本地磁盘文件.这种方法的唯一缺点是保存的文件不如字符串表示可读,但是 HexEditor 可以解决这个问题。
如果您更喜欢更健壮的方式,可以在线使用大量序列化 libraries/tools。它们提供了更多的好处,比如语言中立、机器无关、灵活的压缩算法等。这就是我经常使用的两个:
您的算法分为两部分:
将双数序列化为字符串或字符缓冲区。
将结果写入文件。
可以使用 sprintf 或 fmt. The second item can be sped up by caching results to a buffer or extending the output file stream buffer size before writing results to the output file. You should not use std::endl because it is much slower than using "\n" 改进第一项 (> 20%)。如果您仍然想让它更快,那么以二进制格式写入数据。下面是我的完整代码示例,其中包括我提出的解决方案和来自 Edgar Rokyan 的解决方案。我还在测试代码中包含了 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.
从上面的表格我们可以看出:
Edgar Rokyan 解决方案比 stringstream 解决方案慢 10%。 double 数据类型使用 fmt library is the best for three studied data types which are double, int, and size_t. sprintf + std::vector solution is 1% faster than the fmt 解决方案的解决方案。但是,我不推荐将 sprintf 用于生产代码的解决方案,因为它们不够优雅(仍然以 C 风格编写)并且不能开箱即用地处理不同的数据类型,例如 int 或 size_t.
基准测试结果还表明,fmt 是高级整数数据类型序列化,因为它至少比其他方法快 7 倍。
如果我们使用二进制格式,我们可以将这个算法的速度提高 10 倍。这种方法比写入格式化文本文件要快得多,因为我们只进行从内存到输出的原始复制。如果您想拥有更灵活和便携的解决方案,请尝试 cereal or boost::serialization or protocol-buffer. According to this performance study 谷物似乎是最快的。
好吧,我很难过,有三种解决方案试图给你一条鱼,但没有一种解决方案试图教你如何钓鱼。
当您遇到性能问题时,解决方案是使用分析器,并修复分析器显示的任何问题。
在过去 10 年推出的任何计算机上,将 300,000 个双精度数转换为字符串不会花费 3 分钟。
将 3 MB 数据写入磁盘(平均大小为 300,000 倍)在过去 10 年出货的任何计算机上都不会花费 3 分钟。
如果你分析这个,我猜你会发现 fout 被刷新了 300,000 次,而且刷新很慢,因为它可能涉及阻塞或半阻塞,I/O。因此,您需要避免阻塞 I/O。这样做的典型方法是将所有 I/O 准备到一个缓冲区(创建一个字符串流,写入该缓冲区),然后将该缓冲区一次性写入物理文件。这是 hungptit 描述的解决方案,但我认为缺少的是解释为什么该解决方案是一个好的解决方案。
或者,换句话说:探查器会告诉您调用 write()(在 Linux)或 WriteFile()(在 Windows)比仅调用慢得多将几个字节复制到内存缓冲区中,因为它是 user/kernel 级别转换。如果 std::endl 导致每个 double 都发生这种情况,那么您将度过一段糟糕(缓慢)的时光。将其替换为仅保留在用户 space 中并将数据放入 RAM 中的内容!
如果仍然不够快,可能是字符串上的特定精度版本的 operator<<() 速度慢或涉及不必要的开销。如果是这样,您可以使用 sprintf() 或其他一些可能更快的函数将数据生成到内存缓冲区中,从而进一步加快代码速度,然后再最终将整个缓冲区一次性写入文件。
我目前将一组双打从矢量写入文本文件,如下所示:
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();
但这需要很长时间才能完成。有没有更快或更有效的方法来做到这一点?我很乐意看到并学习它。
std::ofstream fout("vector.txt");
fout << std::setprecision(10);
for(auto const& x : vector)
fout << x << '\n';
我更改的所有内容理论上在您的代码版本中性能更差,但 std::endl
was the real killer. std::vector::at
(具有边界检查,您不需要)将是第二个,那么您没有这样做的事实使用迭代器。
为什么默认构造一个 std::ofstream
然后调用 open
,当你可以一步完成时?当 RAII(析构函数)为您处理时,为什么要调用 close
?您也可以拨打
fout << std::setprecision(10)
只有一次,在循环之前。
如以下评论所述,如果您的矢量是基本类型的元素,则 for(auto x : vector)
可能会获得更好的性能。测量 运行 时间/检查程序集输出。
只是指出另一件引起我注意的事情,这个:
for(l = 0; l < vector.size(); l++)
这是什么l
?为什么要在循环外声明呢?看起来你在外部范围内不需要它,所以不要。还有 post-increment.
结果:
for(size_t l = 0; l < vector.size(); ++l)
我很抱歉为此进行代码审查 post。
您的程序有两个主要瓶颈:输出和格式化文本。
要提高性能,您需要增加每次调用的数据输出量。例如,500 个字符的 1 次输出传输比 1 个字符的 500 次传输更快。
我的建议是将数据格式化为大缓冲区,然后块写入缓冲区。
这是一个例子:
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);
您应该先尝试更改编译器中的优化设置,然后再修改代码。
在迭代器和 copy
函数的帮助下,您还可以使用一种相当简洁的形式将任何 vector
的内容输出到文件中。
std::ofstream fout("vector.txt");
fout.precision(10);
std::copy(numbers.begin(), numbers.end(),
std::ostream_iterator<double>(fout, "\n"));
这个解决方案在执行时间上与LogicStuff的解决方案几乎相同。但它也说明了如何仅使用单个 copy
函数打印内容,我想这看起来很不错。
这里有一个稍微不同的解决方案:以二进制形式保存你的双打。
int fd = ::open("/path/to/the/file", O_WRONLY /* whatever permission */);
::write(fd, &vector[0], vector.size() * sizeof(vector[0]));
既然你提到你有300k double,等于300k * 8 bytes = 2.4M,你可以在不到0.1秒的时间内将它们全部保存到本地磁盘文件.这种方法的唯一缺点是保存的文件不如字符串表示可读,但是 HexEditor 可以解决这个问题。
如果您更喜欢更健壮的方式,可以在线使用大量序列化 libraries/tools。它们提供了更多的好处,比如语言中立、机器无关、灵活的压缩算法等。这就是我经常使用的两个:
您的算法分为两部分:
将双数序列化为字符串或字符缓冲区。
将结果写入文件。
可以使用 sprintf 或 fmt. The second item can be sped up by caching results to a buffer or extending the output file stream buffer size before writing results to the output file. You should not use std::endl because it is much slower than using "\n" 改进第一项 (> 20%)。如果您仍然想让它更快,那么以二进制格式写入数据。下面是我的完整代码示例,其中包括我提出的解决方案和来自 Edgar Rokyan 的解决方案。我还在测试代码中包含了 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.
从上面的表格我们可以看出:
Edgar Rokyan 解决方案比 stringstream 解决方案慢 10%。 double 数据类型使用 fmt library is the best for three studied data types which are double, int, and size_t. sprintf + std::vector solution is 1% faster than the fmt 解决方案的解决方案。但是,我不推荐将 sprintf 用于生产代码的解决方案,因为它们不够优雅(仍然以 C 风格编写)并且不能开箱即用地处理不同的数据类型,例如 int 或 size_t.
基准测试结果还表明,fmt 是高级整数数据类型序列化,因为它至少比其他方法快 7 倍。
如果我们使用二进制格式,我们可以将这个算法的速度提高 10 倍。这种方法比写入格式化文本文件要快得多,因为我们只进行从内存到输出的原始复制。如果您想拥有更灵活和便携的解决方案,请尝试 cereal or boost::serialization or protocol-buffer. According to this performance study 谷物似乎是最快的。
好吧,我很难过,有三种解决方案试图给你一条鱼,但没有一种解决方案试图教你如何钓鱼。
当您遇到性能问题时,解决方案是使用分析器,并修复分析器显示的任何问题。
在过去 10 年推出的任何计算机上,将 300,000 个双精度数转换为字符串不会花费 3 分钟。
将 3 MB 数据写入磁盘(平均大小为 300,000 倍)在过去 10 年出货的任何计算机上都不会花费 3 分钟。
如果你分析这个,我猜你会发现 fout 被刷新了 300,000 次,而且刷新很慢,因为它可能涉及阻塞或半阻塞,I/O。因此,您需要避免阻塞 I/O。这样做的典型方法是将所有 I/O 准备到一个缓冲区(创建一个字符串流,写入该缓冲区),然后将该缓冲区一次性写入物理文件。这是 hungptit 描述的解决方案,但我认为缺少的是解释为什么该解决方案是一个好的解决方案。
或者,换句话说:探查器会告诉您调用 write()(在 Linux)或 WriteFile()(在 Windows)比仅调用慢得多将几个字节复制到内存缓冲区中,因为它是 user/kernel 级别转换。如果 std::endl 导致每个 double 都发生这种情况,那么您将度过一段糟糕(缓慢)的时光。将其替换为仅保留在用户 space 中并将数据放入 RAM 中的内容!
如果仍然不够快,可能是字符串上的特定精度版本的 operator<<() 速度慢或涉及不必要的开销。如果是这样,您可以使用 sprintf() 或其他一些可能更快的函数将数据生成到内存缓冲区中,从而进一步加快代码速度,然后再最终将整个缓冲区一次性写入文件。