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

Performance issue of OpenMP code called from a pthread

我尝试从 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

为什么 运行 时间是二的因数?

源码如下

#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 版本、OS 和 CPU 的组合。请注意,parallel for 构造将工作量平均分布在团队的所有线程之间。如果其中之一被延迟,例如由于必须与其他进程分时共享其 CPU 核心,因此它会延迟整个并行区域的完成。对于 OMP_PROC_BIND=true,不允许 OS 调度程序移动线程,它们必须与来自其他进程的线程分时共享它们的 CPU 核心。如果一个这样的外部线程使用了很多 CPU,那么当线程被绑定时它会比未绑定时造成更大的危害,因为在后一种情况下,调度程序可能会移动受影响的线程。换句话说,在这种特殊情况下,当所有 OpenMP 线程必须共享除一个内核以外的所有内核时,最好将整个程序延迟 #cores / (#cores - 1) 次,而不是将其延迟 100%,因为一个绑定线程必须共享其CPU 核心 50/50 与外螺纹。当然,在那种情况下,需要付出昂贵的跨核移动操作的代价,但如果外部影响太大,它仍然可以带来更好的利用率。