是什么导致两种构建static_vectors向量的方式之间的性能差异如此之大?

时间:2019-07-05 19:50:17

标签: c++ performance vector boost

我写了一个基准,用于测试将boost :: container :: static_vector修改并推入std :: vector的两种方法。他们似乎表现得很奇怪。

第一个:

NODE_PATH=src

它将修改本地版本,将其复制到引导程序上,然后将本地版本恢复为原始状态。

第二个:

typedef boost::container::static_vector<int, 4> Bar;

void foo(Bar& bar, std::vector<Bar>& bars)
{
    const auto oldSize = bar.size();
    bar.emplace_back(4);
    bars.emplace_back(bar);
    bar.resize(oldSize);
}

它无需修改和还原本地版本,而是直接在vector上直接修改其副本。

我的理由是,两个版本中的typedef boost::container::static_vector<int, 4> Bar; void foo(Bar& bar, std::vector<Bar>& bars) { bars.emplace_back(bar); bars.back().emplace_back(4); } 都应具有相同的性能,因为它们都只复制固定大小的对象。此外,两个foo实现都只调用一次vector::emplace_backstatic_vector::emplace_back似乎很便宜。它应该归结为检查resize是否小于当前oldSize的大小(始终是)并覆盖内部bar的大小变量。显然,这里没有析构函数调用。相反,第二个模拟调用bar寻址堆内存。

总而言之,我希望两个版本都具有相同的性能,甚至可能在第一个版本方面稍有优势(看到它只处理一次堆内存,与第二个相反)。然而,简单的基准测试则显示了其他结果。

基准:

vector::back

第一:

std::vector<Bar> bars;
for(int i = 0; i < 100; ++i)
{
    bars.clear();
    Bar bar = {5, 6, 7};
    for(int j = 0; j < 10000000; ++j)
        foo(bar, bars);
}

第二:

real    0m8,044s
user    0m7,846s
sys     0m0,152s

问题是,这种巨大的差异来自何处?

奇怪的是,更改real 0m5,754s user 0m5,559s sys 0m0,184s 的容量似乎可以缩小此差距。例如

static_vector

消除时差。尽管较小的typedef boost::container::static_vector<int, 16> Bar; 容量似乎不受此问题的影响,为什么在这两种方式之间会有如此大的区别?

我使用带有标志static_vector的g ++ 8.3.0。

编辑:

这里的基准测试时间更长,-Ofast -std=c++17 -flto容量更大(8):

static_vector

std::vector<Bar> bars; for(int i = 0; i < 1000; ++i) { bars.clear(); Bar bar = {5, 6, 7, 8, 9, 10, 11}; for(int j = 0; j < 10000000; ++j) foo(bar, bars); } 1m42,282s仍然显示出相似的差异。确实,第一个实现可以摆脱它只是复制更多字节的解释。但是接下来的问题是,为什么容量提升到16的类似基准仅显示1m31,387s2m46,733s。我希望看到相同的11s差异。

1 个答案:

答案 0 :(得分:0)

首先,我们必须认识到两种方法实际上并不等效。我们称它们为fooAfooB

typedef boost::container::static_vector<int, 4> Bar;

void fooA(Bar& bar, std::vector<Bar>& bars)
{
    const auto oldSize = bar.size();
    bar.emplace_back(4);
    bars.emplace_back(bar);
    bar.resize(oldSize);
}

void fooB(Bar& bar, std::vector<Bar>& bars)
{
    bars.emplace_back(bar);
    bars.back().emplace_back(4);
}

检查fooA

当我们叫fooA时,会发生以下情况:

  • 我们得到bar
  • 的大小
  • 我们尝试4附加到bar的末尾。如果失败,则会引发异常,并且该函数将退出,而不会在bars中添加任何内容。 bar可能处于无效状态。
  • bar被复制到bars
  • bar返回其原始状态。

检查fooB

fooB在成功案例中具有相同的行为,而在失败案例中不同的行为

  • 我们将bar复制到bars
  • 我们尝试在4的末尾将bars附加到新向量。如果失败,则抛出异常,函数退出(但bars的末尾附加了一些内容)。 bar永远不会处于无效状态,因为它从未更改。

这是什么意思?

fooAfooB的行为在成功案例中是相同的(无例外),但在引发异常时它们的行为有所不同。 ,编译器不能对它们两者都应用相同的优化,并且不能删除诸如resize之类的行为。这导致fooB更快。