当大小已知时,将元素添加到向量的基准测试

Benchmarking adding elements to vector when size is known

本文关键字:添加 元素 向量 基准测试      更新时间:2023-10-16

我制作了一个小的基准测试,用于向我知道其大小的向量添加新元素。

代码:

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. 为什么我得到这些结果?是什么让templateback胜过其他人
  2. 为什么std::move会使性能稍差

基准条件:

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

另一台机器(通过骑马):

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

首先,reserve_and_push和reserve_ad_push_move在语义上是等价的。您构造的临时foo已经是一个右值(push_back的右值引用重载已经使用);在移动中包装它不会改变任何东西,除了可能会混淆编译器的代码,这可能解释了轻微的性能损失。(尽管我认为这更可能是噪音。)此外,您的类具有相同的复制和移动语义。

其次,如果将循环的主体写为,那么resize_and_index变体可能更为优化

item = foo(5, 5, 5);

尽管只有评测才会显示这一点。重点是编译器可能会为这三个单独的赋值生成次优代码。

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

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

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

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

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

  1. 为1000个foo分配空间
  2. 循环和默认构造各一个
  3. 在所有元素上循环并重新指定值

这里的主要问题是编译器是否意识到第二步中的构造函数是非操作的,并且它可以省略整个循环。装配检查可以表明。

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

  1. 为1000个foo分配空间
  2. 1000次:
    1. 构造一个临时foo对象
    2. 确保仍有足够的分配空间
    3. 将临时移动(对于您的类型,相当于一个副本,因为您的类没有特殊的移动语义)到分配的空间中
    4. 增加矢量的大小

编译器在这里有很大的优化空间。如果它将所有操作内联到同一个函数中,它可能会意识到大小检查是多余的。然后它可能会意识到你的move构造函数不能抛出,这意味着整个循环是不间断的,也意味着它可以将所有增量合并到一个赋值中。如果它没有内联push_back,那么它必须将临时文件放在内存中并向其传递引用;有很多方法可以在特殊情况下提高效率,但不太可能。

但是,除非编译器执行其中的一些操作,否则我预计这个版本会比其他版本慢很多。

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

  1. 为1000个foo分配足够的空间
  2. 1000次:
    1. 确保仍有足够的分配空间
    2. 使用带有三个参数的构造函数,在分配的空间中创建一个新对象
    3. 增加大小

这与前面的类似,有两个不同:首先,MS标准库实现push_back的方式,它必须检查传递的引用是否是对向量本身的引用;这大大增加了函数的复杂性,从而抑制了内联。template_back没有这个问题。其次,template_back获得三个简单的标量参数,而不是对堆栈对象的引用;如果函数没有内联,那么传递.的效率会高得多

除非你只使用微软的编译器,否则我强烈建议你与其他编译器(及其标准库)进行比较。我还认为我建议的版本会击败你的四个版本,但我还没有对此进行介绍。

最后,除非代码确实对性能敏感,否则您应该编写可读性最强的版本。(这是我的版本获胜的另一个地方。)

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

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

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

如果你问为什么它比模板更昂贵,那可能是因为它必须"移动"物体。在这种情况下,移动操作可以很好地简化为复制。因此,一定是复制操作花费了更多的时间,因为此复制不是针对模板情况进行的。
您可以尝试挖掘生成的程序集代码,看看到底发生了什么。
此外,我认为将其余函数与resize_and_index进行比较是不公平的。在其他情况下,对象可能被实例化不止一次。

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