【问题标题】:Why would array<T, N> ever be slower than vector<T>?为什么 array<T, N> 会比 vector<T> 慢?
【发布时间】:2012-07-01 01:56:56
【问题描述】:

今天我决定对 std::vectorstd::array 的 gcc 可优化性进行基准测试并比较一些差异。一般来说,我发现了我的预期:在每个短数组集合上执行任务比在集合等效向量上执行任务要快得多。

但是,我发现有些意外:使用std::vector 存储数组集合比使用std::array更快。以防它是堆栈上大量数据的某些工件的结果,我还尝试将其分配为堆上的数组和堆上的 C 样式数组(但结果仍然类似于堆栈上的数组和数组的向量)。

知道为什么std::vector胜过std::array(编译器有更多的编译时信息)吗?

我使用gcc-4.7 -std=c++11 -O3 编译(gcc-4.6 -std=c++0x -O3 也应该导致这个难题)。运行时间是使用bash-native time 命令(用户时间)计算的。

代码:

#include <array>
#include <vector>
#include <iostream>
#include <assert.h>
#include <algorithm>

template <typename VEC>
double fast_sq_dist(const VEC & lhs, const VEC & rhs) {
  assert(lhs.size() == rhs.size());
  double result = 0.0;
  for (int k=0; k<lhs.size(); ++k) {
    double tmp = lhs[k] - rhs[k];
    result += tmp * tmp;
  }
  return result;
}

int main() {
  const std::size_t K = 20000;
  const std::size_t N = 4;

  // declare the data structure for the collection
  // (uncomment exactly one of these to time it)

  // array of arrays
  // runtime: 1.32s
  std::array<std::array<double, N>, K > mat;

  // array of arrays (allocated on the heap)
  // runtime: 1.33s
  //  std::array<std::array<double, N>, K > & mat = *new std::array<std::array<double, N>, K >;

  // C-style heap array of arrays
  // runtime: 0.93s
  //  std::array<double, N> * mat = new std::array<double, N>[K];

  // vector of arrays
  // runtime: 0.93
  //  std::vector<std::array<double, N> > mat(K);

  // vector of vectors
  // runtime: 2.16s
  //  std::vector<std::vector<double> > mat(K, std::vector<double>(N));

  // fill the collection with some arbitrary values
  for (std::size_t k=0; k<K; ++k) {
    for (std::size_t j=0; j<N; ++j)
      mat[k][j] = k*N+j;
  }

  std::cerr << "constructed" << std::endl;

  // compute the sum of all pairwise distances in the collection
  double tot = 0.0;
   for (std::size_t j=0; j<K; ++j) {
     for (std::size_t k=0; k<K; ++k)
       tot += fast_sq_dist(mat[j], mat[k]);
   }

   std::cout << tot << std::endl;

  return 0;
}

NB 1:所有版本都打印相同的结果。

注意 2: 并且只是为了证明 std::array&lt;std::array&lt;double, N&gt;, K&gt;std::vector&lt;std::array&lt;double, N&gt; &gt;std::vector&lt;std::vector&lt;double&gt; &gt; 之间的运行时差异不仅仅是分配时的分配/初始化,简单分配的运行时集合(即注释掉tot 的计算和打印)分别为 0.000s、0.000s 和 0.004s。

注意 3: 每个方法都是单独编译和运行的(不是在同一个可执行文件中背靠背地计时),以防止缓存中的不公平差异。

注意 4:
数组数组的汇编:http://ideone.com/SM8dB
数组向量的组装:http://ideone.com/vhpJv
向量集合:http://ideone.com/RZTNE

注意 5: 绝对清楚,我绝不打算批评 STL。一个绝对喜欢的 STL,而且我不仅经常使用它,而且有效使用的细节教会了我很多 C++ 微妙而伟大的特性。相反,这是一种智力追求:我只是在安排时间来学习高效 C++ 设计的原则。

此外,责怪 STL 是不合理的,因为很难解开运行时差异的病因:启用优化后,编译器优化可能会减慢而不是加快代码时间>。在关闭优化的情况下,它可能来自不必要的复制操作(将被优化并且永远不会在生产代码中执行),这可能比其他数据类型更偏向某些数据类型。

如果您像我一样好奇,我希望您能帮助解决这个问题。

【问题讨论】:

  • 尝试以类似 1000 的迭代次数运行它,以查看更准确的值。这些看起来可能只是延迟值。
  • @ColeJohnson 你的意思是N=1000 还是K=1000?如果您的意思是N=1000,则数组向量与向量向量几乎相同(因为不展开循环的开销非常高)。使用N=1 会导致数组向量和向量向量之间的差异更大,因为数组向量本质上应该转换为双精度向量。所以比较数组数组和数组向量最有趣的例子是K &lt;&lt; N(数学意义上的&lt;&lt;,而不是位移意义上的)。
  • 如果交换两个测试会发生什么?
  • @Oliver:就像,在vector 测试之后 进行array 测试。或者等等,你是在完全不同的程序上测试它们吗?如果是这样,那我就误会了。
  • 它们各自的内部表示方式过于相似,以至于在性能上没有任何差异。这不是一个真正有效的测试,您需要更大的数据集。

标签: c++ optimization stl c++11


【解决方案1】:

考虑第二个和第三个测试。从概念上讲,它们是相同的:从堆中分配 K * N * sizeof(double) 字节,然后以完全相同的方式访问它们。那么为什么会有不同的时间呢?

所有“更快”的测试都有一个共同点:new[]。所有较慢的测试都分配给new 或在堆栈上。 vector 可能使用 new[] Under the Hood™。造成这种情况的唯一明显原因是 new[]new 的实现比预期的要大得多。

我要建议的是new[] 将回退到mmap 并直接在页面边界上分配,从而加快对齐速度,而其他两种方法不会在页面边界上分配。

考虑使用操作系统分配函数直接映射已提交的页面,然后将std::array&lt;std::array&lt;double, N&gt;, K&gt; 放入其中。

【讨论】:

  • 我尝试std::array&lt;std::array&lt;double, N&gt;, K &gt; &amp; mat = *new std::array&lt;std::array&lt;double, N&gt;, K &gt;[1]; 强制使用new[],但它提供与数组数组相同的运行时间...
  • 除非您提供分配器来执行此操作,否则vector 将不会使用new[]“幕后”。它使用分配器提供的任何东西。除非您另外指定,否则它使用std::allocator&lt;T&gt;。反过来,这将使用operator new 分配原始内存。
  • 哦,是的。忘了分配器。
  • 对齐加速不能解释这一点。总内存使用量为 20000*4*8 ≈ 640K,因此用于分配页面的时间并不多。
  • 那么答案是什么? :s
【解决方案2】:

我怀疑在堆栈或堆上分配array 时,编译器只需为array 对齐,而在使用vector 的分配器时,它可能使用operator new,它必须返回适当对齐的内存任何类型。如果分配的内存恰好更好地对齐,允许更多的缓存命中/更大的读取,那么这似乎可以很容易地解释性能差异。

【讨论】:

  • +1 好主意。我已经尝试使用int 作为内部类型(结果相似),但我想知道使用其他类型是否会更好地对齐数组?也许值得尝试使用floatcharT* 等。此外,您的答案将解释为什么在-O0-O-O3 进行优化时仍然会出现速度差异。
【解决方案3】:

当简单的解释就足够时,不要搜索复杂的解释。这是一个优化器错误。普通的、固定大小的 C 风格堆栈分配数组的性能类似于 std::array,所以不要责怪 std::array 的实现。

【讨论】:

  • 我没有说你责怪 STL。我只是说你不应该,以防万一。顺便说一句,我已经用 -O2 进行了尝试,所有变体在我的机器上都有几乎相同的 performance.l。
  • 有趣...如果您尝试增加K?我在核心 i7 上运行,但仍然是笔记本电脑,因此可能需要更大的规模才能在更好的硬件上显现出来。无论如何,我很震惊数组向量对你来说并不比向量向量快——这对我来说很直观(当KN 大得多时)。是不是让你感到惊讶?
【解决方案4】:

我刚刚在我的桌面上使用 MSVC++ 2010 进行了尝试,除了 vectorvectors 的 5.0 秒之外,所有测试的时间都相同(1.6 秒)。

我会考虑查看您的库对 arrayvector 的实际实现,看看是否有任何明显的差异。

尝试用迭代器样式的循环替换索引样式的循环,看看是否会影响性能。

另外,尝试使用clock() 从程序内部为您的程序计时:除其他外,这可以让您判断代码的哪一部分表现不同。甚至可能值得在嵌套作用域中添加,这样您也可以为对象析构函数计时。

【讨论】:

    【解决方案5】:

    我突然想到的一件事是,堆栈上的这么大的对象可能会触发操作系统重新分配堆栈空间。尝试在 main 末尾转储 /proc/self/maps

    【讨论】:

    • 呵呵,操作系统真的可以做到吗?我认为重新分配堆栈会使程序可能拥有的任何指向堆栈对象的指针无效,从而导致程序可能崩溃......
    • @Jeremy:是的。重新分配不是问题,因为堆栈的地址位于堆的虚拟内存地址空间的另一端,并且使用 mmap 分配的东西。物理页面只能映射到末尾。
    • 对我来说最有趣的区别是堆栈分配的 std::array 和新分配的 std::array (情况 1 和 2)。
    • 我的机器(i5,gcc 4.7.1,-O3)的程序集差异在这里:ideone.com/udMVz。在我的机器上,堆栈版本需要 1.75 秒(平均超过 100 次运行),新分配的 std::array 需要 1.45 秒。我能看到的唯一区别是从第 15 行(标签 L2)开始的指令重新排序,但那是在算术循环之外。我还检查了堆叠数组是 16 字节对齐的。也许在填充数组时发生了几个页面错误并且Linux内核重新分配了堆栈?
    【解决方案6】:

    我看到的唯一大区别是您的数据存储方式不同。在前两种情况下,您的数据存储在一个巨大的块中。所有其他情况都存储指向矩阵中行的指针。我不太清楚为什么这会使您的代码更快,但它可能与查找和 CPU 预取有关。在迭代之前尝试缓存矩阵行,这样您就不需要为每个条目查找mat[k]。这可以使它更快,甚至速度。可能是您的编译器可以在 vector&lt;array&lt;T&gt;&gt; 情况下执行此操作,但不能在 array&lt;array&lt;T&gt;&gt; 情况下执行此操作。

    【讨论】:

    • 我认为array&lt;array&lt;T&gt; &gt;vector&lt;array&lt;T&gt; &gt; 都将它存储在一个大块中(除了vector 将块存储在堆上)。 array&lt;vector&lt;T&gt; &gt;vector&lt;vector&lt;T&gt; &gt; 执行您所说的更多操作(存储一组指针,每行一个)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-21
    • 1970-01-01
    • 2013-02-18
    • 2016-06-26
    • 2015-04-21
    • 1970-01-01
    相关资源
    最近更新 更多