OpenMP 数组初始化影响
OpenMP array initialization impact
我在阵列(工作部分)上与 OpenMP 并行工作。如果我之前并行初始化数组,那么我的工作部分需要 18 毫秒。如果我在没有 OpenMP 的情况下串行初始化数组,那么我的工作部分需要 58 毫秒。是什么导致性能变差?
系统:
- 英特尔(R) 至强(R) CPU E5-2697 v3(28 核/56 线程,2 个插槽)
示例代码:
unsigned long sum = 0;
long* array = (long*)malloc(sizeof(long) * 160000000);
// Initialisation
#pragma omp parallel for num_threads(56) schedule(static)
for(unsigned int i = 0; i < array_length; i++){
array[i]= i%10;
}
// Time start
// Work
#pragma omp parallel for num_threads(56) shared(array, 160000000) reduction(+: sum)
for (unsigned long i = 0; i < array_length; i++)
{
if (array[i] < 4)
{
sum += array[i];
}
}
// Time End
这里有两个方面在起作用:
NUMA 分配
在 NUMA 系统中,内存页可以是 CPU 的本地内存页或远程内存页。默认情况下 Linux 在 first-touch 策略中分配内存,这意味着对内存页面的第一次写访问决定了该页面在哪个节点上物理分配。
如果您的 malloc 足够大,可以从 OS 请求新内存(而不是重新使用现有的堆内存),那么第一次接触将在初始化时发生。因为您对 OpenMP 使用静态调度,所以同一个线程将使用初始化它的内存。因此,除非线程迁移到不同的 CPU,否则内存将是本地的。
如果您不并行初始化,内存最终将位于主线程的本地,这对于位于不同套接字上的线程来说会更糟。
请注意 Windows 不使用 first-touch 策略 (AFAIK)。所以这种行为是不可移植的。
缓存
同上也适用于缓存。初始化会将数组元素放入 CPU 的缓存中。如果相同的CPU在第二阶段访问内存,它将cache-hot并准备好使用。
首先@Homer512的解释是完全正确的
现在我注意到您将此问题标记为“C++”,但您对数组使用的是 malloc
。这在 C++ 中是糟糕的风格:您应该对简单的容器使用 std::vector
,对足够小的容器使用 std::array
。
然后你有一个大问题,因为 std::vector
使用“值初始化”:整个数组自动填充零,你无法让这与 OpenMP 并行完成。
来个大招:
template<typename T>
struct uninitialized {
uninitialized() {};
T val;
constexpr operator T() const {return val;};
double operator=( const T&& v ) { val = v; return val; };
};
vector<uninitialized<double>> x(N),y(N);
#pragma omp parallel for
for (int i=0; i<N; i++)
y[i] = x[i] = 0.;
x[0] = 0; x[N-1] = 1.;
我在阵列(工作部分)上与 OpenMP 并行工作。如果我之前并行初始化数组,那么我的工作部分需要 18 毫秒。如果我在没有 OpenMP 的情况下串行初始化数组,那么我的工作部分需要 58 毫秒。是什么导致性能变差?
系统:
- 英特尔(R) 至强(R) CPU E5-2697 v3(28 核/56 线程,2 个插槽)
示例代码:
unsigned long sum = 0;
long* array = (long*)malloc(sizeof(long) * 160000000);
// Initialisation
#pragma omp parallel for num_threads(56) schedule(static)
for(unsigned int i = 0; i < array_length; i++){
array[i]= i%10;
}
// Time start
// Work
#pragma omp parallel for num_threads(56) shared(array, 160000000) reduction(+: sum)
for (unsigned long i = 0; i < array_length; i++)
{
if (array[i] < 4)
{
sum += array[i];
}
}
// Time End
这里有两个方面在起作用:
NUMA 分配
在 NUMA 系统中,内存页可以是 CPU 的本地内存页或远程内存页。默认情况下 Linux 在 first-touch 策略中分配内存,这意味着对内存页面的第一次写访问决定了该页面在哪个节点上物理分配。
如果您的 malloc 足够大,可以从 OS 请求新内存(而不是重新使用现有的堆内存),那么第一次接触将在初始化时发生。因为您对 OpenMP 使用静态调度,所以同一个线程将使用初始化它的内存。因此,除非线程迁移到不同的 CPU,否则内存将是本地的。
如果您不并行初始化,内存最终将位于主线程的本地,这对于位于不同套接字上的线程来说会更糟。
请注意 Windows 不使用 first-touch 策略 (AFAIK)。所以这种行为是不可移植的。
缓存
同上也适用于缓存。初始化会将数组元素放入 CPU 的缓存中。如果相同的CPU在第二阶段访问内存,它将cache-hot并准备好使用。
首先@Homer512的解释是完全正确的
现在我注意到您将此问题标记为“C++”,但您对数组使用的是 malloc
。这在 C++ 中是糟糕的风格:您应该对简单的容器使用 std::vector
,对足够小的容器使用 std::array
。
然后你有一个大问题,因为 std::vector
使用“值初始化”:整个数组自动填充零,你无法让这与 OpenMP 并行完成。
来个大招:
template<typename T>
struct uninitialized {
uninitialized() {};
T val;
constexpr operator T() const {return val;};
double operator=( const T&& v ) { val = v; return val; };
};
vector<uninitialized<double>> x(N),y(N);
#pragma omp parallel for
for (int i=0; i<N; i++)
y[i] = x[i] = 0.;
x[0] = 0; x[N-1] = 1.;