【问题标题】:STL vectors with uninitialized storage?存储未初始化的 STL 向量?
【发布时间】:2010-09-10 22:16:01
【问题描述】:

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

有什么方法可以阻止初始化,还是有一个类似 STL 的容器,具有可调整大小的连续存储和未初始化的元素?

(我确信这部分代码需要优化,而且我确信初始化是一笔不小的开销。)

另外,请参阅下面的我的 cmets 以了解初始化发生的时间。

一些代码:

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];
    }
}

【问题讨论】:

  • 注意 - 使用 reserve() 不是解决方案,因为您无法合法访问位于 end() 及以上位置的数据。
  • 另一个澄清:不是构造函数将值初始化为0。而是resize调用insert。
  • 你也可以给我们结构声明吗?谢谢... :-)
  • 注意:无论如何您都无法访问未初始化的数据。 vector 过去 .end() 和 T[] 的未初始化成员的问题相同。但是有了向量,调试代码现在很可能会告诉你。阵列代码将在客户 PC 上静默失败。
  • 这是个好问题。对于某些应用程序,重要的是要意识到 std::vector 总是初始化其元素,即使它们是普通旧数据 (POD)。

标签: c++ optimization stl vector


【解决方案1】:

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

最好的办法是使用reserve()push_back(),这样就使用了copy-constructor,避免了default-constructor。

使用您的示例代码:

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 值引用 (T&amp;&amp;) 的功能,它会有所帮助。

std::vector 提供的接口不允许另一种选择,即使用一些类似工厂的类来构造默认值以外的值。下面是这个模式在 C++ 中实现的粗略示例:

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));
    }
}

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

        【解决方案4】:

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

        vect.push_back(MyStruct(fieldValue1, fieldValue2))
        

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

        【讨论】:

        • 事实证明,在 O3 级别的 gcc 优化器不够聪明,无法避免复制。
        【解决方案5】:

        您可以使用boost::noinit_adaptordefault initialize 新元素(对于内置类型没有初始化):

        std::vector<T, boost::noinit_adaptor<std::allocator<T>> memberVector;
        

        只要您不将初始化程序传递给resize,它默认初始化新元素。

        【讨论】:

          【解决方案6】:

          所以问题来了,resize 调用了 insert,它从默认构造的元素为每个新添加的元素进行复制构造。要使其成本为 0,您需要将自己的默认构造函数和自己的复制构造函数编写为空函数。对您的复制构造函数这样做是一个非常糟糕的主意,因为它会破坏 std::vector 的内部重新分配算法。

          总结:你不能用 std::vector 做到这一点。

          【讨论】:

          • 这是真正的问题。 std::vector 应该意识到如果 T 有一个微不足道的默认构造函数,它不需要做任何初始化。感谢您指出复制构造函数在这里做了不必要的工作。
          【解决方案7】:

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

          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.
          };
          

          使用方法:

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

          【讨论】:

            【解决方案8】:

            错误...

            试试方法:

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

            它将使您能够为 x 项保留足够的内存而无需初始化任何项(您的向量仍然是空的)。因此,直到超过 x 才会重新分配。

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

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

            #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 << "\n" ;
               }
            
               return 0 ;
            }
            

            给出以下结果:

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

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

            [编辑] 在 resize 的评论之后,我修改了代码以添加 resize 行。调整大小有效地调用了向量内对象的默认构造函数,但如果默认构造函数什么都不做,那么什么都不会初始化......我仍然相信这是一个工件(我第一次设法让整个向量归零)以下代码:

            aMyStruct.push_back(MyStruct()) ;
            aMyStruct.push_back(MyStruct()) ;
            aMyStruct.push_back(MyStruct()) ;
            

            所以... :-/

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

            struct MyStruct
            {
               MyStruct(int p_d1, int p_d2) : d1(p_d1), d2(p_d2) {}
               int d1, d2 ;
            } ;
            

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

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

            【讨论】:

            • 我在上面写了一个注释。初始化为 0 的不是 vector 的构造函数,而是 resize() 。
            • 在这种情况下,MyStruct 有一个简单的构造函数,因此没有初始化任何内容。这可能与 OP 的情况不同。
            • 我认为你在正确的轨道上。我没有在结构中定义构造函数,所以它的默认构造函数(我相信)零初始化。我会检查添加一个不执行任何操作的默认构造函数是否能解决问题。
            • 如果你没有定义构造函数并且所有元素都是 POD 类型,那么构造函数什么也不做。如果元素不是 POD,那么它只会调用它们的默认构造函数。
            • 格雷格·罗杰斯是对的。我的猜测是,由于某些进程初始化与我编写的代码无关,因此内存为“零”。在 C++ 中,您无需为不使用的东西付费。因此,如果您正在编写类似 C 的代码,则不应有开销。向量在这方面做得很好。
            【解决方案9】:

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

            【讨论】:

              【解决方案10】:

              从您的 cmets 到其他海报,看起来您只剩下 malloc() 和朋友了。 Vector 不会让你有未构造的元素。

              【讨论】:

                【解决方案11】:

                从您的代码看来,您有一个结构向量,每个结构包含 2 个整数。您可以改用 2 个整数向量吗?那么

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

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

                【讨论】:

                • 有趣。这可能行得通——它似乎可以避免构建中间对象。
                【解决方案12】:

                如果你真的坚持让元素未初始化并牺牲一些方法,如 front()、back()、push_back(),请使用来自 numeric 的 boost 向量。它甚至允许您在调用 resize() 时不保留现有元素...

                【讨论】:

                  【解决方案13】:

                  结构本身是否需要位于连续内存中,或者您可以使用 struct* 的向量吗?

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

                  【讨论】:

                  • 它们必须是连续的。它们位于一个缓冲区中,即将通过网络作为一大块发送。
                  【解决方案14】:

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

                  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;
                  }
                  

                  【讨论】:

                    【解决方案15】:

                    我会这样做:

                    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,但这是一个很小的成本,因为它将为您提供两全其美的效果;没有进行不必要的初始化,并且在循环期间不会发生重新分配。

                    【讨论】:

                    • 这似乎并没有解决问题,因为它使用了一个临时的 MyType() 并将其复制到向量中。仍然是双重初始化。
                    【解决方案16】:

                    我不确定所有那些说这是不可能的答案或告诉我们未定义行为的答案。

                    有时,您需要使用 std::vector。但有时,你知道它的最终大小。而且您还知道您的元素将在以后构建。 示例:当您将向量内容序列化为二进制文件时,稍后再将其读回。 虚幻引擎有它的 TArray::setNumUninitialized,为什么没有 std::vector?

                    回答最初的问题 “有什么方法可以阻止初始化,还是有一个类似 STL 的容器,具有可调整大小的连续存储和未初始化的元素?”

                    是和不是。 不,因为 STL 没有公开这样做的方法。

                    是的,因为我们使用 C++ 进行编码,而 C++ 允许做很多事情。如果您准备好成为一个坏人(并且如果您真的知道自己在做什么)。你可以劫持向量。

                    这里的示例代码仅适用于 Windows 的 STL 实现,对于另一个平台,请查看如何实现 std::vector 以使用其内部成员:

                    // This macro is to be defined before including VectorHijacker.h. Then you will be able to reuse the VectorHijacker.h with different objects.
                    #define HIJACKED_TYPE SomeStruct
                    
                    // VectorHijacker.h
                    #ifndef VECTOR_HIJACKER_STRUCT
                    #define VECTOR_HIJACKER_STRUCT
                    
                    struct VectorHijacker
                    {
                        std::size_t _newSize;
                    };
                    
                    #endif
                    
                    
                    template<>
                    template<>
                    inline decltype(auto) std::vector<HIJACKED_TYPE, std::allocator<HIJACKED_TYPE>>::emplace_back<const VectorHijacker &>(const VectorHijacker &hijacker)
                    {
                        // We're modifying directly the size of the vector without passing by the extra initialization. This is the part that relies on how the STL was implemented.
                        _Mypair._Myval2._Mylast = _Mypair._Myval2._Myfirst + hijacker._newSize;
                    }
                    
                    inline void setNumUninitialized_hijack(std::vector<HIJACKED_TYPE> &hijackedVector, const VectorHijacker &hijacker)
                    {
                        hijackedVector.reserve(hijacker._newSize);
                        hijackedVector.emplace_back<const VectorHijacker &>(hijacker);
                    }
                    

                    但请注意,这是我们所说的劫持。这是非常脏的代码,只有在你真的知道自己在做什么的情况下才能使用它。此外,它不可移植,并且严重依赖于 STL 的实现方式。

                    我不建议您使用它,因为这里的每个人(包括我在内)都是好人。但我想让您知道,这可能与之前所有声明不是的答案相反。

                    【讨论】:

                    • 请注意,我在 Visual Studio 2019 Community 16.7.5 上编写了代码。而且我使用了最新的草案......并且一致性模式被禁用(允许-)(因为遗憾的是,模板和一致性模式在很多情况下并不是一个非常好的组合)。最后,它依赖于平台实现 STL 的方式,因此,如果你想使用它,你需要调整解决方案(但你不会弄脏你的手,对吗?)。
                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-09-22
                    • 1970-01-01
                    • 2011-03-04
                    • 2011-05-18
                    • 1970-01-01
                    • 2016-05-27
                    相关资源
                    最近更新 更多