C++ 标准容器中元素的默认初始化导致性能下降
(是的,我知道有一个标题几乎相同,但答案不令人满意,见下文) 对不起,原始问题没有使用编译器优化。现在已经解决了这个问题,但是为了避免琐碎的优化并更接近我的实际用例,测试被分成了两个编译单元 当涉及到性能关键型应用程序时,C++ 标准容器中元素的默认初始化导致性能下降,c++,multithreading,c++11,containers,C++,Multithreading,C++11,Containers,(是的,我知道有一个标题几乎相同,但答案不令人满意,见下文) 对不起,原始问题没有使用编译器优化。现在已经解决了这个问题,但是为了避免琐碎的优化并更接近我的实际用例,测试被分成了两个编译单元 当涉及到性能关键型应用程序时,std::vector的构造函数具有线性复杂性这一事实是一个麻烦。考虑这个简单的代码 // compilation unit 1: void set_v0(type*x, size_t n) { for(size_t i=0; i<n; ++i) x[i] =
std::vector
的构造函数具有线性复杂性这一事实是一个麻烦。考虑这个简单的代码
// compilation unit 1:
void set_v0(type*x, size_t n)
{
for(size_t i=0; i<n; ++i)
x[i] = simple_function(i);
}
// compilation unit 2:
std::vector<type> x(n); // default initialisation is wasteful
set_v0(x.data(),n); // over-writes initial values
然而,正如我的评论所指出的那样,push_-back()
本质上是缓慢的,这使得第二种方法实际上比第一种方法更慢,适用于足够简单的可构造对象,例如
simple_function = [](size_t i) { return i; };
我得到了以下计时结果(使用GCC4.8和-O3;Clang3.2产生了大约10%的慢代码)
如果可以省略元素的默认构造,那么实际可能的加速可以通过以下版本进行估计
// compilation unit 2
std::vector<type> x; x.reserve(n); // no initialisation
set_v0(x,n); // error: write beyond end of vector
// note: vector::size() == 0
因此,我的第一个问题是:是否有任何合法的方法可以使用标准库容器来提供后面的时间?还是我必须自己管理内存
现在,我真正想要的是使用多线程填充容器。朴素的代码(为了简单起见,在本例中使用openMP,暂时不包括clang)
请注意,作弊版本比串行作弊版本快约3.3倍,大致如预期,但标准版本则不然
因此,我的第二个问题:是否有任何合法的方法可以使用标准库容器,在多线程情况下提供后一种计时
PS.我发现,std::vector
通过提供一个未初始化的\u分配器来避免默认初始化。
这不再符合标准,但在我的测试用例中效果很好(请参见下面我自己的答案和详细信息)。使用g++4.5,我能够实现从v0(1.0到0.8秒)大约20%的运行时间减少,并且通过使用生成器直接构造v1,从0.95秒减少到0.8秒:
struct Generator : public std::iterator<std::forward_iterator_tag, int>
{
explicit Generator(int start) : value_(start) { }
void operator++() { ++value_; }
int operator*() const { return value_; }
bool operator!=(Generator other) const { return value_ != other.value_; }
int value_;
};
int main()
{
const int n = 100000000;
std::vector<int> v(Generator(0), Generator(n));
return 0;
}
结构生成器:公共标准::迭代器
{
显式生成器(int start):值(start{}
void运算符++(){++value_;}
int运算符*()常量{返回值}
布尔运算符!=(生成器其他)常量{返回值\!=other.value \;}
int值;
};
int main()
{
常数整数n=100000000;
向量v(生成器(0),生成器(n));
返回0;
}
boost::transformed
对于单线程版本,您可以使用。它有:
返回范围类别:rng的范围类别
这意味着,如果您将随机访问范围
赋予boost::transformed
,它将返回随机访问范围
,这将允许vector
的构造函数预分配所需的内存量
您可以按如下方式使用它:
const auto &gen = irange(0,1<<10) | transformed([](int x)
{
return exp(Value{x});
});
vector<Value> v(begin(gen),end(gen));
const auto&gen=irange(0,1好的,下面是我问这个问题后学到的东西
Q1(是否有任何合法的方法可以使用标准库容器来提供后面的计时?是在某种程度上,如Mark和Evgeny的回答所示。为std::vector
的构造函数提供生成器的方法省去了默认构造
Q2(在多线程情况下,是否有任何合法的方法可以使用标准库容器来进行后一种计时?)否,我不这么认为。原因是在构建时任何符合标准的容器都必须初始化其元素,以确保对元素析构函数的调用(在销毁或调整容器大小时)由于std库容器不支持在构造元素时使用多线程,Q1的技巧在这里无法复制,因此我们不能省略默认构造
因此,如果我们想用强的计算<强> C++,我们的选择在管理大量数据时有点有限。我们可以< /P>
1声明一个容器对象,并在同一个编译单元中,当编译器希望在构建时优化初始化时,立即(同时)填充它
< > > > < 2】< />新[]/Cudio>和 [D]/Cube >或< <代码> MalCube()> <代码> >(或代码>),当所有内存管理和后一种情况下,元素的构建是我们的责任,而C++标准库的潜在使用非常有限。
3欺骗std::vector
不使用自定义
当欺骗和“合法”之间不再有区别时版本。在这种情况下,我实际上会建议您使用自己的容器或寻找替代品,因为在我看来,您固有的问题不是标准容器默认的构造元素。而是尝试使用可变容量容器,其容量可以在构造时确定
没有标准库不必要地默认构造元素的实例。vector
只对其fill构造函数和resize
执行此操作,这两者在概念上都是通用容器所必需的,因为它们的目的是调整容器的大小以包含有效元素我们应该这样做:
T* mem = static_cast<T*>(malloc(num * sizeof(T)));
for (int j=0; j < num; ++j)
new (mem + j) T(...); // meaningfully construct T
...
for (int j=0; j < num; ++j)
mem[j].~T(); // destroy T
free(mem);
T*mem=static_cast(malloc(num*sizeof(T));
对于(int j=0;j// compilation unit 1
void set_v0(type*x, size_t n)
{
#pragma omp for // only difference to set_v0() from above
for(size_t i=0; i<n; ++i)
x[i] = simple_function(i);
}
// compilation unit 2:
std::vector<type> x(n); // default initialisation not mutli-threaded
#pragma omp parallel
set_v0(x,n); // over-writes initial values in parallel
timing std::vector::vector(n) + omp parallel set_v0()
n=10000 time: 0.000389 sec
n=100000 time: 0.000226 sec
n=1000000 time: 0.001406 sec
n=10000000 time: 0.019833 sec
n=100000000 time: 0.35531 sec
timing vector::vector + vector::reserve(n) + omp parallel set_v0(); (CHEATING)
n=10000 time: 0.000222 sec
n=100000 time: 0.000243 sec
n=1000000 time: 0.000793 sec
n=10000000 time: 0.008952 sec
n=100000000 time: 0.089619 sec
struct Generator : public std::iterator<std::forward_iterator_tag, int>
{
explicit Generator(int start) : value_(start) { }
void operator++() { ++value_; }
int operator*() const { return value_; }
bool operator!=(Generator other) const { return value_ != other.value_; }
int value_;
};
int main()
{
const int n = 100000000;
std::vector<int> v(Generator(0), Generator(n));
return 0;
}
const auto &gen = irange(0,1<<10) | transformed([](int x)
{
return exp(Value{x});
});
vector<Value> v(begin(gen),end(gen));
#define BOOST_RESULT_OF_USE_DECLTYPE
#include <boost/range/adaptor/transformed.hpp>
#include <boost/container/vector.hpp>
#include <boost/range/irange.hpp>
#include <boost/progress.hpp>
#include <boost/range.hpp>
#include <iterator>
#include <iostream>
#include <ostream>
#include <string>
#include <vector>
#include <array>
using namespace std;
using namespace boost;
using namespace adaptors;
#define let const auto&
template<typename T>
void dazzle_optimizer(T &t)
{
auto volatile dummy = &t; (void)dummy;
}
// _______________________________________ //
using Value = array<int,1 << 16>;
using Vector = container::vector<Value>;
let transformer = [](int x)
{
return Value{{x}};
};
let indicies = irange(0,1<<10);
// _______________________________________ //
void random_access()
{
let gen = indicies | transformed(transformer);
Vector v(boost::begin(gen), boost::end(gen));
dazzle_optimizer(v);
}
template<bool reserve>
void single_pass()
{
Vector v;
if(reserve)
v.reserve(size(indicies));
for(let i : indicies)
v.push_back(transformer(i));
dazzle_optimizer(v);
}
void cheating()
{
Vector v;
v.reserve(size(indicies));
for(let i : indicies)
v[i]=transformer(i);
dazzle_optimizer(v);
}
// _______________________________________ //
int main()
{
struct
{
const char *name;
void (*fun)();
} const tests [] =
{
{"single_pass, no reserve",&single_pass<false>},
{"single_pass, reserve",&single_pass<true>},
{"cheating reserve",&cheating},
{"random_access",&random_access}
};
for(let i : irange(0,3))
for(let test : tests)
progress_timer(), // LWS does not support auto_cpu_timer
(void)i,
test.fun(),
cout << test.name << endl;
}
// based on a design by Jared Hoberock
// edited (Walter) 10-May-2013, 23-Apr-2014
template<typename T, typename base_allocator = std::allocator<T> >
struct uninitialised_allocator
: base_allocator
{
static_assert(std::is_same<T,typename base_allocator::value_type>::value,
"allocator::value_type mismatch");
template<typename U>
using base_t =
typename std::allocator_traits<base_allocator>::template rebind_alloc<U>;
// rebind to base_t<U> for all U!=T: we won't leave other types uninitialised!
template<typename U>
struct rebind
{
typedef typename
std::conditional<std::is_same<T,U>::value,
uninitialised_allocator, base_t<U> >::type other;
}
// elide trivial default construction of objects of type T only
template<typename U>
typename std::enable_if<std::is_same<T,U>::value &&
std::is_trivially_default_constructible<U>::value>::type
construct(U*) {}
// elide trivial default destruction of objects of type T only
template<typename U>
typename std::enable_if<std::is_same<T,U>::value &&
std::is_trivially_destructible<U>::value>::type
destroy(U*) {}
// forward everything else to the base
using base_allocator::construct;
using base_allocator::destroy;
};
template<typename T, typename base_allocator = std::allocator<T>>
using uninitialised_vector = std::vector<T,uninitialised_allocator<T,base_allocator>>;
timing vector::vector(n) + set_v0();
n=10000 time: 3.7e-05 sec
n=100000 time: 0.000334 sec
n=1000000 time: 0.002926 sec
n=10000000 time: 0.028649 sec
n=100000000 time: 0.293433 sec
timing vector::vector() + vector::reserve() + set_v1();
n=10000 time: 2e-05 sec
n=100000 time: 0.000178 sec
n=1000000 time: 0.001781 sec
n=10000000 time: 0.020922 sec
n=100000000 time: 0.428243 sec
timing vector::vector() + vector::reserve() + set_v0();
n=10000 time: 9e-06 sec
n=100000 time: 7.3e-05 sec
n=1000000 time: 0.000821 sec
n=10000000 time: 0.011685 sec
n=100000000 time: 0.291055 sec
timing vector::vector(n) + omp parllel set_v0();
n=10000 time: 0.00044 sec
n=100000 time: 0.000183 sec
n=1000000 time: 0.000793 sec
n=10000000 time: 0.00892 sec
n=100000000 time: 0.088051 sec
timing vector::vector() + vector::reserve() + omp parallel set_v0();
n=10000 time: 0.000192 sec
n=100000 time: 0.000202 sec
n=1000000 time: 0.00067 sec
n=10000000 time: 0.008596 sec
n=100000000 time: 0.088045 sec
T* mem = static_cast<T*>(malloc(num * sizeof(T)));
for (int j=0; j < num; ++j)
new (mem + j) T(...); // meaningfully construct T
...
for (int j=0; j < num; ++j)
mem[j].~T(); // destroy T
free(mem);