【问题标题】:Initializing a vector with initial value efficiently有效地初始化具有初始值的向量
【发布时间】:2015-10-25 22:08:10
【问题描述】:

给定这种类型的测试:

struct Counter
{
    static int count;

    Counter(int)
    {
        count++;
    }

    Counter(const Counter&)
    {
        count++;
    }

    Counter(Counter&&) noexcept
    {

    }
};

int Counter::count = 0;

假设我们有以下内容:

std::vector<Counter> vec(5, 0);

根据 VS2015,创建了 6 个Counter 对象。我知道只有 5 个 permanent 对象。为什么编译器不将构造函数参数中的对象置位,或者将临时对象移到第一个位置,然后从中复制其余部分?

即使向量的初始大小设置为 0,仍然会创建 1 个对象。

std::vector<Counter> vec(0, 0);

如果在运行时才知道大小并且通常为 0(无操作)并且容器中的类型复制或构造成本很高,这可能很重要。

在一个语句中初始化向量通常很方便,尤其是当它们是初始化列表中的类成员或常量时。我怎样才能像下面的代码一样高效地做到这一点:

std::vector<Counter> vec;
vec.reserve(size);
for (size_t i = 0; i < size; i++)
{
    vec.emplace_back(0);
}

它只构造与存储在向量中一样多的包含对象。

【问题讨论】:

  • 向量构建后应该只有5个对象。向析构函数添加一个计数器减量操作,你应该得到正确的计数。
  • @DanielStrul 我对随着时间的推移构造的对象总数感兴趣。
  • 这是一个类似的问题:stackoverflow.com/questions/6488323/…,答案很不幸。
  • @AlanStokes 我在问题中举了一个例子。大 O 可能会产生误导。如果每次平均元素是 4 个,那么分配的峰值对象将增加 25%。例如,如果对象很大,这可能足以填满 CPU 缓存。我并不是说这通常是一个问题,但在某些情况下可能会出现问题,标准库应该针对这些情况进行很好的优化。
  • 这个关于避免临时性的想法是关于一个极端情况,即要求方便的表示法进行非常边界线的边际优化。对于使用该临时来初始化一堆向量项的通常情况,临时无关紧要。在你想避免它的地方,你可以通过创建一个具有必要容量的向量,而不是使用方便的符号。标准库不能为每个不切实际的用例提供方便的符号。它只提供基本功能和对常用用途的支持。

标签: c++


【解决方案1】:

您可以简单地定义一个函数,以您想要的方式创建一个向量。

作为一个函数,初始化代码是异常安全的。

#include <iostream>
#include <vector>
#include <stddef.h>     // ptrdiff_t
#include <utility>      // std::forward
using namespace std;

struct Counter
{
    static int n_constructor_calls;

    Counter( int )
    {
        ++n_constructor_calls;
    }

    Counter( Counter const& )
    {
        ++n_constructor_calls;
    }

    Counter( Counter&& ) noexcept
    {
        ++n_constructor_calls;
    }
};

int Counter::n_constructor_calls = 0;

//--------------------------------------

using Size = ptrdiff_t;
using Index = Size;

template< class Item, class... Args >
auto make_vector( Size const n, Args&&... args )
    -> vector<Item>
{
    vector<Item>    result;
    result.reserve( n );
    for( Index i = 0; i < n; ++i )
    {
        result.emplace_back( forward<Args>( args )... );
    }
    return result;
}

auto main() -> int
{
    auto vec = make_vector<Counter>( 5, 42 );
    cout << Counter::n_constructor_calls << " constructor calls.\n";
}

(这会输出“5 个构造函数调用”。)

您基本上会问,为什么不定义 vector 构造函数来执行此操作,

为什么编译器不将构造函数参数中的对象置位,或者将临时对象移动到第一个位置,然后从中复制其余部分?

一个原因是这个构造函数是在 C++11 中引入移动语义之前定义的。

相对于非常庞大的现有 C++ 代码库,引入额外的构造函数(这会改变重载行为)或更改现有构造函数的行为是昂贵的。

【讨论】:

    【解决方案2】:

    我想它是有效的,根据您的定义

    测试不同的版本;

    Copies: 100000005 , Construct: 1, Equal copies 0
    real    0m0.075s
    user    0m0.073s
    sys 0m0.002s
    

    和 emplace_back:

    Copies: 0 , Construct: 100000005, Equal copies 0
    real    0m0.195s
    user    0m0.191s
    sys 0m0.004s
    

    您可能是说它节省空间。但是,这是基于用例的选择,而且编译器设计者似乎更喜欢速度。

    这里是代码(我跟踪也一样)

    struct Counter
    {
    static int count_ctor;
    static int count_copy;
    static int count_equal;
    
    Counter(int){count_ctor++;}    
    Counter(const Counter&){count_copy++;}    
    Counter(Counter&&) noexcept{}
    Counter & operator=(Counter const &){ count_equal++ ;}
    };
    
    int Counter::count_copy = 0;
    int Counter::count_ctor = 0;
    int Counter::count_equal = 0;
    
    int main(void)
    {
      int size(100000005);
    
    #ifdef EMPLACE
      std::vector<Counter> v;
      v.reserve(size);
      for(int i = size; i>0 ; --i){ v.emplace_back(0);}
    #else
      std::vector<Counter> v(size,0);
    #endif    
      std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);
      return 0;
    }
    

    使用 g++ -DEMPLACE --std=c++11 -O3 或不使用 EMPLACE 编译以获得所需的二进制文件。

    第二次测试

    为了反驳 OP 的假设,做了以下测试:

    1. 在多个较大的类中创建许多小向量
    2. 使用默认构造复制策略或通过调用 emplace 包装函数创建的所有对象。

    我们用

    生成了两个二进制文件
    g++ -DEMPLACE --std=c++11 -O3 copyc.cpp -o copyc && g++ --std=c++11 -O3 copyc.cpp -o copyc_copy
    

    为了避免两者中的任何一个受到操作系统的优待,我们在它们之间设置了 10 秒的标准暂停,并在系统空闲时启动。

    下面是一个示例性的运行。

    export K=10192 ; time ./copyc_copy $K ; sleep 10; time ./copyc $K
    Copies: 10192 , Construct: 1, Equal copies 0
    real    0m2.888s
    user    0m0.666s
    sys 0m2.219s
    Copies: 0 , Construct: 10192, Equal copies 0
    real    0m3.376s
    user    0m1.105s
    sys 0m2.270s
    

    我在多种情况下都运行过这个,也反过来

    Copies: 0 , Construct: 10192, Equal copies 0
    real    0m3.154s
    user    0m0.886s
    sys 0m2.267s
    Copies: 10192 , Construct: 1, Equal copies 0
    real    0m2.573s
    user    0m0.531s
    sys 0m2.025s
    

    话虽如此,这是一个不完整的测试,但是在这个匹配时间上,我敢打赌编译器设计者做的更多,从 gnu 到 clang 和 VS 都决定实现一个构造复制策略。我敢肯定他们还有其他原因。

    第二次测试的代码如下:

    #include <vector>
    #include <iostream>
    #include <cstdlib>
    
    template<typename T> static std::vector<T> get5()
    {
    std::vector<T> s;
    s.reserve(5);
    for(int i=5; i!=0 ;--i)
    {
    s.emplace_back(T());
    }
    return s;
    }
    
    struct test_struct
    {
        volatile int internals[255];
    };
    
    struct test_create
    {
    std::vector<test_struct> s;
    test_create() : s(5){}
    };
    
    struct test_emplace
    {
    std::vector<test_struct> s;
    test_emplace() : s(get5<test_struct>()){}
    };
    
    
    struct Counter
    {
    static int count_ctor;
    static int count_copy;
    static int count_equal;
    
    #ifdef EMPLACE
    test_emplace t[100];
    #else
    test_create t[100];
    #endif
    
    Counter(int)
    {
    count_ctor++;
    }
    
    Counter(const Counter&)
    {
    count_copy++;
    }
    
    Counter(Counter&&) noexcept
    {
    
    }
    Counter & operator=(Counter const &){ count_equal++ ;}
    };
    
    int Counter::count_copy = 0;
    int Counter::count_ctor = 0;
    int Counter::count_equal = 0;
    
    int main(int arg, char const * argv[])
    {
    int size(std::atoi(argv[1]));
    
    #ifdef EMPLACE
    std::vector<Counter> v;
    v.reserve(size);
    for(int i = size; i>0 ; --i)
    {
        v.emplace_back(0);
    }
    #else
    std::vector<Counter> v(size,0);
    #endif
    
    std::printf("Copies: %d , Construct: %d, Equal copies %d",Counter::count_copy, Counter::count_ctor, Counter::count_equal);
    
    return 0;
    
    }
    

    【讨论】:

    • emplace_back 的副本执行次数最多?
    • 你在容器中使用的是什么类型的?
    • @NeilKirk ,我使用了您的代码,自己尝试一下,并测试一个大尺寸,是的,它在 emplace_back 循环之前保留到适当的尺寸。
    • 您需要使用构建成本高的对象来代表我的用例。
    • @NeilKirk ,我想说一个复制成本很高的对象,但仍然必须做出选择。
    猜你喜欢
    • 1970-01-01
    • 2015-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多