从 pthread调用的 OpenMP 代码的性能问题

Performance issue of OpenMP code called from a pthread

本文关键字:性能 问题 代码 OpenMP 调用 pthread      更新时间:2023-10-16

我尝试从 I/O 绑定操作异步执行一些计算。为此,我使用了一个 pthread,其中循环使用 OpenMP 并行化。但是,与在 pthread 中执行 I/O 绑定操作或根本不创建 pthread 的情况相比,这会导致性能下降。

演示行为的最小示例(见下文)使用 usleep 来模拟 I/O 绑定任务。该程序的输出为

g++ simple.cpp -O3 -fopenmp
export OMP_PROC_BIND=false
No pthread: 34.0884
Compute from pthread: 36.6323
Compute from main: 34.1188

我不明白为什么在创建的 pthread 中使用 OpenMP 会降低性能。更令我惊讶的是以下行为。

export OMP_PROC_BIND=true
./a.out                  
No pthread: 34.0635
Compute from pthread: 60.6081
Compute from main: 34.0347

为什么运行时有 2 倍?

源代码如下

#include <iostream>
using namespace std;
#include <unistd.h>
#include <omp.h>
int n = 1000000000;
int rep = 100;
double elapsed;
void compute() {
    double t = omp_get_wtime();
    double s = 0.0;
    #pragma omp parallel for reduction(+:s)
    for(int i=0;i<n;i++)
        s += 1/double(i+1);
    elapsed += omp_get_wtime()-t;
}
void* run(void *threadid) {
    compute();
}
void* wait(void* threadid) {
    usleep(150e3);
}
int main() {
    elapsed = 0;
    for(int k=0;k<rep;k++)
        compute();
    cout << "No pthread: " << elapsed << endl;
    elapsed = 0;
    for(int k=0;k<rep;k++) {
        pthread_t t; void* status;
        pthread_create(&t, NULL, run, NULL);
        usleep(150e3);
        pthread_join(t, &status);
    }
    cout << "Compute from pthread: " << elapsed << endl;
    elapsed = 0;
    for(int k=0;k<rep;k++) {
        pthread_t t; void* status;
        pthread_create(&t, NULL, wait, NULL);
        compute();
        pthread_join(t, &status);
    }
    cout << "Compute from main: " << elapsed << endl;
}

首先,一个公平的警告 - 从不受 OpenMP 管理的线程创建并行区域是非标准行为,可能会导致相当不可移植的应用程序。

GNU OpenMP 运行时库 (libgomp) 使用线程池来加速线程团队的创建。池与并行区域的主线程相关联,方法是在 TLS(线程本地存储)中的特殊结构中存储对它的引用。创建并行区域时,libgomp 会查询主线程的 TLS 以查找线程池。如果不存在线程池,即这是该线程第一次创建并行区域,它将创建池。

当您从主线程调用compute()时,libgomp 会在每次创建新的并行区域时在 TLS 中找到对线程池的引用并使用它,因此生成线程的代价仅在第一次创建并行区域时支付。

当您从第二个线程调用compute()时,libgomp 无法在 TLS 中找到特殊结构,并创建一个新结构和一个新的线程池。线程终止后,一个特殊的析构函数负责终止线程池。因此,每次调用compute()时都会创建线程池,这仅仅是因为您在外部循环的每个迭代中生成一个新线程。20 毫秒的开销(100 次迭代相差 2 秒)是此类操作的典型值。

我无法用OMP_PROC_BIND=true重现这种情况 - 程序的运行与没有它的情况基本相同,甚至由于绑定而更好一点。这可能是特定于您的GCC版本,操作系统和CPU组合的东西。请注意,parallel for构造将工作量均匀分布在团队的所有线程之间。如果其中一个被延迟,例如,由于必须与其他进程分时其CPU内核,那么它将延迟整个并行区域的完成。使用OMP_PROC_BIND=true,操作系统调度程序不允许移动线程,它们必须与其他进程的线程分时共享其CPU内核。如果一个这样的外部线程使用大量 CPU,则线程绑定时比未绑定线程时危害更大,因为在后一种情况下,调度程序可能会移动受影响的线程。换句话说,在这种特殊情况下,当所有 OpenMP 线程必须共享除一个内核之外的所有内核时,最好将整个程序延迟 #cores / (#cores - 1) 次,而不是延迟 100%,因为一个绑定线程必须与该外部线程共享其 CPU 内核 50/50。当然,在这种情况下,人们要为昂贵的跨核心移动操作付出代价,但如果外部影响过于具有破坏性,它仍然可以带来更好的利用。