C++ 构造大向量shared_ptr到类的有效方法

C++ Efficient way to construct large vector of shared_ptr to class

本文关键字:有效 方法 ptr 向量 shared C++      更新时间:2023-10-16

我需要构造一个大std::vector<std::shared_ptr<A>> many_ptr_to_A

理想情况下,对于A,使用带有参数的非默认构造函数。下面的代码示例中定义了几个变体:

#include <iostream>
#include <vector>
#include <memory>
#include <ctime>
class A
{
public:
    A(std::vector<double> data):
        data(data)
    {}
    A():
        data(std::vector<double>(3, 1.))
    {}
    std::vector<double> data;
};
int main()
{
    int n = 20000000;
    std::vector<std::shared_ptr<A>> many_ptr_to_A;
    // option 1
    std::clock_t start = std::clock();
    std::vector<A> many_A(n, std::vector<double>(3, 1.));
    std::cout << double(std::clock() - start) / CLOCKS_PER_SEC << std::endl;
    // end option 1
    many_ptr_to_A.clear();
    // option 2
    start = std::clock();
    many_ptr_to_A.reserve(n);
    for (int i=0; i<n; i++) {
        many_ptr_to_A.push_back(std::shared_ptr<A>(new A(std::vector<double>(3, 1.))));
    }
    std::cout << double(std::clock() - start) / CLOCKS_PER_SEC << std::endl;
    // end option 2
    many_ptr_to_A.clear();
    // option 3
    start = std::clock();
    A* raw_ptr_to_A = new A[n];
    for (int i=0; i<n; i++) {
        many_ptr_to_A.push_back(std::shared_ptr<A>(&raw_ptr_to_A[i]));
    }
    std::cout << double(std::clock() - start) / CLOCKS_PER_SEC << std::endl;
    // end option 3
    return 0;
}

选项 1

相当快,但不幸的是我需要指针而不是原始对象。创建指向结果分配空间的指针并防止向量删除对象的方法会很棒,但我想不出一个。

选项 2

这有效,我可以在构造函数中为每个A提供特定数据。不幸的是,这是相当缓慢的。使用std::make_shared而不是new并不能真正改善这种情况。

更糟糕的是,在多个线程中使用时,这似乎是一个很大的瓶颈。假设我使用 2 在 2 个线程中运行选项 10 n_thread = n / 10 ,而不是快十倍左右,整个事情慢了大约四倍。为什么会这样?当多线程尝试分配许多小块内存时,是否有问题?

我使用的服务器上的内核数大于线程数。我的应用程序的其余部分可以很好地扩展内核数量,因此这实际上代表了一个瓶颈。

不幸的是,我在并行化方面并没有真正的经验......

选项 3

通过这种方法,我尝试将快速分配与原始new一次性和shared_ptrs相结合。这会编译,但不幸的是,当调用向量的析构函数时会产生分割错误。我不完全明白为什么会发生这种情况。是因为A不是 POD?

在这种方法中,

我会在对象创建后手动将特定于对象的数据填充到对象中。

问题

如何以有效的方式执行大量A shared_ptr的分配,以便在许多线程/内核上使用时也能很好地扩展?我是否错过了一种一次性构建std::vector<std::shared_ptr<A>> many_ptr_to_A的明显方法?

我的系统是 Linux/Debian 服务器。我使用 g++ 和 -O3 编译,-std=c++11 选项。

任何帮助都非常感谢:)

选项 3 是未定义的行为,您有n shared_ptrs,它们都将尝试delete单个A,但整个数组必须只有一个delete[],不能delete n次使用。不过,您可以这样做:

std::unique_ptr<A[]> array{ new A[n] };
std::vector<std::shared_ptr<A>> v;
v.reserve(n);
v.emplace_back(std::move(array));
for (int i = 1; i < n; ++i)
  v.push_back(std::shared_ptr<A>{v[0], v[0].get() + i});

这将创建一个数组,然后创建n shared_ptr对象,这些对象都共享数组的所有权,并且每个对象都指向数组的不同元素。这是通过创建一个拥有数组(和一个合适的删除器)的shared_ptr,然后创建别名第一个数组的n-1 shared_ptrs来完成的,即共享相同的引用计数,即使它们的get()成员将返回不同的指针。

首先用数组初始化一个unique_ptr<A[]>,以便default_delete<A[]>将用作删除器,并且该删除器将被转移到第一个shared_ptr,以便当最后一个shared_ptr放弃所有权时,将使用权利delete[]来释放整个数组。要获得相同的效果,您可以像这样创建第一个shared_ptr:

v.push_back(std::shared_ptr<A>{new A[n], std::default_delete<A[]>{}});

或:

v.emplace_back(std::unique_ptr<A[]>{new A[n]});