【问题标题】:Vector: initialization or reserve?向量:初始化还是保留?
【发布时间】:2012-02-14 06:13:18
【问题描述】:

我知道向量的大小,初始化它的最好方法是什么?

选项 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

选项 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

我猜,Option2 比 Option1 好。是吗?还有其他选择吗?

【问题讨论】:

  • 定义“更好”。
  • vector&lt;int&gt; vec(3); 你不应该在头文件中初始化变量。
  • @Ale 两种方法,reserve 和 initialise,都同样专业。
  • @SigTerm:是的,我明白了。难道只是为了避免小错误
  • 5 年后,我希望这个问题有一个真正的答案......接受的答案是错误的(除非在给出答案后对问题进行了编辑)。

标签: c++ vector


【解决方案1】:

两种变体具有不同的语义,即您在比较苹果和橙子。

第一个给你一个包含 n 个默认初始化值的向量,第二个变体保留内存,但不初始化它们。

选择更适合您需求的,即在特定情况下什么是“更好”的。

【讨论】:

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

“最好”的方法是:

vector<int> vec = {var1, var2, var3};

可用于支持 C++11 的编译器。

不确定您在头文件或实现文件中执行操作的确切含义。一个可变的全局对我来说是禁忌。如果是类成员,则可以在构造函数初始化列表中进行初始化。

否则,如果您知道要使用多少个项目并且默认值(0 表示 int)会很有用,则通常会使用选项 1。
在这里使用at 意味着你不能保证索引是有效的。这样的情况本身就令人担忧。即使您能够可靠地检测到问题,使用push_back 肯定更简单,并且不必担心索引是否正确。

在选项2的情况下,无论您是否保留内存,通常都会产生零性能差异,因此不保留更简单*。除非向量包含复制成本非常高的类型(并且在 C++11 中不提供快速移动),或者向量的大小将非常巨大。


* 来自 Stroustrups C++ Style and Technique FAQ:

人们有时会担心 std::vector 增长的成本 逐渐地。我曾经担心这一点并使用reserve() 优化增长。在测量我的代码并反复拥有 很难真正找到 Reserve() 的性能优势 程序,我停止使用它,除非需要避免 迭代器失效(在我的代码中很少见)。再次:测量之前 你优化。

【讨论】:

  • 我的第一个迭代器失效,因为我没有使用保留。感谢 Stroustrups 的报价!
  • 这是一个很好的答案。唯一应该添加的是 Troyseph 回答的一部分:填充构造函数不适用于不可默认构造的类型,除非您为所有类型提供默认值。在许多情况下,这可能非常低效。在这种情况下,reserve() 和 push_back() 是更好的选择
  • 此选项也允许向量保持不变:vector&lt;int&gt; const vec = {var1, var2, var3};
  • IMO,Stroustrup 在这一点上是完全错误的。如果你知道你将拥有多少个元素,或者有一个很好的猜测,那么你应该reserve()。在最坏的情况下,它什么也没有改变。否则,它可以避免逐渐达到足够的capacity 的无意义的宣传。它只需要一条线。为什么不去做呢?我想的一个论点是它使代码不那么通用,但是如果您使用vector,那是因为您知道在没有其他证据的情况下它是最好的容器,并且您可能永远不需要更改它;那么,为自己而写泛型是一种反模式。
  • 还有一点值得指出:虽然initializer_list 构造函数在一些ints(和许多其他的)这样的琐碎案例中显然是最漂亮且明显正确的,但它确实需要复制从列表到容器,这对于较大的类类型可能是一个很大的开销,或者根本不可能。所以有时emplaceing 是唯一的选择,或者即使不是严格需要也可能更快。
【解决方案3】:

不知何故,一个完全错误的非答案答案在大约 7 年的时间里一直被接受并获得最高票数。这不是一个苹果和橘子的问题。这不是一个可以用模糊的陈词滥调来回答的问题。

要遵循一个简单的规则:

选项 #1 更快...

...但这可能不是您最关心的问题。

首先,差异很小。其次,随着我们加快编译器优化,差异变得更小。例如,在我的 gcc-5.4.0 上,当运行 3 级编译器优化 (-O3) 时,差异可以说是微不足道的:

所以一般来说,我建议您在遇到这种情况时使用方法#1。但是,如果您不记得哪个是最佳的,则可能不值得努力找出。只需选择其中一个并继续,因为这不太可能导致整个程序明显放缓。


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

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

我使用的头文件位于这里:

【讨论】:

  • 如果缺少默认构造函数,一个字也没有?
  • @doak int 的默认构造函数永远不会丢失。如果 OP 想要进行更广泛的讨论,他们应该提出更广泛的问题。这是对他们所问问题的一个很好的回答,它解决了基本元素类型并扩展到它们的合理数量。
  • @underscore_d,就性能而言,这是一个非常好的答案。但是如果没有默认构造函数,剩下的就是初始化。您可能会争辩说“选项 2”意味着必须有一个,但也许 OP 还没有意识到这个问题。
  • 那么前两张图片的优化级别是多少?我知道它可能是 -O0 ,因此没有相关性......
  • 这是一个很好的答案,而且 令人惊讶 因为选项 1 应该将所有值设置为某个值,这似乎比简单地分配内存而不修改它更昂贵。修改一些东西——尤其是很多东西——怎么能比在它周围设置一个栅栏更快呢?不过,谢谢。
【解决方案4】:

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


构造函数

std::vector<MyType> myVec(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();

在第一种情况下,使用构造函数,sizenumberOfElementsToStart 将相等,capacity 将大于或等于它们。

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


预留

std::vector<MyType> myVec;
myVec.reserve(numberOfElementsToStart);
int size = myVec.size();
int capacity = myVec.capacity();

使用reserve 函数时,size 将是0,直到您向数组中添加一个元素并且capacity 将等于或大于numberOfElementsToStart

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

请注意,push_back() 仍需要内部检查以确保 size 和 increment size,因此您可能需要权衡默认成本建设。


列表初始化

std::vector<MyType> myVec{ var1, var2, var3 };

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

【讨论】:

  • "没有开销" 是错误的。在检查是否有足够的capacity 并增加size 时,每个push_back() 显然存在开销。拥有reserve()d 仅意味着对capacity 的检查将始终成功,但它不能阻止库必须检查并且显然要升迁。那么这些检查/增量是否会抵消最初默认构造元素所节省的时间,这是一个非常有效的问题。
  • @underscore_d 并非所有元素都可以默认构造,并且对于某些默认构造可能代价高昂,但是是的,我将修改我的答案。供将来参考,“只是错误”听起来比“错误”更具侵略性。对于某些人来说,这种差异可能意味着他们变得防御而不是合作。
  • 您在这里有一些有用的信息和分析,但它有点没有重点,并没有直接解决问题中给出的比较。也许您想要给出的答案,特别是对于原始问题中的代码,是它们是相同的。在这种情况下,请多强调并给出理由/解释。
【解决方案5】:

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

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

【讨论】:

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

工作原理

这是特定于实现的,但一般来说,内部的 Vector 数据结构将具有指向元素实际驻留的内存块的指针。默认情况下,GCC 和 VC++ 都分配 0 个元素。所以你可以认为Vector的内存指针默认为nullptr

当您在选项 1 中调用 vector&lt;int&gt; vec(N); 时,会使用默认构造函数创建 N 个对象。这称为填充构造函数

当您像选项 2 一样执行vec.reserve(N); after 默认构造函数时,您会得到数据块来保存 3 个元素,但不会像选项 1 那样创建对象。

为什么选择选项 1

如果您知道向量将保留的元素数量,并且您可能会将大部分元素保留为其默认值,那么您可能需要使用此选项。

为什么选择选项 2

这个选项通常比这两个更好,因为它只分配数据块以供将来使用,而不是实际填充从默认构造函数创建的对象。

【讨论】:

  • 您显然错过了 OP 的第二个选项调用 .reserve() 的位置,它的存在完全是为了“在添加元素时消除不必要的向量扩展。”您似乎还暗示 @987654325 @ing 小于实现的默认值将重新分配,但这不是必需的,并且会表明实现非常愚蠢......而且他们确切地知道大小。 Q 是默认初始化然后重新分配基本类型是否更便宜 - 或者重复push_back(),使用分支检查每个元素的.capacity()等等。这似乎完全错过了,谈论其他事情
  • 我想你误解了我的回答。我只是重写了它,希望能解决你的困惑。
  • 我觉得它没有说服力。我不认为奇怪的实现的机会意味着它总是最好冗余地默认构造然后重新分配,而不注意(a)对于只能构造而不能分配的类型是不可能的,例如由于const 或参考成员和 (b) 基准测试显示在这些奇怪的库中重新分配的开销(因为我们忽略了推送必须做的其他事情)超过了默认构造然后重新分配的开销。 (b)我没测过,但好像整个线程中也没有其他人……所以这一切都只是闲散的猜测。
  • 你说的“最好是冗余地默认构造然后重新分配”是什么意思?不知道你是如何解释的。如果您知道将进入向量的元素数量,那么始终建议在构造函数中指定它。 .Net 等其他一些系统默认使用非零分配的原因是因为大量程序的统计数据表明非零分配的性能会更好。
  • “推荐”在哪里,由谁推荐?这不是我的解释,而是一个显而易见的事实:“选项 1”是初始化一个填充有默认值的向量,然后为每个元素重新分配它总是应该具有的值。我要强调的是 (a) 并非所有类型都可以这样做 & (b) 我们需要的不仅仅是猜测这样做是否比“选项 2”更可靠,这本身意味着必须考虑重新分配,未提及的推回开销(检查capacity,增量size)等。很可能是这样!但我没有看到任何令人信服的论据/证据 ITT。
【解决方案7】:

从长远来看,这取决于元素的使用和数量。

运行下面的程序,了解编译器是如何预留空间的:

vector<int> vec;
for(int i=0; i<50; i++)
{
  cout << "size=" << vec.size()  << "capacity=" << vec.capacity() << endl;
  vec.push_back(i);
}

size 是实际元素的数量,而 capacity 是实现向量的数组的实际大小。 在我的电脑里,直到 10 点,两者都是一样的。但是,当大小为 43 时,容量为 63。根据元素的数量,可能会更好。例如,增加容量可能会很昂贵。

【讨论】:

  • 我不明白这有什么关系。您谈论随着规模的增加而重新分配。 OP 谈到了一个已知的大小,并且在这两种情况下都避免了重新分配(通过初始化或保留空间)
【解决方案8】:

另一种选择是信任您的编译器(tm)并执行push_backs,而无需先调用reserve。当您开始添加元素时,它必须分配一些空间。也许它和你一样好?

拥有完成相同工作的更简单的代码会“更好”。

【讨论】:

  • 你的想法会与实际相矛盾。请参阅以下示例:en.cppreference.com/w/cpp/container/vector/reserve
  • 这是一个示例。其他实现可能使用 16 字节的第一次分配,因为他们知道无论如何这是最小的堆分配大小。然后这将适用于 4 个 push_backs。
  • 感谢您的评论回复中的另一个 example
【解决方案9】:

由于似乎已经过去了 5 年,错误的答案仍然是被接受的答案,而最受好评的答案完全没用(只见树木不见森林),我将添加一个真实的回应。

方法#1:我们将初始大小参数传递给向量,我们称之为n。这意味着向量填充有n 元素,这些元素将被初始化为其默认值。例如,如果向量包含ints,它将用n 零填充。

方法#2:我们首先创建一个空向量。然后我们为n 元素保留空间。在这种情况下,我们永远不会创建 n 元素,因此我们永远不会对向量中的元素执行任何初始化。由于我们计划立即覆盖每个元素的值,因此缺少初始化不会对我们造成任何伤害。另一方面,由于我们整体上做得更少,这将是更好的*选择。

* 更好 - 真正的定义:永远不会更糟。聪明的编译器总有可能找出你想要做什么并为你优化它。


结论:使用方法#2。

【讨论】:

  • 接受的答案有什么问题?它还说第一种方法使用默认值创建n 条目,而第二种方法只保留内存。除了第一个具有默认初始化和边界检查的开销之外,第二个具有push_back 逻辑中的开销(更改当前大小并检查是否必须增加保留的内存)。
  • '方法 #2 更好,即永远不会更糟' - 这将与一年后的 your own other, far better answer 相矛盾!我真的很喜欢那个。我不太确定这个。一个不太聪明的编译器可能不会意识到它不必每次都检查容量与大小,并且会这样做,这会浪费时间加载、测试和分支;而初始化然后分配永远不必检查容量。正如您的其他答案所示,使用实际数量的元素进行基准测试是评估这一点的唯一方法。
  • 作者应该删除这个答案,转而支持their own other, far better answer
猜你喜欢
  • 2014-12-29
  • 1970-01-01
  • 2016-02-01
  • 2021-12-19
  • 1970-01-01
  • 2011-05-18
  • 1970-01-01
  • 1970-01-01
  • 2011-03-04
相关资源
最近更新 更多