使用 vector<char> 作为缓冲区而不在 resize() 上初始化它
Using vector<char> as a buffer without initializing it on resize()
我想使用vector<char>
作为缓冲区。这个接口非常适合我的需要,但是当调整它的大小超过当前大小时,会有性能损失,因为内存是初始化的。我不需要初始化,因为数据在任何情况下都会被一些第三方C函数覆盖。是否有一种方法或特定的分配器来避免初始化步骤?请注意,我确实想使用resize()
,而不是像reserve()
和capacity()
这样的其他技巧,因为我需要size()
在任何时刻始终表示我的"缓冲区"的有效大小,而capacity()
可能在resize()
之后大于其大小,因此,再次,我不能依赖capacity()
作为我的应用程序的有效信息。此外,vector的(新)大小是事先不知道的,所以我不能使用std::array
。如果vector不能以这种方式配置,我想知道我可以使用哪种容器或分配器来代替vector<char, std::alloc>
。唯一的要求是,vector的替代品必须最多基于STL或Boost。我可以使用c++ 11。
这是一个已知的问题,即使显式地关闭std::vector
的初始化。
人们通常实现自己的pod_vector<>
,不做任何元素的初始化。
另一种方法是创建一个与char布局兼容的类型,其构造函数不做任何事情:
struct NoInitChar
{
char value;
NoInitChar() noexcept {
// do nothing
static_assert(sizeof *this == sizeof value, "invalid size");
static_assert(__alignof *this == __alignof value, "invalid alignment");
}
};
int main() {
std::vector<NoInitChar> v;
v.resize(10); // calls NoInitChar() which does not initialize
// Look ma, no reinterpret_cast<>!
char* beg = &v.front().value;
char* end = beg + v.size();
}
标准库中没有满足您要求的东西,我也不知道boost中有什么。
我能想到三个合理的选择:
- 现在坚持使用
std::vector
,在代码中留下注释,如果这导致了应用程序的瓶颈,请回到它。 - 使用带有空
construct
/destroy
方法的自定义分配器-并希望您的优化器足够聪明以删除对它们的任何调用。 - 为动态分配的数组创建包装器,仅实现所需的最小功能。
作为与不同pod类型的矢量一起工作的替代解决方案:
template<typename V>
void resize(V& v, size_t newSize)
{
struct vt { typename V::value_type v; vt() {}};
static_assert(sizeof(vt[10]) == sizeof(typename V::value_type[10]), "alignment error");
typedef std::vector<vt, typename std::allocator_traits<typename V::allocator_type>::template rebind_alloc<vt>> V2;
reinterpret_cast<V2&>(v).resize(newSize);
}
然后你可以:
std::vector<char> v;
resize(v, 1000); // instead of v.resize(1000);
这很可能是UB,尽管对于我更关心性能的情况,它对我来说工作得很好。clang生成的程序集的差异:
test():
push rbx
mov edi, 1000
call operator new(unsigned long)
mov rbx, rax
mov edx, 1000
mov rdi, rax
xor esi, esi
call memset
mov rdi, rbx
pop rbx
jmp operator delete(void*)
test_noinit():
push rax
mov edi, 1000
call operator new(unsigned long)
mov rdi, rax
pop rax
jmp operator delete(void*)
所以总结一下在stackoverflow上找到的各种解决方案:
- 使用一个特殊的默认初始化分配器。(https://stackoverflow.com/a/21028912/1984766)
缺点:将矢量类型更改为std::vector<char, default_init_allocator<char>> vec;
- 在具有空构造函数的char周围使用包装结构
struct NoInitChar
,从而跳过值初始化(https://stackoverflow.com/a/15220853/1984766)
缺点:将矢量类型更改为std::vector<NoInitChar> vec;
- 暂时将
vector<char>
转换为vector<NoInitChar>
并调整其大小(https://stackoverflow.com/a/57053750/1984766)
缺点:不改变向量的类型,但你需要调用your_resize_function (vec, x)
而不是vec.resize (x)
。
但是——>自从方法1 &2 .改变向量的类型,当你在更"复杂"的情况下使用这些向量时会发生什么?考虑这个例子:
#include <time.h>
#include <vector>
#include <string_view>
#include <iostream>
//high precision-timer
double get_time () {
struct timespec timespec;
::clock_gettime (CLOCK_MONOTONIC_RAW, ×pec);
return timespec.tv_sec + timespec.tv_nsec / (1000.0 * 1000.0 * 1000.0);
}
//method 1 --> special allocator
//reformated to make it readable
template <typename T, typename A = std::allocator<T>>
class default_init_allocator : public A {
private:
typedef std::allocator_traits<A> a_t;
public:
template<typename U>
struct rebind {
using other = default_init_allocator<U, typename a_t::template rebind_alloc<U>>;
};
using A::A;
template <typename U>
void construct (U* ptr) noexcept (std::is_nothrow_default_constructible<U>::value) {
::new (static_cast<void*>(ptr)) U;
}
template <typename U, typename...Args>
void construct (U* ptr, Args&&... args) {
a_t::construct (static_cast<A&>(*this), ptr, std::forward<Args>(args)...);
}
};
//method 2 --> wrapper struct
struct NoInitChar {
public:
NoInitChar () noexcept { }
NoInitChar (char c) noexcept : value (c) { }
public:
char value;
};
//some work to waste time
template<typename T>
void do_something (T& vec, std::string_view str) {
vec.push_back ('"');
vec.insert (vec.end (), str.begin (), str.end ());
vec.push_back ('"');
vec.push_back (',');
}
int main (int argc, char** argv) {
double timeBegin = get_time ();
std::vector<char> vec; //normal case
//std::vector<char, default_init_allocator<char>> vec; //method 1
//std::vector<NoInitChar> vec; //method 2
vec.reserve (256 * 1024 * 1024);
for (int i = 0; i < 1024 * 1024; ++i) {
do_something (vec, "foobar1");
do_something (vec, "foobar2");
do_something (vec, "foobar3");
do_something (vec, "foobar4");
do_something (vec, "foobar5");
do_something (vec, "foobar6");
do_something (vec, "foobar7");
do_something (vec, "foobar8");
vec.resize (vec.size () + 64);
}
double timeEnd = get_time ();
std::cout << (timeEnd - timeBegin) * 1000 << "ms" << std::endl;
return 0;
}
你会期望方法1 &2优于法向量与每一个"最近"的编译器,因为调整大小是自由的,其他操作是相同的。我们再考虑一下:
g++ 7.5.0 g++ 8.4.0 g++ 9.3.0 clang++ 9.0.0
vector<char> 95ms 134ms 133ms 97ms
method 1 130ms 159ms 166ms 91ms
method 2 166ms 160ms 159ms 89ms
所有的测试应用程序都是这样编译的,并以最低的测量值执行50次:
$(cc) -O3 -flto -std=c++17 sample.cpp
封装
初始化为最大大小(不保留)。
保持对迭代器的引用,表示实际大小的末尾。
使用begin
和real end
,而不是end
,为您的算法
你很少需要这样做;我强烈建议您对您的情况进行基准测试,以绝对确定需要这个hack。
即便如此,我还是更喜欢NoInitChar解决方案。(见《Maxim》的回答)
但是如果你确信你会从中受益,并且NoInitChar不适合你,并且你正在使用clang, gcc或MSVC作为编译器,那么考虑使用folly的例程来实现此目的。
见https://github.com/facebook/folly/blob/master/folly/memory/UninitializedMemoryHacks.h
基本思想是每个库实现都有一个未初始化调整大小的例程;你只需要调用它。
虽然有点黑客,但你至少可以安慰自己,因为facebook的c++代码依赖于这个黑客的正常工作,所以如果这些库实现的新版本需要它,他们会更新它。
- 是否可以初始化不可复制类型的成员变量(或基类)
- C++使用整数的压缩数组初始化对象
- C++初始化基类
- 多成员Constexpr结构初始化
- 复制列表初始化的隐式转换的等级是多少
- 内联映射初始化的动态atexit析构函数崩溃
- 如何在C++中初始化嵌套类中的2个memeber
- 如何声明特征矩阵,然后通过嵌套循环初始化它
- 没有用于初始化C++中的变量模板的匹配构造函数
- 在未初始化映射的情况下,将值插入到映射的映射中
- C++成员初始化
- 为什么在C++中首先初始化成员类
- 同时具有"聚合初始化"和"模板推导"
- 初始化具有非默认构造函数的std::数组项的更好方法
- 是否可以在编译时初始化数组,以便在运行时不会花费时间?
- 我可以使用条件运算符初始化C风格的字符串文字吗
- 在C和C++中初始化结构中的数组
- std::vector:<T>:resize( n, val ) 是否足以进行初始化?
- std::vector<> 使用 resize() 派生,它不初始化原语和转发construct_back
- 使用 vector<char> 作为缓冲区而不在 resize() 上初始化它