我想统计测量我使用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
个数字连续初始化为1
或100
,以便点击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
)。
在第一个实验中,我尝试了动态调度,并得到了以下图表:
这很奇怪,因为似乎添加线程会减慢程序的速度。检查exp-01
和8
个帖子的HPCToolkit瓶颈,我注意到OpenMP花费了大量时间来切换dynamic
调度模式:
所以我将调度模式切换到static
并重新运行实验,然后得到以下结果:
现在有一些扩展,至少对于2
个线程,但现在4
线程振荡太多,并且使用8
线程没有太大影响。我再次使用HPCToolkit
进行了检查,得到了这个:
我认为这告诉我,启动和停止线程正在耗尽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
结果如下:
显然,线程创建/破坏引起的振荡要低得多,但它们会消失吗?有没有办法改变程序,以便我不必依赖旋转线程?调查此程序的瓶颈仍将显示旋转线程花费的大量CPU周期。
答案 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
,并且还在执行内部循环的任务之间共享。在并行区域内但在taskloop
(b
,c
)之外声明的变量在并行区域内private
和firstprivate
- 每个线程有自己的副本,用外部值初始化。最后d
只是每个循环迭代的本地。
编辑:不要设置任何障碍。由于隐式任务组,串行部分和任务在执行时被隔离。