关于具有未初始化存储的c ++:STL向量?

关于具有未初始化存储的c ++:STL向量?

STL vectors with uninitialized storage?

我正在编写一个内部循环,该循环需要将struct放置在连续存储中。 我不知道这些struct会提前多少。 我的问题是STL的vector将其值初始化为0,所以无论我做什么,我都会承担初始化费用以及将struct成员设置为其值的费用。

是否有任何方法可以防止初始化,或者那里有一个带有STL的容器,该容器具有可调整大小的连续存储和未初始化的元素?

(我确定需要对代码的这一部分进行优化,并且我确定初始化会花费大量的成本。)

另外,请参阅以下我的评论以了解有关何时进行初始化的说明。

某些代码:

1
2
3
4
5
6
7
8
9
void GetsCalledALot(int* data1, int* data2, int count) {
    int mvSize = memberVector.size()
    memberVector.resize(mvSize + count); // causes 0-initialization

    for (int i = 0; i < count; ++i) {
        memberVector[mvSize + i].d1 = data1[i];
        memberVector[mvSize + i].d2 = data2[i];
    }
}

std::vector必须以某种方式初始化数组中的值,这意味着必须调用某些构造函数(或复制构造函数)。如果您要访问数组的未初始化部分(就像它已初始化一样),则vector(或任何容器类)的行为是不确定的。

最好的方法是使用reserve()push_back(),以便使用复制构造函数,从而避免使用默认构造。

使用示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct YourData {
    int d1;
    int d2;
    YourData(int v1, int v2) : d1(v1), d2(v2) {}
};

std::vector<YourData> memberVector;

void GetsCalledALot(int* data1, int* data2, int count) {
    int mvSize = memberVector.size();

    // Does not initialize the extra elements
    memberVector.reserve(mvSize + count);

    // Note: consider using std::generate_n or std::copy instead of this loop.
    for (int i = 0; i < count; ++i) {
        // Copy construct using a temporary.
        memberVector.push_back(YourData(data1[i], data2[i]));
    }
}

像这样调用reserve()(或resize())的唯一问题是,您最终可能会以比所需次数更多的次数调用复制构造函数。如果您可以对数组的最终大小做出好的预测,则最好在开始时reserve()一次。但是,如果您不知道最终尺寸,则至少平均份数会最少。

在当前版本的C ++中,内部循环的效率不高,因为在堆栈上构造了一个临时值,然后将其复制构造到向量存储器中,最后破坏了该临时值。但是,C ++的下一版本具有称为R-Value引用(T&&)的功能,该功能将有所帮助。

std::vector提供的接口不允许使用其他选项,即使用某些类似于工厂的类来构造默认值以外的值。这是在C ++中实现该模式的大致示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
template <typename T>
class my_vector_replacement {

    // ...

    template <typename F>
    my_vector::push_back_using_factory(F factory) {
        // ... check size of array, and resize if needed.

        // Copy construct using placement new,
        new(arrayData+end) T(factory())
        end += sizeof(T);
    }

    char* arrayData;
    size_t end; // Of initialized data in arrayData
};

// One of many possible implementations
struct MyFactory {
    MyFactory(int* p1, int* p2) : d1(p1), d2(p2) {}
    YourData operator()() const {
        return YourData(*d1,*d2);
    }
    int* d1;
    int* d2;
};

void GetsCalledALot(int* data1, int* data2, int count) {
    // ... Still will need the same call to a reserve() type function.

    // Note: consider using std::generate_n or std::copy instead of this loop.
    for (int i = 0; i < count; ++i) {
        // Copy construct using a factory
        memberVector.push_back_using_factory(MyFactory(data1+i, data2+i));
    }
}

这样做确实意味着您必须创建自己的向量类。在这种情况下,这也应该使一个简单的示例变得复杂。但是有时可能会使用这样的工厂函数更好,例如,如果insert以其他值作为条件,并且您将不得不无条件地构造一些昂贵的临时对象,即使实际上并不需要它。


在C ++ 11(和增强版)中,可以使用unique_ptr的数组版本分配未初始化的数组。这不是一个stl容器,但是仍然是内存管理和C ++风格的,对于许多应用程序来说已经足够了。

1
auto my_uninit_array = std::unique_ptr<mystruct[]>(new mystruct[count]);


C ++ 0x在vector上添加了一个新的成员函数模板emplace_back(依赖可变参数模板和完美的转发),该模板完全摆脱了任何临时对象:

1
memberVector.emplace_back(data1[i], data2[i]);

要阐明reserve()的响应:您需要将reserve()与push_back()结合使用。这样,不是为每个元素调用默认构造函数,而是为副本构造函数调用。您仍然要承担在堆栈上设置结构,然后将其复制到向量的代价。另一方面,如果您使用

1
vect.push_back(MyStruct(fieldValue1, fieldValue2))

编译器将直接在向量的内存中构造新实例。这取决于优化器的智能程度。您需要检查生成的代码以找出答案。


因此,这里的问题是,resize调用了insert,它正在为每个新添加的元素从默认构造的元素进行复制构造。为了使此成本为0,您需要将自己的默认构造函数和自己的副本构造函数编写为空函数。对您的副本构造函数执行此操作不是一个好主意,因为它将破坏std :: vector的内部重新分配算法。

摘要:您将无法使用std :: vector做到这一点。


呃...

尝试方法:

1
std::vector< T >::reserve(x)

这将使您能够为x项保留足够的内存,而无需初始化任何项(您的向量仍然为空)。因此,只有经过x才可以重新分配。

第二点是向量不会将值初始化为零。您是否正在调试中测试代码?

在g ++上验证后,以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <vector>

struct MyStruct
{
   int m_iValue00 ;
   int m_iValue01 ;
} ;

int main()
{
   MyStruct aaa, bbb, ccc ;

   std::vector<MyStruct> aMyStruct ;

   aMyStruct.push_back(aaa) ;
   aMyStruct.push_back(bbb) ;
   aMyStruct.push_back(ccc) ;

   aMyStruct.resize(6) ; // [EDIT] double the size

   for(std::vector<MyStruct>::size_type i = 0, iMax = aMyStruct.size(); i < iMax; ++i)
   {
      std::cout <<"[" << i <<"] :" << aMyStruct[i].m_iValue00 <<"," << aMyStruct[0].m_iValue01 <<"
"
;
   }

   return 0 ;
}

给出以下结果:

1
2
3
4
5
6
[0] : 134515780, -16121856
[1] : 134554052, -16121856
[2] : 134544501, -16121856
[3] : 0, -16121856
[4] : 0, -16121856
[5] : 0, -16121856

您看到的初始化可能是工件。

[编辑]关于调整大小的评论之后,我修改了代码以添加调整大小行。调整大小有效地调用了vector内部对象的默认构造函数,但是如果默认构造函数不执行任何操作,则不会初始化任何内容...我仍然相信这是一个伪像(我设法第一次将整个向量与以下代码:

1
2
3
aMyStruct.push_back(MyStruct()) ;
aMyStruct.push_back(MyStruct()) ;
aMyStruct.push_back(MyStruct()) ;

所以...
:-/

[编辑2]就像Arkadiy已经提供的那样,解决方案是使用带有所需参数的内联构造函数。就像是

1
2
3
4
5
struct MyStruct
{
   MyStruct(int p_d1, int p_d2) : d1(p_d1), d2(p_d2) {}
   int d1, d2 ;
} ;

这可能会内联到您的代码中。

但是无论如何,您都应该使用探查器来研究代码,以确保这段代码是应用程序的瓶颈。


您可以在元素类型周围使用包装器类型,并使用不执行任何操作的默认构造函数。例如。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
struct no_init
{
    T value;

    no_init() { static_assert(std::is_standard_layout<no_init< T >>::value && sizeof(T) == sizeof(no_init< T >),"T does not have standard layout"); }

    no_init(T& v) { value = v; }
    T& operator=(T& v) { value = v; return value; }

    no_init(no_init< T >& n) { value = n.value; }
    no_init(no_init< T >&& n) { value = std::move(n.value); }
    T& operator=(no_init< T >& n) { value = n.value; return this; }
    T& operator=(no_init< T >&& n) { value = std::move(n.value); return this; }

    T* operator&() { return &value; } // So you can use &(vec[0]) etc.
};

使用方法:

1
2
std::vector<no_init<char>> vec;
vec.resize(2ul * 1024ul * 1024ul * 1024ul);

如果您确实坚持要对元素进行未初始化,并牺牲了诸如front(),back(),push_back()之类的方法,请使用numeric中的boost向量。它甚至允许您在调用resize()时不保留现有元素。


从您的代码看来,您似乎有一个结构向量,每个结构都包含2个整数。您可以改用2个int向量吗?然后

1
2
copy(data1, data1 + count, back_inserter(v1));
copy(data2, data2 + count, back_inserter(v2));

现在,您无需为每次复制结构付费。


从您的评论到其他海报,您似乎都剩下malloc()和朋友了。向量不会让您拥有未构造的元素。


使用std :: vector :: reserve()方法。它不会调整向量的大小,但是会分配空间。


我会做类似的事情:

1
2
3
4
5
6
7
8
9
void GetsCalledALot(int* data1, int* data2, int count)
{
  const size_t mvSize = memberVector.size();
  memberVector.reserve(mvSize + count);

  for (int i = 0; i < count; ++i) {
    memberVector.push_back(MyType(data1[i], data2[i]));
  }
}

您需要为memberVector中存储的类型定义一个ctor,但这是一个很小的开销,因为它将为您提供两全其美的优势。不会进行不必要的初始化,并且在循环期间不会发生任何重新分配。


我不认为STL是您的答案。您将需要使用realloc()推出自己的解决方案。您必须存储一个指针以及元素的大小或数量,并使用该指针查找在realloc()之后开始添加元素的位置。

1
2
3
4
5
6
7
8
9
10
int *memberArray;
int arrayCount;
void GetsCalledALot(int* data1, int* data2, int count) {
    memberArray = realloc(memberArray, sizeof(int) * (arrayCount + count);
    for (int i = 0; i < count; ++i) {
        memberArray[arrayCount + i].d1 = data1[i];
        memberArray[arrayCount + i].d2 = data2[i];
    }
    arrayCount += count;
}

结构本身是否需要在连续的内存中,还是可以避免使用struct *向量?

向量会复制您添加的内容,因此使用指针向量而非对象是提高性能的一种方法。


推荐阅读