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

时间:2013-03-05 09:19:38

标签: c++ boost c++11

我想使用vector<char>作为缓冲区。该接口非常适合我的需求,但由于内存已初始化,因此在将其大小调整到超出当前大小时会有性能损失。我不需要初始化,因为在任何情况下,某些第三方C函数都会覆盖数据。有没有办法或特定的分配器来避免初始化步骤?请注意,我确实要使用resize(),而不是reserve()capacity()等其他技巧,因为我需要size()始终代表我的“缓冲区”的有意义的大小。在capacity()之后,虽然resize()可能大于其大小,但同样,我不能依赖capacity()作为我的应用程序的重要信息。此外,矢量的(新)大小事先不知道,所以我不能使用std::array。如果无法以这种方式配置vector,我想知道我可以使用哪种容器或分配器而不是vector<char, std::alloc>。唯一的要求是vector的替代方案最多必须基于STL或Boost。我可以访问C ++ 11。

6 个答案:

答案 0 :(得分:24)

已知问题是,即使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();
}

答案 1 :(得分:18)

标准库中没有任何内容符合您的要求,也没有任何我知道的提升。

我能想到三个合理的选择:

  • 暂时坚持std::vector,在代码中留下评论,如果这会导致您的应用程序出现瓶颈,请回复它。
  • 使用带有construct / destroy空方法的自定义分配器 - 并希望您的优化器足够聪明,可以删除对它们的任何调用。
  • 围绕动态分配的数组创建一个包装器,只实现您需要的最小功能。

答案 2 :(得分:2)

封装它。

将其初始化为最大尺寸(不是保留)。

保持对表示实际大小结尾的迭代器的引用,就像你所说的那样。

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

答案 3 :(得分:2)

因此,总结一下stackoverflow上的各种解决方案:

  1. 使用特殊的default-init分配器。 (https://stackoverflow.com/a/21028912/1984766
    drawback:将向量类型更改为std::vector<char, default_init_allocator<char>> vec;
  2. 在具有空构造函数的char周围使用包装器结构struct NoInitChar,因此跳过值初始化(https://stackoverflow.com/a/15220853/1984766
    drawback:将向量类型更改为std::vector<NoInitChar> vec;
  3. 临时将vector<char>强制转换为vector<NoInitChar>并调整其大小(https://stackoverflow.com/a/57053750/1984766
    drawback:不会更改向量的类型,但是您需要调用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

答案 4 :(得分:0)

作为alternative solution,可与不同类型的豆荚一起使用:

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,即使它在我更关心性能的情况下对我来说也正常工作。生成的程序集as produced by 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*)

答案 5 :(得分:0)

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

即使那样,我还是更喜欢NoInitChar解决方案。 (请参见Maxim的答案)

但是,如果您确定会从中受益,并且NoInitChar不适合您,并且您正在使用clang或gcc或MSVC作为编译器,请考虑为此使用folly的例程。

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

基本思想是,这些库实现中的每个实现都有一个用于执行未初始化的调整大小的例程;您只需要调用它即可。

虽然很黑,但是您至少可以在知道Facebook的C ++代码依赖于这种黑客正常工作的情况下安慰自己,因此,如果这些库实现的新版本需要它,他们将对其进行更新。