新的运算符+OpenMP动态调度子句

The new operator + OpenMP dynamic schedule clause

本文关键字:动态调度 子句 +OpenMP 运算符      更新时间:2023-10-16

我一直在使用C++代码来执行量子化学、原子和分子任务,这意味着要对阵列(1D、2D、3D等)进行大量工作。我有一个完整的类array来处理这个问题。当然,从一开始,最基本的成员函数就是为这些数组动态分配内存、调整大小或删除它们的函数。

data = new double **[row]();
#pragma omp for schedule(static) nowait
for(unsigned int i = 0; i < row; ++i)
{
    data[i] = new double *[column]();
}

现在我正在做的是用OpenMP来加速这些例程。在大多数例程中,我一直在使用schedule(static) nowait类将循环划分为step/threads的块,因为这些块在线程处理上花费的时间几乎相同。

但是,对于像上面这样的循环,通过多次调用new运算符,我有一种(糟糕的)感觉,即这些循环的块在其线程中执行的时间不一样,从某种意义上说,我应该考虑应用schedule(dynamic, chunk_size)

你们同意吗?动态分配并不是一项简单的任务,而且可能会很昂贵,因此动态分配块的执行时间可能会有所不同。

事实上,我不确定我是否在堆栈碎片或诸如此类的事情上犯了任何错误。我们欢迎您的建议。

PS.:我使用nowait子句来尽量减少隐性障碍的瓶颈。

如果使用默认的new运算符,您的特定循环可能不会提供太多并行机会,因为堆是单个资源,对它的访问需要通过互斥体序列化。然而,假设您希望使用OpenMP的其他循环,以下内容应该会有所帮助。

来自OpenMP 3.1规范:

static当指定scheduletaticchunk_size)时,迭代被划分为大小为chunk_size的块,并且这些块按照线程号的顺序以循环方式分配给团队中的线程。

当没有指定chunk_size时,迭代空间被划分为大小大致相等的块,并且每个线程最多分配一个块。请注意,在这种情况下,块的大小是未指定的。

dynamic当指定scheduledynamicchunk_size)时,迭代为在线程请求时以块的形式分发给团队中的线程。每个线程执行一个迭代块,然后请求另一个块,直到没有剩余的块要分发。

每个区块都包含chunk_size迭代,但要分发的最后一个区块除外,它可能具有较少的迭代。

如果未指定chunk_size,则默认为1。

在您的情况下,您没有指定chunk_size,因此每个任务的迭代次数是未指定的。

一般来说,我更喜欢对线程数量和每个任务执行的迭代次数进行一些控制。我发现(在使用mingw-w64编译的Windows上),任务启动新的工作块会有很大的开销,所以给它们尽可能大的块是有益的。我倾向于使用动态(尽管我可以对固定执行时间的任务使用静态),并将chunk_size设置为循环计数除以线程数。在您的情况下,如果您怀疑任务执行时间不均衡,可以将其除以2或4。

// At the top of a C++ file:
static int NUM_THREADS = omp_get_num_procs();
// Then for your loop construct (I'm using a combined parallel for here):
#pragma omp parallel for num_threads(NUM_THREADS) 
   schedule(dynamic, row / NUM_THREADS / 2)
for(unsigned int i = 0; i < row; ++i)
{
   data[i] = new double *[column]();
}

请注意,如果不设置num_threads,则默认值将为nthreads var(由omp_get_max_threads确定)。

关于nowait子句,显然要确保您没有在循环构造之外使用data。我在上面使用了一个组合的并行循环结构,这意味着不能指定nowait

有一种更干净的方法可以做到这一切:

std::vector<double> actual_data(omp_get_num_procs() * column);
std::vector<double *> data(omp_get_num_procs());
for (unsigned i = 0; i < row; i++) {
    data[i] = &(actual_data[i * column]);
}

现在,您有一个一次性分配的单个数组,其中有一个指向该数组的指针数组。在并行算法中,您可以使用data[i][j]来获得您想要的成员,而开销几乎为零(开销发生在编译时)。

唯一的潜在风险是错误共享,因为矩阵的行可能在其端点共享缓存线。

内存管理是自动的;不需要CCD_ 13任何东西。

如果你计划做很多基本的线性代数运算(BLAS),那么我建议你不要在多维数组中使用数组数组。不幸的是,多维静态和动态数组的C语法破坏了对称性。这导致Java和C#使用相同的语法,因此人们通常认为,当他们分配2D数组时,他们得到的东西与静态数组相同,但来自堆而不是堆栈。您的2D阵列实际上是锯齿形阵列。

根据我在BLAS操作(以及图像处理)方面的经验,您需要用于2D阵列的连续内存块。因此,对于nxn矩阵,您应该像double[n*n]一样进行分配,并将其作为data[i*n+j]进行访问,或者创建一个C++矩阵类,在该类中您可以像matrix.get(i,j)一样访问矩阵。

不要并行分配内存。无论如何,大多数BLAS操作都将是内存绑定的,因此OpenMP只适用于级别3的BLAS操作,如矩阵乘法、LU因子分解、cholesky分解(称为O(n^3))。