矢量:初始化还是保留?

Ale*_*Ale 48 c++ vector

我知道向量的大小,这是初始化它的最佳程序?:

选项1

vector<int> vec(3); //in .h
vec.at(0)=var1;     //in .cpp
vec.at(1)=var2;     //in .cpp
vec.at(2)=var3;     //in .cpp
Run Code Online (Sandbox Code Playgroud)

选项2

vector<int> vec;     //in .h
vec.reserve(3);     //in .cpp
vec.push_back(var1);     //in .cpp
vec.push_back(var2);     //in .cpp
vec.push_back(var3);     //in .cpp
Run Code Online (Sandbox Code Playgroud)

我猜选项2优于1.是吗?其他选择?

Seb*_*ach 45

两种变体都有不同的语义,即你比较苹果和橘子.

第一个给出了n个默认初始化值的向量,第二个变量保留了内存,但没有初始化它们.

选择更适合您需求的东西,即在某种情况下"更好"的东西.

  • 不,两者都给你一个包含`{var1,var2,var3}`的矢量略有不同的路线.问题是,要么路线要好于另一个? (11认同)
  • 所以,我认为如果我不使用它们,初始化n个默认值是没用的. (2认同)
  • @Ale:在这种情况下初始化为默认值确实没用,但是对于像int这样的类型,默认初始化它们然后覆盖真的不是那么扩展,而`push_back`通常比`operator []`更贵,所以在这种情况下效率可能没有明确的答案.最后我会说在大多数情况下你真的不应该担心这一点 (2认同)
  • @Mike Seymour:我考虑过这个变体。然而,标题指出“初始化还是保留?”,这就是我现在重点回答的问题。 (2认同)
  • @Ale `push_back()` 等人必须检查 `capacity`(无论您是否为 `reserve()`)并增加每个元素的 `size`。对我来说,必须权衡“选项 2”中的结果分支/加载/存储/任何内容与从“选项 1”中保存非默认初始化/重新分配的情况,这是非常有效的。从概念上讲,很容易认为您在 #2 方面做得更少,但我想得越多,我就越不相信。显然,某些类需要(或至少非常可取)#2,但对于基本类型则不太清楚。与往常一样,真正的答案似乎是:基准测试,或者不要推测。 (2认同)

Unc*_*ens 43

"最好"的方式是:

vector<int> vec = {var1, var2, var3};
Run Code Online (Sandbox Code Playgroud)

提供支持C++ 11的编译器.

通过标题或实现文件中的操作不确定您的意思.一个可变的全球对我来说是禁忌.如果它是类成员,则可以在构造函数初始化列表中初始化它.

否则,如果您知道将要使用多少项,则通常会使用选项1,默认值(int为0)将非常有用.在此处
使用at意味着您无法保证索引有效.像这样的情况令人担忧.即使您能够可靠地检测到问题,但使用起来肯定更简单,push_back并且不再担心索引正确.

在选项2的情况下,无论您是否保留内存,通常它都会产生零性能差异,因此更容易保留*.除非矢量包含复制非常昂贵的类型(并且不能在C++ 11中提供快速移动),否则矢量的大小将会非常大.


*来自Stroustrups C++风格和技术常见问题解答:

人们有时会担心std :: vector的成本会逐渐增加.我曾经担心这一点,并使用reserve()来优化增长.在测量我的代码并且在实际程序中反复找不到reserve()的性能优势之后,我停止使用它,除非需要避免迭代器失效(在我的代码中很少见).再次:在优化之前进行测量.

  • IMO,Stroustrup 在这方面完全错误。如果你知道你将有多少元素,或者有一个很好的猜测,那么你应该`reserve()`。最糟糕的是,它什么也改变不了。否则,它避免了逐渐达到足够“容量”的毫无意义的大张旗鼓。它只需要一行。为什么不这样做呢?我想象的一个论点是它使代码不那么通用,但是如果您使用 `vector`,那是因为您知道它是在其他人缺乏证据的情况下最好的容器,并且您可能永远不需要更改它;那么,为了它自己而进行泛型写作是一种反模式。 (3认同)
  • 这是一个非常好的答案.应该添加的唯一内容是Troyseph的答案的一部分:fill构造函数不适用于不是默认构造的类型,除非您提供一个默认值来用于所有类型.在许多情况下,这可能非常低效.在这种情况下,reserve()和push_back()是更好的选择 (2认同)
  • 另一件值得指出的事情是:虽然 `initializer_list` 构造函数显然在像一些 `int`s(以及许多其他)这样微不足道的情况下是最漂亮且显然正确的,但它确实需要从列表复制到容器,这可能是较大的类类型的显着开销,或者根本不可能。所以有时`emplace`ing 是唯一的选择,或者即使不是严格需要也可能更快。 (2认同)

Tro*_*eph 8

虽然您的示例基本相同,但可能是在使用的类型不是int您的选择时.如果你的类型没有默认构造函数,或者你以后必须重新构造每个元素,我会使用reserve.只是不要陷入我所做的陷阱并使用reserve然后operator[]进行初始化!


构造函数

std::vector<MyType> myVec(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();
Run Code Online (Sandbox Code Playgroud)

在该第一情况下,使用构造,size并且numberOfElementsToStart将等于和capacity将大于或等于它们.

将myVec视为包含MyType可以访问和修改的多个项目的向量,push_back(anotherInstanceOfMyType)将其附加到向量的末尾.


保留

std::vector<MyType> myVec;
myVec.reserve(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();
Run Code Online (Sandbox Code Playgroud)

使用该reserve函数时,size0直到您向数组添加元素并且capacity将等于或大于numberOfElementsToStart.

可以将myVec想象成一个向量,它可以使用新的项目附加到它上面,至少对第一个元素push_back 没有内存分配numberOfElementsToStart.

请注意,push_back()仍需要进行内部检查以确保大小<容量增加大小,因此您可能需要将其与默认构造的成本进行权衡.


列表初始化

std::vector<MyType> myVec{ var1, var2, var3 };
Run Code Online (Sandbox Code Playgroud)

这是初始化矢量的附加选项,虽然它仅适用于非常小的矢量,但它是一种用已知值初始化小矢量的明确方法.size将等于您初始化它的元素数量,并且capacity将等于或大于size.现代编译器可以优化临时对象的创建并防止不必要的复制.


nob*_*nob 6

选项2更好,因为reserve只需要保留内存(3*sizeof(T)),而第一个选项为容器内的每个单元调用基类型的构造函数.

对于类C类型,它可能是相同的.

  • 选项2更好,即使对于标量类型也是如此.第一个将每个元素设置为零,而第二个将使内存未初始化. (4认同)
  • @SigTerm:`reserve` 的定义是它在必要时增加容量(即分配的内存量),但*不*改变大小(即对象的数量)。因此,它不能构造任何对象,除非开始时向量不为空时移动它们。 (2认同)
  • @phresnel:既然两者都是正确的,并且两者都没有特别的可读性,那么"更好"的唯一合理定义就是"更有效". (2认同)
  • @MikeSeymour:在不将元素设置为零方面,`reserve()` 可能做的工作较少,但 `push_back` 通常比 `operator[]` 更昂贵,所以在性能方面,真的不清楚哪个更有效对于简单的类型,因此“更好”(尽管在大多数情况下,担心这一点显然很愚蠢) (2认同)
  • @nob 有多少元素?你是怎么测量的?(我曾经测量过 `std::vector&lt;double&gt;`、g++,不久前,我发现构造完整的向量,然后使用 `[]` 初始化每个元素,是最快的解决方案。) (2认同)

Apo*_*ica 5

不知何故,一个完全错误的非答案性答案一直被接受,并且在大约7年中被最强烈地接受。这不是一个苹果和桔子的问题。这不是用模糊的陈词滥调来回答的问题。

遵循以下简单规则:

选项#1更快... 在此处输入图片说明 在此处输入图片说明

...但这可能不是您最大的担忧。

首先,差异很小。其次,随着我们加快编译器优化的步伐,两者之间的差异变得越来越小。例如,在我的gcc-5.4.0上,运行级别3编译器优化(-O3)时,差异可以说是微不足道的: 在此处输入图片说明

因此,总的来说,我建议您在遇到这种情况时使用方法1。但是,如果您不记得哪一个是最佳的,可能就不值得花时间去寻找。只需选择其中一个并继续前进,因为这不可能导致整个程序整体明显变慢。


通过从正态分布中采样随机向量大小,然后使用两种方法对这些大小的向量进行初始化计时,来运行这些测试。我们保留一个虚拟的sum变量,以确保向量初始化未得到优化,并且我们对向量大小和值进行随机化,以努力避免由于分支预测,缓存和其他此类技巧而导致的任何错误。

main.cpp

/* 
 * Test constructing and filling a vector in two ways: construction with size
 * then assignment versus construction of empty vector followed by push_back
 * We collect dummy sums to prevent the compiler from optimizing out computation
 */

#include <iostream>
#include <vector>

#include "rng.hpp"
#include "timer.hpp"

const size_t kMinSize = 1000;
const size_t kMaxSize = 100000;
const double kSizeIncrementFactor = 1.2;
const int kNumVecs = 10000;

int main() {
  for (size_t mean_size = kMinSize; mean_size <= kMaxSize;
       mean_size = static_cast<size_t>(mean_size * kSizeIncrementFactor)) {
    // Generate sizes from normal distribution
    std::vector<size_t> sizes_vec;
    NormalIntRng<size_t> sizes_rng(mean_size, mean_size / 10.0); 
    for (int i = 0; i < kNumVecs; ++i) {
      sizes_vec.push_back(sizes_rng.GenerateValue());
    }
    Timer timer;
    UniformIntRng<int> values_rng(0, 5);
    // Method 1: construct with size, then assign
    timer.Reset();
    int method_1_sum = 0;
    for (size_t num_els : sizes_vec) {
      std::vector<int> vec(num_els);
      for (size_t i = 0; i < num_els; ++i) {
        vec[i] = values_rng.GenerateValue();
      }
      // Compute sum - this part identical for two methods
      for (size_t i = 0; i < num_els; ++i) {
        method_1_sum += vec[i];
      }
    }
    double method_1_seconds = timer.GetSeconds();
    // Method 2: reserve then push_back
    timer.Reset();
    int method_2_sum = 0;
    for (size_t num_els : sizes_vec) {
      std::vector<int> vec;
      vec.reserve(num_els);
      for (size_t i = 0; i < num_els; ++i) {
        vec.push_back(values_rng.GenerateValue());
      }
      // Compute sum - this part identical for two methods
      for (size_t i = 0; i < num_els; ++i) {
        method_2_sum += vec[i];
      }
    }
    double method_2_seconds = timer.GetSeconds();
    // Report results as mean_size, method_1_seconds, method_2_seconds
    std::cout << mean_size << ", " << method_1_seconds << ", " << method_2_seconds;
    // Do something with the dummy sums that cannot be optimized out
    std::cout << ((method_1_sum > method_2_sum) ? "" : " ") << std::endl;
  }

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

我使用的头文件位于:

  • 没有一个单词以防默认构造函数丢失? (5认同)
  • @doak `int` 的默认构造函数永远不会丢失。如果OP想要进行更广泛的讨论,他们应该提出更广泛的问题。这是对他们要求的一个很好的答案,它解决了基本元素类型和合理数量的比例。 (2认同)