C++ STL 容器用作简单缓冲区时速度很慢(我做了一个基准测试)
C++ STL containers are slow when used as simple buffers (I did a benchmark)?
我一直认为C++代码在效率方面通常与 C 代码相当,如果不是更好的话(例如:std::sort 由于比较器内联而击败 qsort)。人们也普遍认为,如果需要动态内存缓冲区,std::vector 将是一个合理的首选。
最近,我实施了一个基准测试来获取硬数据,并得到了令人惊讶的结果。基本上,在紧密循环中附加到向量(或字符串)相对较慢。
如果可以使其更快,请启发我!
有关基准测试的血腥细节,请参见下文。
当然,一种选择是使用自定义容器。出于显而易见的原因(代码的一致性加上在同一事物的不兼容表示之间进行转换的开销,例如 std::string 和 custom_string),我迫切希望尽可能长时间地避免这条路线。
关于基准。
有一个输入缓冲区和输出缓冲区。如果输入字节通过验证,则按原样传输到输出,对其进行编码并传输生成的 3 个字节(此处进行某种转义)。正在实现的算法是 UTF-8 验证。
基本上这是代码:
// Templated Sink allows us to play with different methods for building
// the output to estimate the relative efficiency of various approaches
// (ex: a large buffer with no bounds checking vs. std::string).
template <typename Sink>
const unsigned char *
fix_utf8_engine(Sink &sink,
const unsigned char *i, const unsigned char *end)
{
while (i < end) {
// for sinks with limited capacity
if (!sink.check_capacity())
return i;
switch (i[0]) {
case 0x00 ... 0x7f:
// 1-byte UTF-8 sequence
sink.template write<1>(i);
i += 1;
continue;
...
我已经实现了fix_utf8的不同变体,包括写入预分配的"无限"缓冲区(无边界检查或增长,基线),生成动态增长的malloc-ed缓冲区(malloc),产生std::string(字符串)和生成std::vector(矢量)的
。以下是结果(Ivy Bridge Core i7笔记本电脑,clang-600.0.56 -O3)(基于LLVM 3.5svn)):
ASCII Unicode Unicode Unicode Unicode Unicode Random 小全恶 混合 恶 短 恶 长基线 0.01307 0.01816 0.01912 0.01909 0.03104 0.03781 0.06127Malloc 0.01798 0.02068 0.02116 0.02095 0.03918 0.04684 0.06909字符串 0.02791 0.03045 0.02575 0.02520 0.07871 0.11513 0.09580矢量 0.06210 0.04925 0.04017 0.04027 0.10103 0.15159 0.12871
不同的列用于不同类型的随机生成的输入(Unicode 小 - 有限范围,最多 2 个字节的结果 UTF-8,邪恶混合 - 各种损坏的 UTF-8 数据穿插正常数据,邪恶的短/长 - 所有 UTF-8 序列都被截断 1 个字节)。
这些结果清楚地表明,字符串和向量变体在至少两个非常重要的用例上要慢得多 - ASCII和Unicode(小)。
任何改善这些结果的帮助都非常感谢!
是的,你是对的。 像这样使用向量或字符串确实很慢。 基本上,每次追加到容器末尾时,都可能要求容器重新分配新缓冲区,将数据复制到新缓冲区,然后删除旧缓冲区。
您可以通过两种方式改善结果。 首先,如果您知道大小,则可以使用reserve()
方法。 其次,如果你使用的是向量,你可以切换到deque
,当我对我想要的大小感觉不好时,这就是我使用的。 在此用例中,这将更好地工作。 这是因为 deque 不会重新分配内存 - 它会按页数优雅地增长。 对于这种灵活性(一个间接级别),在访问方面有一个权衡,但由于您现在只是插入,因此它应该不适用。
一条评论声称您保留了足够的资金,但事实并非如此。 你可以写的比你读的多。我仍然会使用与 check_capacity() 中的 malloc 策略类似的增长策略。 这将为您提供最好的苹果与苹果的比较。
这是一个模板版本,它应该正确实现 check_capacity() 并适用于所有 STL 容器(包括 deque)。 使用它代替std_vector_sink和std_string_sink。
// Write output to a container
template < typename CONTAINER >
struct std_sink
{
CONTAINER &v_;
std_sink(CONTAINER &v): v_(v) {}
bool check_capacity() {
if (v_.capacity() - v_.size() <= 8) {
v_.reserve(v_.size() + (v_.size() / 2));
}
return true;
}
template<size_t n> void write(const unsigned char *p) {
v_.insert(v_.end(), p, p+n);
}
void write_bad(const unsigned char *p) {
unsigned char esc[] = {
utf8b_1(*p),
utf8b_2(*p),
utf8b_3(*p)
};
v_.insert(v_.end(), esc, esc + 3);
}
};
另外,尝试使用deque:
std_sink< std::deque< unsigned char> > sink(result);
当您了解如何使用它们时,它们会很快。 我的猜测是你不断地调用vector.push_back(value),每次用完内存时都会重新分配一个新的缓冲区(通常每次达到限制时,它的分配都会加倍)。 你想先做一个vector.reserve(reasonableCapacity)。
在我的特定环境中,以下内容有所帮助。
使用 std::string 的原始代码速度很慢,因为许多代码路径以不同的 insert() 调用结尾。插入部分内联导致大量代码膨胀(尽管根本不内联会更糟)。
有效的解决方案是放弃 insert() 并使用指针渲染输出。一旦输出指针赶上输出缓冲区结束指针,我们确定字符串内的输出偏移量,将字符串大小增加固定数量并更新指针。
std::string output;
char *opos; // output pos
char *end_obuf;
while (...) {
if (opos + 6 > end_obuf) { // 6 bytes produced per iteration or less
output.resize(s.size() + 128);
// reinit opos, end_obuf pointers
...
}
// processing
...
}
重要的是永远不要写超过字符串末尾,因为没有合法的方法使该数据成为字符串的一部分(一旦字符串增长,超过末尾的数据就会被删除,至少使用默认分配器)。
因此,我们实际上在这里调整字符串的大小(故意使用 resize() 而不是 reserve())。
按固定量增长是可以的;实际上,当内容线性增长时,标准要求缓冲区呈指数级增长。特定的数量(128)是一种权衡:增长太多是不好的,因为我们要零初始化那么多字节。增长太少也是不好的,因为我们不想太频繁地执行调整大小代码。
总而言之,对 insert() 的许多调用被替换为对 resize() 的单个调用,使生成的代码更加精简。调整大小分支不经常执行,因此在多个循环迭代中分摊成本。
几句结束语:
将 STL 分配器与 realloc 进行比较确实是不公平的。Realloc 通常可以在不复制的情况下调整缓冲区的大小。 由于 OOP 问题,STL 分配器总是制作一个完整的副本。
功能请求:如果 STL 默认对 POD 类型使用 realloc 会很好。
"悲观化"的malloc使结果几乎相同(大约20%的性能原始差异是出于这个原因)。
有趣的是,字符串缓冲区的动态重新分配是否施加了那么多的性能开销。过度预留加上悲观的malloc不足以缩小差距。在这个特定的问题中,数据处理速度更为重要(注意:这里的字符串大小为8MiB,字符串越小,结果可能会改变。
按配置文件优化(-fprofile-generate/-fprofile-use)允许持续获得另外5-10%,非常感谢此选项。
- 使用rdtsc进行基准测试的缺点是什么
- 对 'std::thread::_M_start_thread CMake 的未定义引用进行基准测试
- 更高效地在微控制器上对C++进行基准测试
- _mm256_load_ps调试模式下导致谷歌/基准测试的分段错误
- 二叉树基准测试结果
- 如何使用谷歌基准测试对自定义界面进行基准测试
- 谷歌基准测试,如何只调用一次代码?
- 使用 std::chrono::steady_clock 对线程/异步中的代码进行基准测试
- 谷歌基准测试结果中显示的时间没有意义
- 使用 Google 基准测试时返回值会发生什么情况?
- 如何在Qt测试框架中对信号进行基准测试?
- C/C++memcpu基准测试:测量CPU和墙时间
- 如何将参数传递给Google基准测试程序
- C++多态性:如何测试一个类是否派生自另一个基类
- 如何对CUDA项目进行基准测试
- 为什么这个简单的 C++ SIMD 基准测试在使用 SIMD 指令时运行速度较慢?
- 多部分基准测试的权重是多少?
- 为了准确地进行基准测试,我应该制作一个大函数原子吗
- C++ STL 容器用作简单缓冲区时速度很慢(我做了一个基准测试)
- 基准测试一个纯C++函数