当已知大小时,对向量添加元素进行基准测试

时间:2015-12-04 08:48:02

标签: c++ c++11 vector benchmarking push-back

我已经为向向量添加新元素做了一个小小的基准,我知道它的大小。

代码:

struct foo{
    foo() = default;
    foo(double x, double y, double z) :x(x), y(y), z(y){

    }
    double x;
    double y;
    double z;
};

void resize_and_index(){
    std::vector<foo> bar(1000);
    for (auto& item : bar){
        item.x = 5;
        item.y = 5;
        item.z = 5;
    }
}

void reserve_and_push(){
    std::vector<foo> bar;
    bar.reserve(1000);
    for (size_t i = 0; i < 1000; i++)
    {
        bar.push_back(foo(5, 5, 5));
    }
}

void reserve_and_push_move(){
    std::vector<foo> bar;
    bar.reserve(1000);
    for (size_t i = 0; i < 1000; i++)
    {
        bar.push_back(std::move(foo(5, 5, 5)));
    }
}

void reserve_and_embalce(){
    std::vector<foo> bar;
    bar.reserve(1000);
    for (size_t i = 0; i < 1000; i++)
    {
        bar.emplace_back(5, 5, 5);
    }
}

然后我将每个方法调用100000次。

结果:

resize_and_index: 176 mSec 
reserve_and_push: 560 mSec
reserve_and_push_move: 574 mSec 
reserve_and_embalce: 143 mSec

致电代码

const size_t repeate = 100000;
auto start_time = clock();
for (size_t i = 0; i < repeate; i++)
{
    resize_and_index();
}
auto stop_time = clock();
std::cout << "resize_and_index: " << (stop_time - start_time) / double(CLOCKS_PER_SEC) * 1000 << " mSec" << std::endl;


start_time = clock();
for (size_t i = 0; i < repeate; i++)
{
    reserve_and_push();
}
stop_time = clock();
std::cout << "reserve_and_push: " << (stop_time - start_time) / double(CLOCKS_PER_SEC) * 1000 << " mSec" << std::endl;


start_time = clock();
for (size_t i = 0; i < repeate; i++)
{
    reserve_and_push_move();
}
stop_time = clock();
std::cout << "reserve_and_push_move: " << (stop_time - start_time) / double(CLOCKS_PER_SEC) * 1000 << " mSec" << std::endl;


start_time = clock();
for (size_t i = 0; i < repeate; i++)
{
    reserve_and_embalce();
}
stop_time = clock();
std::cout << "reserve_and_embalce: " << (stop_time - start_time) / double(CLOCKS_PER_SEC) * 1000 << " mSec" << std::endl;

我的问题:

  1. 为什么我得到这些结果?是什么让emplace_back优于其他人?
  2. 为什么std :: move使性能稍差?
  3. 基准测试条件:

    • 编译器:VS.NET 2013 C ++编译器(/ O2最大速度优化)
    • 操作系统:Windows 8
    • 处理器:Intel Core i7-410U CPU @ 2.00 GHZ

    另一台机器(horstling):

    VS2013,Win7,Xeon 1241 @ 3.5 Ghz

    resize_and_index: 144 mSec
    reserve_and_push: 199 mSec
    reserve_and_push_move: 201 mSec
    reserve_and_embalce: 111 mSec
    

3 个答案:

答案 0 :(得分:1)

  

为什么我得到这些结果?是什么让emplace_back优于   其他

你得到了这些结果,因为你对它进行了基准测试,你必须得到一些结果:)。
在这种情况下,Emplace会做得更好,因为它直接在向量保留的内存位置创建/构造对象。因此,它不必首先在外部创建一个对象(临时可能),然后将其复制/移动到向量的保留位置,从而节省一些开销。

  

为什么std :: move使性能稍差?

如果你问为什么它比昂贵更昂贵,那么它将是因为它必须移动&#39;物体。在这种情况下,移动操作可以很好地减少复制。因此,复制操作必须花费更多时间,因为此复制操作不会发生在emplace案例中。
您可以尝试挖掘生成的汇编代码,看看究竟发生了什么。
另外,我不认为将其余功能与&#39; resize_and_index&#39;进行比较。是公平的。在其他情况下,对象可能会被多次实例化。

答案 1 :(得分:1)

首先,reserve_and_push和reserve_and_push_move在语义上是等效的。你构造的临时foo已经是一个rvalue(已经使用了push_back的rvalue引用重载);将它包装在一个移动中不会改变任何东西,除非可能模糊编译器的代码,这可以解释轻微的性能损失。 (虽然我认为它更可能是噪音。)此外,你的类具有相同的复制和移动语义。

其次,如果将循环体写为

,则resize_and_index变体可能更为优化
item = foo(5, 5, 5);

虽然只有剖析才会显示出来。关键是编译器可能会为三个单独的分配生成次优代码。

第三,你也应该试试这个:

std::vector<foo> v(100, foo(5, 5, 5));

第四,这个基准测试对编译器非常敏感,他们意识到这些函数实际上都没有做任何事情,只是简单地优化它们的完整体。

现在进行分析。请注意,如果您真的想知道发生了什么,则必须检查编译器生成的程序集。

第一个版本执行以下操作:

  1. 为1000个foos分配空间。
  2. 循环并默认构造每一个。
  3. 遍历所有元素并重新分配值。
  4. 这里的主要问题是编译器是否意识到第二步中的构造函数是无操作,并且它可以省略整个循环。装配检查可以显示出来。

    第二个和第三个版本执行以下操作:

    1. 为1000个foos分配空间。
    2. 1000次:
      1. 构建临时foo对象
      2. 确保仍有足够的分配空间
      3. 移动(对于你的类型,相当于一个副本,因为你的类没有特殊的移动语义)临时进入分配的空间。
      4. 增加矢量的大小。
    3. 这里有很多优化空间用于编译器。如果它将所有操作内联到同一个函数中,它可以意识到大小检查是多余的。然后它可以意识到你的移动构造函数不能抛出,这意味着整个循环是不可中断的,这意味着它可以将所有增量合并为一个赋值。如果它没有内联push_back,它必须将临时文件放在内存中并传递对它的引用;有很多方法可以让它更有效率,但它不太可能。但/ / p>

      但除非编译器执行其中某些操作,否则我希望此版本比其他版本慢很多。

      第四个版本执行以下操作:

      1. 为1000个foos分配足够的空间。
      2. 1000次:
        1. 确保仍有足够的分配空间
        2. 使用带有三个参数的构造函数
        3. 在分配的空间中创建一个新对象
        4. 增加尺寸
      3. 这与前面的类似,有两点不同:第一,MS标准库实现push_back的方式,它必须检查传递的引用是否是对向量本身的引用;这极大地增加了功能的复杂性,抑制了内联。 emplace_back没有这个问题。其次,emplace_back获取三个简单的标量参数,而不是对堆栈对象的引用;如果函数没有内联,则传递效率要高得多。

        除非您专门使用Microsoft的编译器,否则我强烈建议您与其他编译器(及其标准库)进行比较。我也认为我建议的版本会击败你的所有四个版本,但我没有对此进行分析。

        最后,除非代码对性能非常敏感,否则应编写最易读的版本。 (这是我的版本获胜的另一个地方,IMO。)

答案 2 :(得分:0)

我不确定reserve_and_push和reserve_and_push_move之间的差异是否只是噪音。我使用g ++ 4.8.4做了一个简单的测试,注意到可执行文件大小/附加汇编指令的增加,尽管理论上在这种情况下编译器可以忽略std :: move。