【问题标题】:Performance impact when resizing vector within capacity在容量范围内调整矢量大小时的性能影响
【发布时间】:2020-02-13 23:50:25
【问题描述】:

我的代码有以下综合示例:

#include <vector>
#include <array>
#include <cstdlib>

#define CAPACITY 10000

int main() {
    std::vector<std::vector<int>> a;
    std::vector<std::array<int, 2>> b;

    a.resize(CAPACITY, std::vector<int> {0, 0})
    b.resize(CAPACITY, std::array<int, 2> {0, 0})

    for (;;) {
        size_t new_rand_size = (std::rand() % CAPACITY);

        a.resize(new_rand_size);
        b.resize(new_rand_size);

        for (size_t i = 0; i < new_rand_size; ++i) {
            a[i][0] = std::rand();
            a[i][1] = std::rand();
            b[i][0] = std::rand();
            b[i][1] = std::rand();
        }

        process(a); // respectively process(b)
    }
}

很明显,数组版本更好,因为它需要更少的分配,因为数组的大小是固定的并且在内存中是连续的(对吗?)。它只是在容量范围内再次向上调整大小时重新初始化。

因为无论如何我都要覆盖,所以我想知道是否有办法跳过初始化(例如,通过覆盖分配器或类似方法)以进一步优化代码。

【问题讨论】:

  • 你知道emplace_back()怎么用吗?

标签: c++ arrays optimization vector allocator


【解决方案1】:

很明显,

“显然”这个词通常用来表示“我真的非常希望以下内容为真,所以我将跳过我确定是否为真的部分是的。” ;) (诚然,你做得比大多数人都好,因为你确实为你的结论提出了一些理由。)

数组版本更好,因为它需要更少的分配,因为数组的大小是固定的并且在内存中是连续的(对吗?)。

这件事的真实性取决于实现,但这里有一些有效性。我会采用一种不那么微观管理的方法,并说数组版本是首选,因为最终大小是固定的。使用为您的特殊情况设计的工具(固定大小的数组)往往比使用针对更一般情况的工具产生的开销更少。不过,并不总是更少。

另一个需要考虑的因素是默认初始化元素的成本。当std::array 被构造时,它的所有元素也会被构造。使用std::vector,您可以推迟构造元素,直到获得构造参数。对于默认构造成本很高的对象,您可以使用向量而不是数组来衡量性能增益。 (如果您无法衡量差异,请不要担心。)

当您进行比较时,请确保通过正确使用向量来获得公平的机会。由于预先知道大小,reserve 立即需要空间。另外,请使用emplace_back 以避免不必要的复制。

最后说明:“连续”比“连续”更准确/更具描述性。

它只是在容量范围内再次放大时重新初始化。

这是影响这两种方法的一个因素。事实上,这会导致您的代码表现出未定义的行为。例如,假设您的第一次迭代将外部向量的大小调整为 1,而第二次将其大小调整为 5。将您的代码与以下内容进行比较:

std::vector<std::vector<int>> a;
a.resize(CAPACITY, std::vector<int> {0, 0});
a.resize(1);
a.resize(5);
std::cout << "Size " << a[1].size() <<".\n";

输出表明此时大小为零,但您的代码会将值分配给a[1][0]。如果您希望 a 的每个元素默认为 2 个元素的向量,则需要在每次调整 a 的大小时指定该默认值,而不仅仅是最初。

因为无论如何我都要覆盖,所以我想知道是否有办法跳过初始化(例如通过覆盖分配器或类似方法)以进一步优化代码。

是的,您可以跳过初始化。事实上,这样做是明智的。使用专为手头任务设计的工具。您的初始化用于增加向量的容量。所以使用唯一目的是增加向量容量的方法:vector::reserve

另一种选择——取决于具体情况——可能是根本不调整大小。从数组数组开始,并跟踪外部数组中的最后一个可用元素。这是一种倒退,因为您现在有一个单独的变量来跟踪大小,但是如果您的实际代码有足够的迭代次数,那么在大小减小时不调用析构函数所节省的成本可能会使这种方法值得。 (为了更简洁的代码,编写一个封装数组数组并跟踪可用大小的类。)

【讨论】:

  • 好主意,谢谢。几个 cmets:(3/4 段)- 是的,但即使我立即为嵌套向量保留容量,调整大小也只是意味着可以轻松构建嵌套向量,但仍然需要为其分配内存元素,而不是数组,因为大小是固定的。 emplace_back 很好,但是我不知道如何将std::arrayinitializer_list 传递给外部向量的emplace_back,这样可以为每个元素保存一个副本。 (倒数第二段)是的,但不幸的是,这不适用于 std::array ...
  • 默认初始化总是完成的。我在上一段中考虑了这个想法,它肯定会导致最佳优化结果......如果我需要它,我会沿着这条路走。
【解决方案2】:

因为无论如何我都要覆盖,所以我想知道是否有办法跳过初始化

是的:不要调整大小。相反,保留容量并推送(或安置)新元素。

【讨论】:

  • 我稍微更新了我的示例。新大小的数组完全由另一个函数(库的一部分)处理。所以在一些迭代中,数组可能比以前小,而在一些比以前大。
  • 您仍然需要调整大小以减小大小(或者您可以删除,如果您愿意)。
  • 是的,但我想问的是在容量范围内调整大小时是否可以跳过初始化以提高效率。
  • 啊。不,您不能完全跳过初始化。您必须调整大小(从而通过复制初始化具有相同值的新元素)或 push/emplace 以为每个元素使用不同的值进行初始化。您可以跳过其中之一,但不能同时跳过两者。
  • 如何在不构造中间对象的情况下用最终值放置一个新数组?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多