【问题标题】:Why is vector(size) slower than new[]?为什么vector(size)比new []慢?
【发布时间】:2011-06-07 23:43:45
【问题描述】:

我正在对一些 STL 算法进行基准测试,我对以下代码所花费的时间感到惊讶:(我用 time 命令测量了 g++ 编译的代码 [无优化])

#include <vector>
struct vec2{
    int x, y;
    vec2():x(0), y(0) {}
};
int main(int argc, char* argv[]){
    const int size = 200000000;
    std::vector<vec2> tab(size); //2.26s
//  vec2* tab = new vec2[size]; //1.29s
//  tab[0].x = 0;
//  delete[] tab;
    return 0;
}

向量初始化所用的时间是 2.26 秒,而 new(和 delete)需要 1.29 秒。向量 ctor 做了什么会花费这么长时间? new[] 在每个元素上调用构造函数,就像 vector ctor 那样,对吧?

然后我用-O3编译,一切都快了,但是两个代码之间还是有差距的。 (我分别得到了 0.83s 和 0.75s)

有什么想法吗?

【问题讨论】:

  • 除了 DeadMG 所说的之外,做“一次性”基准测试(如您所见,没有优化的基准测试)是没有意义的,因为缓存未命中和 co 会导致统计波动。您应该多次重复基准测试(在同一过程中),测量每个测试的时间并计算平均值和标准偏差。只有在获得这些数据后,您才能进行合理的比较:如果两次差异小于您测量的不确定性(结合 RSS 中的两个标准偏差计算得出),则没有显着差异。
  • 请注意,由于您的代码实际上只引用tab[0],因此编译器只分配单个元素是完全合法的。实际上,由于您只写入tab[0] 而从不回读值,而且它不是volatile,因此编译器完全删除输出中的main() 的主体是完全合法的,因为它没有可观察的副作用。
  • 在 Visual Studio 中尝试了这个测试,得到了相反的结果(向量的最佳时间大约好 10%),但测量噪声 (~20-25%) 大于差异。因此,对于实际应用而言,这些测量结果确实没有实际意义。
  • “我是在进行基准测试...没有优化” 那么这完全没有意义。这就像问两辆车中哪一辆的最高速度更高......同时迫使它们保持空档。
  • @GMan :对非优化代码进行基准测试确实有用:当我调试我的程序时,代码没有优化,调试速度更快总是好的。但我同意最相关的是基准优化代码。

标签: c++ stl memory-management


【解决方案1】:

速度取决于实现,但最有可能导致矢量变慢的原因是矢量无法默认​​构造其元素。向量元素始终是复制构造的。例如

std::vector<vec2> tab(size);

实际上被解释为

std::vector<vec2> tab(size, vec2());

即第二个参数从默认参数中获取其值。然后该向量分配原始内存并将这个从外部传递的默认构造元素复制到新向量的每个元素中(通过使用复制构造函数)。这通常比直接默认构造每个元素要慢(就像new[] 所做的那样)。

用代码草图说明区别,new vec2[size] 大致相当于

vec2 *v = (vec2 *) malloc(size * sizeof(vec2));

for (size_t i = 0; i < size; ++i)
  // Default-construct `v[i]` in place
  new (&v[i]) vec2();

return v;

vector&lt;vec2&gt;(size) 大致相当于

vec2 source; // Default-constructed "original" element

vec2 *v = (vec2 *) malloc(size * sizeof(vec2));

for (size_t i = 0; i < size; ++i)
  // Copy-construct `v[i]` in place
  new (&v[i]) vec2(source);

return v;

根据实施情况,第二种方法可能会变慢。

虽然速度上的两倍差异很难证明是合理的,但是对未优化的代码进行基准测试也没有任何意义。您在优化代码中观察到的不太显着的差异正是在这种情况下人们可能合理预期的结果。

【讨论】:

  • 我打赌编译器内联了默认构造函数,可能是“将内存块初始化为零”操作的特殊情况。
  • 请注意,在 C++0x 中,调用向量的单参数构造函数默认初始化它创建的元素,而不是复制“模板”。
【解决方案2】:

两个版本都初始化内存。

正如一些人所指出的,向量使用复制构造,而数组使用默认构造函数。您的编译器似乎比前者更能优化后者。

请注意,在现实生活中,您很少希望一举初始化如此庞大的数组。 (一堆零有什么用?显然你最终打算在里面放些别的东西……而且初始化数百兆字节对缓存非常不友好。)

相反,您可以编写如下内容:

const int size = 200000000;
std::vector<vec2> v;
v.reserve(size);

然后,当您准备好将一个真实元素放入向量中时,您可以使用v.push_back(element)reserve() 分配内存而不初始化它; push_back() 复制构造到保留空间中。

或者,当您想将新元素放入向量中时,可以使用v.resize(v.size()+1),然后修改元素v.back()。 (这就是“池分配器”的工作方式。)虽然这个序列会初始化元素然后覆盖它,但它都会发生在 L1 缓存中,这几乎与根本不初始化它一样快。

因此,为了公平比较,请尝试使用大向量(使用 reserve)与创建一系列不同项目的数组。您应该会发现向量更快。

【讨论】:

    【解决方案3】:

    在分析了这两种情况下 VC++ 生成的程序集后,我发现了以下内容。编译器几乎内联了所有内容,并为内存分配后的初始化生成了非常相似的循环。在向量内循环的情况下是这样的:

    013E3FC0  test        eax,eax  
    013E3FC2  je          std::_Uninit_def_fill_n<vec2 *,unsigned int,vec2,std::allocator<vec2>,vec2>+19h (13E3FC9h)  
    013E3FC4  mov         dword ptr [eax],edx  
    013E3FC6  mov         dword ptr [eax+4],esi  
    013E3FC9  add         eax,8  
    013E3FCC  dec         ecx  
    013E3FCD  jne         std::_Uninit_def_fill_n<vec2 *,unsigned int,vec2,std::allocator<vec2>,vec2>+10h (13E3FC0h)  
    

    edxesi 寄存器在循环外被归零:

    00013FB5  xor         edx,edx  
    00013FB7  xor         esi,esi  
    00013FB9  lea         esp,[esp]  
    

    如果是new[],内循环如下所示:

    009F1800  mov         dword ptr [ecx],0  
    009F1806  mov         dword ptr [ecx+4],0  
    009F180D  add         ecx,8  
    009F1810  dec         edx  
    009F1811  jns         main+30h (9F1800h)  
    

    差异非常小,在vector 的情况下还有更多的指令,但来自寄存器的movs 可能也更快。由于在大多数现实生活中,构造函数所做的不仅仅是分配零,因此这种差异几乎不会被注意到。所以这个测试的价值是值得怀疑的。

    【讨论】:

    • 有趣;谢谢你。第一个代码显然效率低下; jne 回到 test + je 不可能评估为真......所以它可能使 jne 循环回到第一个 mov。另外,你确定零常数的移动比寄存器快吗?
    • @Nemo:我不知道实际的mov 性能是否易于在现代处理器上分析。但是查看我的旧 Intel 书籍,386 在 2 个周期内完成了 mov mem, regmov mem, immed
    猜你喜欢
    • 2023-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-22
    • 1970-01-01
    • 2011-03-23
    相关资源
    最近更新 更多