如何在程序中统计测量OpenMP性能?

时间:2018-01-17 14:10:54

标签: openmp

我想统计测量我使用OpenMP并行化的程序的性能。我选择在测试应用程序中编写循环,执行并行算法MAX_EXPERIMENTS次,并将时间测量报告到文件中。

问题解决方案似乎比提取外部循环上方的并行编译指示更复杂,因为我在内部并行循环之间有代码的串行部分。

代码:

#include <omp.h>
#include <vector> 
#include <random>
#include <iostream>
#include <cmath>
#include <fstream>
#include <sstream>
#include <iomanip>

using namespace std;

int main()
{
    const int MAX_NUMBERS = 1e07; 
    const int MAX_EXPERIMENTS = 1e02; 

    std::random_device rd;  
    std::mt19937 gen(rd()); 
    std::bernoulli_distribution dis(0.1);

    vector<double> numbers;
    numbers.reserve(MAX_NUMBERS); 

    for(unsigned int i = 0; i < MAX_NUMBERS; ++i)
    {
        if (dis(gen))
            numbers.emplace_back(100); 
        else 
            numbers.emplace_back(1);
    }

    stringstream ss; 
    ss << "time-measurements-nthread-" << setfill('0') << setw(2) 
        << omp_get_max_threads() << ".csv"; 

    ofstream exp(ss.str()); 
    exp << "time\n"; 

    for (unsigned int i = 0; i < MAX_EXPERIMENTS; ++i)
    {
        // BEGIN: Tested parallel program
        double t0 = omp_get_wtime();  

        // Some serial work.

        double x = 0; 
        //#pragma omp parallel for schedule(dynamic) reduction(+:x)  // exp-01
        #pragma omp parallel for schedule(static) reduction(+:x)  // exp-02
        for(unsigned int i = 0; i < numbers.size(); ++i) 
        {
            if (numbers[i] > 1)
               x = x + cos(numbers[i]); // Some work being done.
        }
        double t1 = omp_get_wtime(); 

        // Some serial work

        // Measure program execution
        exp << t1 - t0 << "\n";

        // END: Tested parallel program
    }

};

该计划首先将1e07个数字连续初始化为1100,以便点击100的概率为10%,这与我的真实世界相匹配输入数据。

主测试循环执行100实验,循环体模拟经过测试的并行算法。并行算法的某些部分必须以串行方式执行。在循环中编写pragma omp parallel for应该是一个坏主意,因为它会在每次创建实验时打开和关闭线程。

问题1 :尽管通常会避免在循环中打开并行区域,但在这种情况下这是合理的,其中每个实验循环步骤表示独立的并行程序执行,以及输入数据的准备实验在运行时要快得多吗?

为了可视化书面数据,我使用了python(jupyter nootebook代码):

%matplotlib inline

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import rcParams
import os

rcParams["figure.figsize"] = [10,20]
rcParams["font.size"] = 24 

def plot_experiment(expattern):
    thread1df = pd.read_csv("time-measurements-nthread-01-%s.csv" % expattern)
    thread2df = pd.read_csv("time-measurements-nthread-02-%s.csv" % expattern)
    thread4df = pd.read_csv("time-measurements-nthread-04-%s.csv" % expattern)
    thread8df = pd.read_csv("time-measurements-nthread-08-%s.csv" % expattern)


    f, (ax1, ax2) = plt.subplots(2, 1, sharex=True)

    ax1.plot(thread1df["time"], label="time 1", color='g')
    ax1.plot(thread2df["time"], label="time 2", color='r')
    ax1.plot(thread4df["time"], label="time 4", color='b')
    ax1.plot(thread8df["time"], label="time 8", color='k')

    ax2.plot(thread1df["time"] / thread8df["time"], label="speedup 8", color='k')
    ax2.plot(thread1df["time"] / thread4df["time"], label="speedup 4", color='b')
    ax2.plot(thread1df["time"] / thread2df["time"], label="speedup 2", color='r')

    ax1.set_ylabel("Time in seconds")
    ax1.legend()

    ax2.set_xlabel("Test number")
    ax2.legend()
    ax2.set_ylabel("Speedup")


plot_experiment("exp-01")

应用程序使用gcc编译,使用优化:g++ -std=c++1y -fopenmp -O3 main.cpp -o main

使用for i in 1 2 4 8; do export OMP_NUM_THREADS=$i && ./main && sleep 5; done;

执行实验

然后使用for file in *nthread-0[0-9].csv*; do mv $file ${file/.csv/-exp-02.csv}; done为pandas重新命名实验文件(对于第一个实验,将exp-02替换为exp-01)。

在第一个实验中,我尝试了动态调度,并得到了以下图表:

enter image description here

这很奇怪,因为似乎添加线程会减慢程序的速度。检查exp-018个帖子的HPCToolkit瓶颈,我注意到OpenMP花费了大量时间来切换dynamic调度模式:

enter image description here

所以我将调度模式切换到static并重新运行实验,然后得到以下结果:

enter image description here

现在有一些扩展,至少对于2个线程,但现在4线程振荡太多,并且使用8线程没有太大影响。我再次使用HPCToolkit进行了检查,得到了这个:

enter image description here

认为这告诉我,启动和停止线程正在耗尽85%我的运行时8个线程,但HPCToolkit手册说明

  

此外,如果过滤的节点是“假”程序的子节点   (例如program_root和thread_root),独有的指标   来电者观看和平面观可能会产生误导。

问题2 :在实验循环中打开和关闭并行区域时,实验02是否有显着的开销?如果是这样,如何解决这个问题,考虑到算法的连续部分?

软件:Arch Linux,g + +(GCC)7.1.1 20170630,hpcrun:HPCToolkit的成员,版本2017.11,CPU:Intel(R)Core(TM)i7-4710HQ CPU @ 2.50GHz

修改

我尝试使用建议的in the answer to this question

中的环境变量来更改线程持久性行为
export OMP_WAIT_POLICY=active GOMP_SPINCOUNT=infinite

结果如下:

enter image description here

显然,线程创建/破坏引起的振荡要低得多,但它们会消失吗?有没有办法改变程序,以便我不必依赖旋转线程?调查此程序的瓶颈仍将显示旋转线程花费的大量CPU周期。

1 个答案:

答案 0 :(得分:1)

从评论中的讨论看来,你的主要问题似乎是你有一个复杂的现有应用程序,并希望在一些内部部分中放置一个工作共享循环。但是只创建所有线程,应用程序中的开销太大,libgomp的线程池似乎不够。

如果您想在不重组的情况下执行此操作,可能有助于使用taskloop,其工作方式与for类似,但可以嵌套在single部分中。反过来,它可能不如`for。基本上你的代码看起来像这样:

int a;
#pragma omp parallel
{
  int b;
  #pragma omp single
  {
    int c;
    // lots of serial code
    // somewhere inbetween
    #pragma omp taskloop
    for (...) {
      int d;
    }
    // lots of serial code
  }
}

请注意,任务生成结构的数据共享的工作方式略有不同。默认情况下,在并行区域(a)之外声明的变量在shared区域内为parallel,并且还在执行内部循环的任务之间共享。在并行区域内但在taskloopbc)之外声明的变量在并行区域内privatefirstprivate - 每个线程有自己的副本,用外部值初始化。最后d只是每个循环迭代的本地。

编辑:不要设置任何障碍。由于隐式任务组,串行部分和任务在执行时被隔离。