【问题标题】:std::vector do extra operations when shifting elementsstd::vector 在移动元素时做额外的操作
【发布时间】:2013-09-21 19:54:58
【问题描述】:

我需要将排序后的元素连续存储在内存中,所以我想到了 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 需要将前一个组件移动到一个新的位置
移动赋值运算符 //OK,新组件需要移动到flat_set的前面

但是向量呢?

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

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

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

[Edit 2]:我用 boost::container::vector 做了同样的测试,输出(和速度!)与 flag_set 相同。微软实现vector有问题吗?哎哟

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

【问题讨论】:

  • 这可能是在向量中使用子函数的情况......这是一个优化的构建吗? (可以省略复制/移动构造函数的调用)
  • 哦,对不起,我忘了提这个。我使用的是 Visual Studio 2013,我的测试是使用默认的发布设置进行的,所以是的,优化已启用。
  • Boost 标头路径应该是boost 而不是Boost。两者都适用于 Windows,因为文件系统不区分大小写,但使用 Boost 只会产生不必要的可移植性问题。
  • 确实如此,但在我实际工作的项目中,我使用的是 DX11 和 win32 API,因此这里的可移植性并不是一个真正的问题。 ;)
  • 除非您现在已将代码粘贴到 SO 上,而且人们(例如我)必须修复在他们自己的平台上运行代码的路径。

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


【解决方案1】:

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

要在包含 N 个元素(容量足够)的向量的开头插入一个右值,典型的方法是:

  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 &gt; 0; --i)
    _M_data[i] = std::move(_M_data[i-1]);
  3. 临时移动构造并将其分配给索引 0
    _M_data[0] = T(std::move(arg));

这意味着您将看到一个移动构造,然后是 (N-1) 个移动分配(在您的情况下,向量只有一个元素,因此您在第 2 步中看不到任何内容),然后是移动构造和移动分配。

在第 3 步中构造临时对象,因为 emplaceinsert 使用相同的插入逻辑,所以它实际上使用零个或多个参数执行 T(std::move(args)...),在只有一个类型参数的情况下T 本来可以只做 _M_data[0] = std::move(arg); 并避免移动构造。

我不确定为什么最后会看到额外的移动分配,GCC 的标准库实现没有这样做,我不确定为什么需要它。您可以很容易地修改您的代码以打印正在构造/分配的对象的标识,以查看 哪些 元素被移动了两次,这可能会更清楚地说明它。

【讨论】:

  • 我在输出中添加了 10 个组件和被移动组件的 id,但是正如您所看到的,所有操作都与向量相乘。我不明白为什么,但是 flat_set 只调用一次赋值运算符,向量调用移动构造函数 + 赋值运算符两次。这可能与 flat_set 以良好的顺序移动组件的事实有关,而 vector 显然是以完全随机的方式移动它们。我真的迷路了,因为组件是有序的并连续存储在 flat_set 和 vector 中。
  • 哇,有趣,这确实是做两次!使用 GCC,输出几乎与 flat_set 相同,只是在最后一次移动分配之前添加了一个 Move constructor of component: 0 行,正如我在上面的回答中所解释的那样,它确实是 _M_data[0] = T(std::move(arg));,实际上 _M_data[0] = std::move(arg); 就足够了.我不知道微软图书馆在那里做什么。
  • 我尝试了 boost::container::vector,输出/性能与 flag_set 相同!是微软vector impl的问题?
  • 从我对微软向量实现的了解来看,与 libstdc++ 或 libc++ 相比,它太复杂了,所以我一点也不感到惊讶。也可能是 MSVC 在删除副本方面不如 GCC 好。
  • @SebastianRedl,我认为 GCC 不会发生任何省略,移动是通过引用或循环完成的,而不是按值返回,我认为没有任何内容符合省略的条件。
【解决方案2】:

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

来自 Microsoft Connect 的问候!

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

嗨,

感谢您报告此错误。我已经通过彻底检修来修复它 向量的正确性和性能,此修复将可用 在 VS“15”RC 中。以前,插入/放置称为 rotate() 某些情况。现在我们更智能地排序我们的动作, 避免旋转元素。

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

您可能会收到一般的“反馈项目已更新”通知 好吧,如果 Microsoft 进行了任何其他更改。

感谢您使用 Microsoft Connect!

问候,

Microsoft Connect 团队

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

如果您在访问上面的反馈链接时遇到问题,请转到 到http://connect.microsoft.com/help/default.aspx 页面举报 问题,在您的提交中,请确保粘贴副本 上面的链接到报告中。

万岁!很快就会修复。 :D 虽然有点晚了,但这仍然是一件好事。

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 我已经尝试过了,我得到了相同的输出,只是调用了一个补充移动构造函数。
    • reserve 的调用确保它不需要需要创建一个新向量,删除该调用不会改变所看到的行为。
    猜你喜欢
    • 1970-01-01
    • 2012-12-12
    • 1970-01-01
    • 2018-02-02
    • 1970-01-01
    • 2020-10-11
    • 2018-01-11
    • 2011-12-27
    • 2015-07-18
    相关资源
    最近更新 更多