std :: vector在移动元素时执行额外的操作

时间:2013-09-17 11:35:10

标签: c++ boost c++11 vector move

我需要在内存中连续存储已排序的元素,所以我想到了std :: vector和boost :: flat_set。我已经尝试了两种,我检查了它们的性能,并且尽管使用std :: vector后插入更快一点,使用boost :: flat_set可以更快地进行前插入。这是我的测试代码:

#include <iostream>
#include <vector>
#include <boost/container/flat_set.hpp>
#include <boost/chrono.hpp>
#include <windows.h>

// Just a basic movable object for my tests
class Component
{
public :
    Component( void ) :
        id( 0 ),
        name( "default" ),
        data( 100 )
    {
    }

    Component( uint32_t id ) :
        id( id ),
        name( "default" ),
        data( 100 )
    {
    }

    Component( Component&& component ) throw() :
        id( std::move( component.id ) ),
        name( std::move( component.name ) ),
        data( std::move( component.data ) )
    {
    }

    Component& operator=( Component&& component ) throw()
    {
        id = std::move( component.id );
        name = std::move( component.name );
        data = std::move( component.data );
        return ( *this );
    }

    uint32_t    get_id( void ) const
    {
        return ( id );
    }

private :
    uint32_t                id;
    std::string             name;
    std::vector< uint32_t > data;
};

// This object can be sorted
inline bool operator<( const Component& component1, const Component& component2 )
{
    return ( component1.get_id() < component2.get_id() );
}

#define COMP_NB 1000000

int main( void )
{
    /*******************************/
    /* Test vector insertion speed */
    /*******************************/
    std::vector< Component > vector;

    vector.reserve( COMP_NB + 1 );

    std::cout << "Push back components in the vector: ";
    auto startTime = boost::chrono::steady_clock::now();

    // Back insertion
    for ( uint32_t i = 0; i < COMP_NB; ++i )
    {
        vector.push_back( Component( i + 1 ) );
    }

    auto thisTime = boost::chrono::steady_clock::now();
    std::cout << boost::chrono::duration_cast< boost::chrono::milliseconds >( thisTime - startTime ).count() << "ms" << std::endl;

    std::cout << "Insert one component at the beginning of the vector: ";
    startTime = boost::chrono::steady_clock::now();

    // Front insertion (all components are shifted)
    vector.insert( vector.begin(), Component( 0 ) );

    thisTime = boost::chrono::steady_clock::now();
    std::cout << boost::chrono::duration_cast< boost::chrono::milliseconds >( thisTime - startTime ).count() << "ms" << std::endl;

    /*********************************/
    /* Test flat_set insertion speed */
    /*********************************/
    boost::container::flat_set< Component > flat_set;

    flat_set.reserve( COMP_NB + 1 );

    std::cout << "Push back components in the flat_set: ";
    startTime = boost::chrono::steady_clock::now();

    // Back insertion
    for ( uint32_t i = 0; i < COMP_NB; ++i )
    {
        flat_set.insert( Component( i + 1 ) );
    }

    thisTime = boost::chrono::steady_clock::now();
    std::cout << boost::chrono::duration_cast< boost::chrono::milliseconds >( thisTime - startTime ).count() << "ms" << std::endl;

    std::cout << "Insert one component at the beginning of the flat_set: ";
    startTime = boost::chrono::steady_clock::now();

    // Front insertion (all components are shifted)
    flat_set.insert( Component( 0 ) );

    thisTime = boost::chrono::steady_clock::now();
    std::cout << boost::chrono::duration_cast< boost::chrono::milliseconds >( thisTime - startTime ).count() << "ms" << std::endl;

    system( "PAUSE" );

    return ( 0 );
}

输出:

  

推回向量中的组件:852ms
  在向量的开头插入一个组件:59ms
  推回flat_set中的组件:912ms
  在flat_set的开头插入一个组件:16ms

使用flat_set,前插入速度提高了3.6倍!所以我跑了另一个测试,因为我想看看我的移动功能是否被使用,我发现了一些奇怪的东西。这是新代码:

#include <iostream>
#include <vector>
#include <boost/container/flat_set.hpp>
#include <boost/chrono.hpp>
#include <windows.h>

// Just a basic movable object for my tests
class Component
{
public :
    Component( void ) :
        id( 0 ),
        name( "default" ),
        data( 100 )
    {
        std::cout << "Default constructor" << std::endl;
    }

    Component( uint32_t id ) :
        id( id ),
        name( "default" ),
        data( 100 )
    {
        std::cout << "Custom constructor" << std::endl;
    }

    Component( Component&& component ) throw() :
        id( std::move( component.id ) ),
        name( std::move( component.name ) ),
        data( std::move( component.data ) )
    {
        std::cout << "Move constructor" << std::endl;
    }

    Component& operator=( Component&& component ) throw()
    {
        std::cout << "Move assignment operator" << std::endl;
        id = std::move( component.id );
        name = std::move( component.name );
        data = std::move( component.data );
        return ( *this );
    }

    uint32_t    get_id( void ) const
    {
        return ( id );
    }

private :
    uint32_t                id;
    std::string             name;
    std::vector< uint32_t > data;
};

// This object can be sorted
inline bool operator<( const Component& component1, const Component& component2 )
{
    return ( component1.get_id() < component2.get_id() );
}

#define COMP_NB 1

int main( void )
{
    /*******************************/
    /* Test vector insertion speed */
    /*******************************/
    std::vector< Component > vector;

    vector.reserve( COMP_NB + 1 );

    std::cout << "-- Push back one component in the vector: " << std::endl;

    // Back insertion
    for ( uint32_t i = 0; i < COMP_NB; ++i )
    {
        vector.push_back( Component( i + 1 ) );
    }

    std::cout << "-- Insert one component at the beginning of the vector: " << std::endl;

    // Front insertion (the other component is shifted)
    vector.insert( vector.begin(), Component( 0 ) );

    std::cout << std::endl;

    /*********************************/
    /* Test flat_set insertion speed */
    /*********************************/
    boost::container::flat_set< Component > flat_set;

    flat_set.reserve( COMP_NB + 1 );

    std::cout << "-- Push back one component in the flat_set: " << std::endl;

    // Back insertion
    for ( uint32_t i = 0; i < COMP_NB; ++i )
    {
        flat_set.insert( Component( i + 1 ) );
    }

    std::cout << "-- Insert one component at the beginning of the flat_set: " << std::endl;

    // Front insertion (the other component is shifted)
    flat_set.insert( Component( 0 ) );

    system( "PAUSE" );

    return ( 0 );
}

新输出:

  

- 向后推回向量中的一个组件:
  定制构造函数
  移动构造函数
   - 在向量的开头插入一个组件:
  定制构造函数
  移动构造函数
  移动构造函数
  移动赋值运算符
  移动赋值运算符
  
   - 推回flat_set中的一个组件:
  定制构造函数
  移动构造函数
   - 在flat_set的开头插入一个组件:
  定制构造函数
  移动构造函数
  移动赋值运算符

这里有些奇怪的事。 Flat_set行为似乎很正常:

  

- 在flat_set的开头插入一个组件:
  自定义构造函数//好的,我问了一个新组件的创建   移动构造函数//确定,flat_set需要将前一个组件移动到新位置
  移动赋值运算符//确定,需要将新组件移动到flat_set的前面

但是矢量呢?

  

- 在向量的开头插入一个组件:
  自定义构造函数//好的,我问了一个新组件的创建   移动构造函数//好的,向量需要将前一个组件移动到新位置
  移动构造函数//等等......什么?   移动赋值运算符// OK,新组件需要移动到向量的前面
  移动赋值运算符// Wtf?

我尝试了更多组件,并且向量行为是相同的:它继续执行所有移动操作两次。为什么?可以避免吗?如果没有,我应该坚持flat_set(我有时需要移动我的数据)?

[编辑]:这是输出,其中10个组件插入后面,1个插入前面,并且正在构造或移动组件的id: http://pastebin.com/SzT5M8yP

[编辑2]:我使用boost :: container :: vector进行了相同的测试,输出(和速度!)与flag_set相同。这是向量的Microsoft实现的问题吗? O.o

[编辑3]:提交给Microsoft的问题:https://connect.microsoft.com/VisualStudio/feedback/details/801205/std-vector-do-extra-operations-when-shifting-elements

3 个答案:

答案 0 :(得分:2)

它没有进行两次所有移动操作,如果你的向量中有多个元素,你会看到只有一些操作发生两次。

要在N个元素(具有足够容量)的向量的开头插入一个rvalue,典型的方法是:

  1. 从索引N-1的元素移动索引N处的构造新元素    new (_M_data+N) T(_M_data[N-1]);
       _M_size += 1;
  2. 将索引0到N-2的赋值元素移动到索引1到N-1
       for (int i = N-1; i > 0; --i)
       _M_data[i] = std::move(_M_data[i-1]);
  3. 移动构造临时并移动将其指定为索引0
       _M_data[0] = T(std::move(arg));
  4. 这意味着您将看到一个移动构造,然后是(N-1)移动分配(在您的情况下,向量只有一个元素,因此您在步骤2中看不到任何内容),然后是移动构造和移动分配。 / p>

    在第3步中构造临时因为emplace使用相同的插入逻辑作为insert,所以它实际上T(std::move(args)...)具有零个或多个args,在那里只是T类型的一个参数,它可以只执行_M_data[0] = std::move(arg);并避免移动构造。

    我不确定为什么你最后会看到一个额外的移动任务,GCC的标准库实现不会这样做,我不确定为什么需要它。您可以非常轻松地修改代码以打印正在移动的对象的构造/分配的标识,以查看哪些元素被移动两次,这可能会对它有更多的了解。

答案 1 :(得分:0)

3年后,这就是我在邮箱中收到的内容:

  

来自Microsoft Connect的问候!

     

此通知是为反馈项生成的:std :: vector do   转移您提交的元素时的额外操作   theMicrosoft Connect网站。

     

您好,

     

感谢您报告此错误。我通过彻底检修修复了它   矢量的正确性和性能,这个修复程序将可用   在VS&#34; 15&#34; RC。以前,插入/放置称为rotate()in   某些情况。现在我们更智能地对我们的行为进行排序,   避免需要旋转元素。

     

Stephan T. Lavavej Visual C ++库的首席软件工程师   stl@microsoft.com

     

您可能会收到一般信息&#34;反馈项目已更新&#34;通知为   好吧,如果微软做出任何其他改动。

     

感谢您使用Microsoft Connect!

     

此致

     

Microsoft Connect小组

     

请不要直接回复此邮件,因为它是从中生成的   不受监控的电子邮件帐户。如果您有与您相关的评论   反馈,请在评论部分输入(发表评论至   Microsoft)您的反馈项目导航到的反馈项目   上面的链接。

     

如果您在访问上面的反馈链接时遇到问题,请转到   到http://connect.microsoft.com/help/default.aspx页面进行报告   问题,在您的提交中,请务必粘贴一份副本   将上面的内容链接到报告中。

乌拉!它很快就会修好。 :D认为它有点晚了,但它仍然是一件好事。

答案 2 :(得分:-1)

我的猜测是std::vector通过分配一个新的向量,将当前的奇异值复制到新的向量,然后插入新的值来增加向量的大小。尝试删除对reserve的调用,以便std::vector的内存管理为您服务。