为什么 array<T, N> 会比 vector<T> 更慢?

17

今天我决定对比一下 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 原生的 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: 所有版本都输出相同的结果。

NB 2: 只是为了演示 std::array<std::array<double,N>,K>std::vector<std::array<double,N>>std::vector<std::vector<double>> 之间的运行时差异不仅仅是在分配时进行赋值/初始化的原因,只分配集合的运行时间(即注释掉 tot 的计算和打印)分别为0.000s,0.000s和0.004s。

NB 3: 每种方法都单独编译和运行(而不是在同一个可执行文件中反复计时),以避免缓存中的不公平差异。

NB 4:
数组的汇编代码: http://ideone.com/SM8dB
向量数组的汇编代码: http://ideone.com/vhpJv
向量向量的汇编代码: http://ideone.com/RZTNE

NB 5: 只是为了明确,我绝对不打算批评STL。我非常喜欢STL,并且不仅经常使用它,还学到了很多关于C++的微妙和出色特性的细节。相反,这是一种智力追求:我只是计时以学习高效的C++设计原则。

此外,归因于运行时间差异的起因很难分解,因为启用优化时,可能是编译器优化导致代码变慢而不是变快。关闭优化时,可能是不必要的复制操作(在生产代码中将被优化掉并且永远不会执行),这可能对某些数据类型的偏见更大。

如果你像我一样好奇,我会很乐意得到你的帮助来解决这个问题。


5
尝试将迭代次数设置为1000,以获得更准确的值。那些看起来可能只是延迟值。 - Cole Tobin
@ColeJohnson 你是指 N=1000 还是 K=1000?如果你是指 N=1000,那么数组的向量几乎与向量的向量相同(因为不展开循环的开销非常高)。使用 N=1 会导致向量数组和向量向量之间的差异更大,因为向量数组应该基本上转换为双精度向量。因此,比较数组和向量的最有趣的情况是 K << N(在数学意义上而不是位移意义上的 <<)。 - user
1
@Oliver:就是说,在进行vector测试之后再进行array测试。等等,你是在完全不同的程序中测试它们吗?如果是这样,那我误解了。 - user541686
汇编清单与程序不对应。例如,双重输出等内容缺失。 - ergosys
@ergosys 我不确定为什么会这样。我刚刚编译了汇编代码并重新上传了它(还有向量的汇编代码)。 - user
显示剩余11条评论
6个回答

3
考虑第二个和第三个测试。从概念上讲,它们是相同的:在堆上分配 K * N * sizeof(double) 个字节,然后以完全相同的方式访问它们。那为什么会有不同的时间呢?
所有“更快”的测试都有一个共同点:它们都使用了 new[]。所有较慢的测试都是使用 new 或栈上分配的。 vector 可能在底层使用了 new[]。唯一明显的原因是,new[]new 的实现比预期的更不同。
我要建议的是,new[] 会回退到 mmap 并直接在页面边界上进行分配,从而加快对齐速度,而另外两种方法则不会在页面边界上进行分配。
考虑使用操作系统分配函数来直接映射已提交的页面,然后将 std::array<std::array<double, N>, K> 放入其中。

我尝试使用 std::array<std::array<double, N>, K > & mat = *new std::array<std::array<double, N>, K >[1]; 强制使用 new[],但它与数组的运行时相同... - user
10
除非你提供一个分配器来执行此操作,否则 vector 不会在内部使用 new[]。它使用分配器提供的任何内容。除非另有指定,它将使用 std::allocator<T>。而 std::allocator<T> 将使用 operator new 来分配原始内存。 - Jerry Coffin
哦,是的。忘记了分配器。 - Puppy
对此,对齐加速并不能解释。总内存使用量为2000048≈640K,因此分配页面所花费的时间不多。 - Potatoswatter
那么答案是什么?:s - intrigued_66

3

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


+1 不错的想法。我已经尝试过使用 int 作为内部类型(结果类似),但我想知道是否使用其他类型会更好地对齐数组?也许值得尝试使用 floatcharT* 等。此外,你的回答可以解释为什么即使在 -O0-O-O3 优化下仍然存在速度差异。 - user

1
不要在简单的解释无法解决问题时寻找复杂的解释。这是一个优化器的错误。普通的固定大小的C风格栈分配数组可以提供类似于std::array的性能,所以不要责怪std::array的实现。

我没有说你指责了STL。我只是想说,以防万一你不应该这样做。顺便说一下,我已经用-O2尝试过了,所有变体在我的机器上的性能几乎都相同。 - n. m.
有趣...也许你可以尝试增加K的值?我正在使用一台i7核心的笔记本电脑,但可能需要更大的规模才能在更好的硬件上显现。无论如何,我很惊讶向量数组并不比向量向量更快--这对我来说是直观的(当K远大于N时)。这难道不让你感到惊讶吗? - user

1

我刚在我的桌面电脑上使用MSVC++ 2010尝试了一下,除了vectorvectors外,所有测试的时间都是相同的(1.6秒)。

我建议您查看库中arrayvector的实际实现,看看是否有任何明显的差异。

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

此外,尝试使用clock()在程序内部计时:这将让您知道代码的哪个部分表现不同。甚至值得添加一个嵌套作用域,以便您也可以计时对象析构函数。


0
我想到的一件事是,一次在堆栈上分配如此大的对象可能会触发操作系统重新分配堆栈空间。尝试在main函数结束时转储/proc/self/maps。

2
哦,这是操作系统真的能做到的吗?我认为重新分配堆栈会使程序可能拥有的指向堆栈对象的指针无效,从而导致程序崩溃... - Jeremy Friesner
1
为确保这不是使用堆栈的原因,我在上面进行了一个测试,在那里我在堆上分配了数组的数组 - 我得到了相同的运行时间。 - user
@Jeremy:是的。重新分配并不是一个问题,因为栈的地址位于虚拟内存地址空间的另一端,与堆和使用mmap分配的内容不同。物理页面可以映射到末尾。 - notlostyet
对我来说最有趣的区别是堆栈分配的std::array和新分配的std::array(情况1和2)之间的区别。 - notlostyet
1
我的机器(i5,gcc 4.7.1,-O3)的汇编差异在这里:http://ideone.com/udMVz。在我的机器上,堆栈版本需要1.75秒(100次运行的平均值),而新分配的std :: array需要1.45秒。我唯一能看到的区别是从第15行(标签L2)开始的指令重新排序,但这在算术循环之外。我还检查了堆栈数组是否为16字节对齐。也许由于数组被填充了几个页面故障,Linux内核会重新分配堆栈? - notlostyet
@notlostyet 感谢您的帮助!我认为这是更多的编译器优化,因为在堆上分配数组的版本与堆栈版本的速度仍然相同(使用*new std::array<std::array<double, N>, K >;进行分配)。 - user

0
我唯一看到的大区别是你的数据存储方式不同。在前两种情况下,你的数据存储在一个巨大的块中。而其他情况则将指针存储在矩阵的行中。我不太清楚为什么这会使你的代码更快,但可能与查找和CPU预取有关。在迭代之前尝试缓存矩阵行,这样你就不需要为每个条目查找mat[k]。这可能会使它更快,并且速度会变得更加平稳。可能是你的编译器可以在vector<array<T>>情况下做到这一点,但在array<array<T>>情况下却不能。

我认为 array<array<T> >vector<array<T> > 都将其存储在一个大块中(除了 vector 将该块存储在堆上)。array<vector<T> >vector<vector<T> > 更多地实现了您所说的内容(存储每行的指针集合)。 - user

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接