使用 vector<char> 作为缓冲区而不在 resize() 上初始化它

Using vector<char> as a buffer without initializing it on resize()

本文关键字:resize 初始化 缓冲区 char lt vector gt 使用      更新时间:2023-10-16

我想使用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上找到的各种解决方案:

  1. 使用一个特殊的默认初始化分配器。(https://stackoverflow.com/a/21028912/1984766)
    缺点:将矢量类型更改为std::vector<char, default_init_allocator<char>> vec;
  2. 在具有空构造函数的char周围使用包装结构struct NoInitChar,从而跳过值初始化(https://stackoverflow.com/a/15220853/1984766)
    缺点:将矢量类型更改为std::vector<NoInitChar> vec;
  3. 暂时将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, &timespec);
    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

封装

初始化为最大大小(不保留)。

保持对迭代器的引用,表示实际大小的末尾。

使用beginreal end,而不是end,为您的算法

你很少需要这样做;我强烈建议您对您的情况进行基准测试,以绝对确定需要这个hack。

即便如此,我还是更喜欢NoInitChar解决方案。(见《Maxim》的回答)

但是如果你确信你会从中受益,并且NoInitChar不适合你,并且你正在使用clang, gcc或MSVC作为编译器,那么考虑使用folly的例程来实现此目的。

见https://github.com/facebook/folly/blob/master/folly/memory/UninitializedMemoryHacks.h

基本思想是每个库实现都有一个未初始化调整大小的例程;你只需要调用它。

虽然有点黑客,但你至少可以安慰自己,因为facebook的c++代码依赖于这个黑客的正常工作,所以如果这些库实现的新版本需要它,他们会更新它。