c++ - 由于标准容器中元素的默认初始化导致性能下降

标签 c++ multithreading c++11 containers

(是的,我知道有一个 question 的标题几乎相同,但答案并不令人满意,见下文)

编辑对不起,原来的问题没有使用编译器优化。现在已修复此问题,但为了避免琐碎的优化并更接近我的实际用例,测试已分为两个编译单元。

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

当大量时间浪费在构造 x 上时.正如 this question 所探讨的,解决此问题的传统方法,似乎只是保留存储和使用push_back()填写数据:

// compilation unit 1:
void set_v1(std::vector<type>&x, size_t n)
{
  x.reserve(n);
  for(size_t i=0; i<n; ++i)
    x.push_back(simple_function(i));
}

// compilation unit 2:
std::vector<type> x(); x.reserve(n);        // no initialisation
set_v1(x,n);                                // using push_back()

但是,正如我的评论所指出的,push_back()本质上很慢,对于足够简单的可构造对象,例如 size_t,第二种方法实际上比第一种方法 s,当为

simple_function = [](size_t i) { return i; };

我得到以下时间(使用带有 -O3 的 gcc 4.8;clang 3.2 产生的代码慢了约 10%)

timing vector::vector(n) + set_v0();
n=10000 time: 3.9e-05 sec
n=100000 time: 0.00037 sec
n=1000000 time: 0.003678 sec
n=10000000 time: 0.03565 sec
n=100000000 time: 0.373275 sec

timing vector::vector() + vector::reserve(n) + set_v1();
n=10000 time: 1.9e-05 sec
n=100000 time: 0.00018 sec
n=1000000 time: 0.00177 sec
n=10000000 time: 0.020829 sec
n=100000000 time: 0.435393 sec

如果可以省略元素的默认构造,实际上可能的加速可以通过以下作弊版本来估计

// 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

当我们得到

timing vector::vector + vector::reserve(n) + set_v0();          (CHEATING)
n=10000 time: 8e-06 sec
n=100000 time: 7.2e-05 sec
n=1000000 time: 0.000776 sec
n=10000000 time: 0.01119 sec
n=100000000 time: 0.298024 sec

那么,我的第一个问题:是否有任何合法的方式来使用标准库容器来提供这些后一个时间?还是我必须自己管理内存?

现在,我真正想要的是使用多线程来填充容器。幼稚的代码(为了简单起见,本例中使用了 openMP,暂时不包括 clang)

// 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

现在所有元素的默认初始化都不是多线程的,这会导致潜在的严重性能下降。以下是 set_omp_v0() 的时间安排和一个等效的作弊方法(使用我的macbook 4核8超线程的intel i7芯片):

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

请注意,作弊版比串行作弊版快约 3.3 倍,大致符合预期,但标准版则不然。

那么,我的第二个问题:是否有任何合法的方法可以使用标准库容器,从而在多线程情况下提供后面的计时?

PS。我找到了 this question , 其中 std::vector通过提供 uninitialized_allocator 来欺骗它以避免默认初始化. 这不再符合标准,但非常适合我的测试用例(请参阅下面我自己的答案和 this question 了解详细信息)。

最佳答案

使用 g++ 4.5,通过使用生成器直接构建,我能够实现从 v0(1.0s 到 0.8s)的运行时间减少大约 20% 并且从 v1 的 0.95s 到 0.8s 稍微减少:

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;
}

关于c++ - 由于标准容器中元素的默认初始化导致性能下降,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15952412/

相关文章:

c++ - MFC DYNAMIC_DOWNCAST 与 dynamic_cast

multithreading - 在计时器上运行任务

c++ - C++ 中的常量正确性语义

c++ - Boost 1.57.0 程序选项 MSVS 2013 链接器错误

c++ - 检测窗口何时与来自相同或不同进程的另一个窗口重叠

当两个不同的参数中有空格时,C++ system() 不起作用

c++ - 我不明白程序中 next 和 prev 之间的值是如何变化的?

Java代码 - 线程相互阻塞

c# - 如何在单独的线程中无休止地运行相同的方法

c++ - 表达式模板的模板化返回类型特化