我正在try 解决并行计算中的一个家庭作业(job)示例.以下是给定的代码段:

for (int i=0; i < n-1; i++) {
    x[i] = (y[i] + x[i+1]) / 7;
}

我们必须分析代码段的依赖关系,并try 对给定的代码段进行并行化,并使用适当的样本量对其进行基准测试.我的第一次try 只是复制x并删除此处的反依赖:

double *x_old = malloc(sizeof(*x_old) * n); // the plain "copy" of x
memcpy(x_old, &x[1], (n - 1) * sizeof(*x)); // to prevent i + 1 in loop we start x[1] and copy n - 1 elements
#pragma omp parallel for 
for (int i = 0; i < n-1; i++) {
    x[i] = (y[i] + x_old[i]) / 7; 
}

这段代码有效,我判断了数组的串行和并行校验和,它们都是相同的.不计算memcpy()英寸的加速比在1.6 - 2100_000_00 - 400_000_000 array elements之间.但如果我进一步增大数组大小(在470_000_000 elements附近),则加速比会显著降低,在500_000_000 array elements处,加速比为0.375.

我在3种不同的设备上对其进行了基准测试,结果相同(加速方面):

联想Ideapad 5

laptop

台式电脑

home

在我们的群集 node PC上

cluster

为什么加速比会下降,为什么下降得这么快?为什么会在特定大小的数组上发生这种情况?

我在这里提供了一个程序的最小示例:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "omp.h"

void init(double *arr, int size);
double sum(double *arr, int size);

int main(int argc, char **argv){
    if(argc != 3){
        printf("Wrong use of program! (1) Size of array, (2) num threads\n");
        return EXIT_FAILURE;
    }
    int n = atoi(argv[1]);
    int threads = atoi(argv[2]);
    omp_set_num_threads(threads);

    double *x = malloc(sizeof(*x) * n);
    double *y = malloc(sizeof(*y) * n);
    init(x, n);
    init(y, n);
    double start = omp_get_wtime();
    for (int i = 0; i < n-1; i++) {
        x[i] = (y[i] + x[i+1]) / 7;
    }
    double end = omp_get_wtime();
    double elapsed_s = end - start;
    printf("single elapsed: %fs\n", elapsed_s);
    double checksum_s = sum(x, n);
    printf("single checksum: %f\n", checksum_s);
    init(x, n); //reinit
    init(y, n); //reinit
    double *x_old = malloc(sizeof(*x_old) * n); // the plain "copy" of x
    memcpy(x_old, &x[1], (n - 1) * sizeof(*x)); // to prevent i + 1 in loop we start x[1] and copy n - 1 elements
    start = omp_get_wtime();
    #pragma omp parallel for 
    for (int i = 0; i < n-1; i++) {
        x[i] = (y[i] + x_old[i]) / 7; 
    }
    end = omp_get_wtime();
    double elapsed_p = end - start;
    printf("omp threads %d Thread(s) elapsed: %fs\n", omp_get_max_threads(), elapsed_p);
    double checksum_p = sum(x, n);
    printf("omp threads %d Thread(s) single checksum: %f\n", omp_get_max_threads(), checksum_p);
    printf("%s\n", fabs((checksum_p - checksum_s)) < __DBL_EPSILON__ ? "OK" : "FAILED");
    printf("Speedup: %.3f\n", elapsed_s / elapsed_p);
}

void init(double *arr, int size){
    for(int i = 0; i < size; ++i){
        arr[i] = 1.0;
    }
}
double sum(double *arr, int size){
    double sum = 0.0;
    for(int i = 0; i < size; ++i){
        sum += arr[i];
    }
    return sum;
}

这里是一个示例基准:

benchmark

推荐答案

问题当然来自swap memory.实际上,该代码分配了3个500\u 000\u 000项的数组,导致每个数组需要4 GiB的RAM,总内存为12 GiB.问题是有两台基准测试机器只有16 GiB的RAM可用,操作系统以及运行的应用程序通常需要1-4 GiB的RAM.当可用物理内存量变小时,操作系统开始在一个称为交换内存的空间上移动内存页,交换内存通常映射到比普通RAM慢得多的存储设备上.请注意,一些操作系统(如Windows)已经使用ZRAM以便在可用内存量变小时压缩数据.主流操作系统(Windows、Linux和MAC也需要一些额外的缓冲区空间(例如存储设备缓冲区、网络等).当内存量变小时,内存页的分配也会变慢.还请注意,页面仅在第一次接触主流系统时才在RAM中分配.欲了解更多信息,请阅读this article.

Allocating a huge 100 array is not efficient(在时间和空间上).事实上,memcopy将花费大量时间在内存中复制数据,并且此操作受到RAM吞吐量的限制,RAM的吞吐量通常较低.通过在chunks上操作,可以加快操作速度.其 idea 是在每个线程中复制一个小数据块(比如说16 KiB),这样操作就可以在每个核心的一级缓存中完成.这意味着memcpy的成本将几乎消失,因为CPU缓存比RAM快得多(延迟和吞吐量).事实上,笔记本电脑/台式电脑的一级CPU缓存应为>;x_old GiB/核,导致累计吞吐量>;600 GiB,而实际上RAM的吞吐量不应超过40 GiB/s(慢15倍).

对于集群 node 来说,这有点复杂,因为它会受到NUMA影响.NUMA体系 struct 非常复杂.有关更多信息,我建议您先阅读this article,然后阅读Explanation for why effective DRAM bandwidth reduces upon adding CPUs(包括相关链接)和Problem of sorting OpenMP threads into NUMA nodes by experiment.简而言之,您需要关心页面的分配位置(分配策略).关键是控制页面上的第一次touch (在大多数平台上).

C++相关问答推荐

自定义malloc实现上奇怪的操作系统依赖行为

为什么海湾合作委员会在共享对象中的. init_data的虚拟内存地址之前留出一个空白

如何在不修改字符串缓冲区早期使用的情况下覆盖字符串缓冲区

%p与char* 等组合缺少的GCC Wform警告

当打印字符串时,为什么在c中没有使用常量限定符时我会收到警告?

使用NameSurname扫描到两个单独的字符串

堆栈在作用域阻塞后会被释放吗?

在C++中通过空指针隐式访问常量变量的值

每个 struct 变量在C中都有自己的命名空间吗?

是否可以通过调用两个函数来初始化2D数组?示例:ARRAY[STARTING_ROWS()][STARTING_COLUMNS()]

在移动数组元素时获得意外输出

仅从限制指针参数声明推断非混叠

是否有单独的缓冲区用于读写库调用?

Linux/C:带有子进程的进程在添加waitid后都挂起

';malloc():损坏的顶部大小';分配超过20万整数后

在C中打印指针本身

访问未对齐联合的成员是否为未定义行为,即使被访问的成员已充分对齐?

使用复合文字数组初始化的指针数组

C 中 struct 体自赋值是否安全?特别是如果一侧是指向 struct 的指针?

C99 的 %zu 格式说明符不起作用