我有一个C ++代码,可以执行生活在2D空间网格上的四个变量的时间演变。为了节省一些时间,我尝试将我的代码与OpenMP并行化,但我无法让它工作:无论我使用多少核,运行时基本保持不变或增加。 (我的代码确实使用了24个核心或者我指定的多个核心,因此编译不是问题。)
我觉得单个时间步长的运行时间太短,生成线程的开销会导致潜在的加速。
我的代码布局是:
for (int t = 0; t < max_time_steps; t++) {
// do some book-keeping
...
// perform time step
// (1) calculate righthand-side of ODE:
for (int i = 0; i < nr; i++) {
for (int j = 0; j < ntheta; j++) {
rhs[0][i][j] = A0[i][j] + B0[i][j] + ...;
rhs[1][i][j] = A1[i][j] + B1[i][j] + ...;
rhs[2][i][j] = A2[i][j] + B2[i][j] + ...;
rhs[3][i][j] = A3[i][j] + B3[i][j] + ...;
}
}
// (2) perform Euler step (or Runge-Kutta, ...)
for (int d = 0; d < 4; d++) {
for (int i = 0; i < nr; i++) {
for (int j = 0; j < ntheta; j++) {
next[d][i][j] = current[d][i][j] + time_step * rhs[d][i][j];
}
}
}
}
我认为这段代码应该很容易并行化......我把“#pragma omp parellel for”放在(1)和(2)循环前面,我还指定了核心数量(例如4核心) for loop(2)因为有四个变量)但是根本就没有加速。
我发现OpenMP在创建/销毁线程时非常聪明。即它意识到线程很快就会被要求,然后它们才会睡着以节省开销时间。
我认为一个“问题”是我的时间步长是在子程序中编码的(我使用的是RK4而不是Euler),而右手边的计算又是在time_step()调用的另一个子程序中功能。因此,我认为由于这个原因,OpenMP无法看到线程应该保持打开更长时间,因此线程在每个时间步都被创建和销毁。
在时间循环之前放置一个“#pragma omp parallel”以便在最开始创建线程会有帮助吗?然后对右手边(1)和欧拉步骤(2)进行实际的并行化?但是我该怎么做?
我已经找到了很多关于如何并行化嵌套for循环的例子,但是没有一个关注内部循环被分配到单独模块的设置。这会成为并行化的障碍吗?
我现在删除了d
循环(通过使索引显式化)并折叠了i
和j
循环(通过仅使用一个变量遍历整个2D数组)。
代码如下:
for (int t = 0; t < max_time_steps; t++) {
// do some book-keeping
...
// perform time step
// (1) calculate righthand-side of ODE:
#pragma omp parallel for
for (int i = 0; i < nr*ntheta; i++) {
rhs[0][0][i] = A0[0][i] + B0[0][i] + ...;
rhs[1][0][i] = A1[0][i] + B1[0][i] + ...;
rhs[2][0][i] = A2[0][i] + B2[0][i] + ...;
rhs[3][0][i] = A3[0][i] + B3[0][i] + ...;
}
// (2) perform Euler step (or Runge-Kutta, ...)
#pragma omp parallel for
for (int i = 0; i < nr*ntheta; i++) {
next[0][0][i] = current[0][0][i] + time_step * rhs[0][0][i];
next[1][0][i] = current[1][0][i] + time_step * rhs[1][0][i];
next[2][0][i] = current[2][0][i] + time_step * rhs[2][0][i];
next[3][0][i] = current[3][0][i] + time_step * rhs[3][0][i];
}
}
nr*ntheta
的大小为400*40=1600
,而我的max_time_steps=1000
时间步长为time
。尽管如此,并行化并不会导致加速:
没有OpenMP的运行时(命令行上real 0m23.597s
user 0m23.496s
sys 0m0.076s
的结果):
real 0m23.162s
user 7m47.026s
sys 0m0.905s
使用OpenMP运行时(24核)
double
我不明白这里发生了什么。
我在上面的代码片段中没有显示的一个特点是我的变量实际上不是double
,而是两个 static void Main(string[] args)
{
string CSVPath = @"D:\test.csv";
string outputText = "";
using (var reader = File.OpenText(CSVPath))
{
outputText = reader.ReadToEnd();
}
var colSplitter = ',';
var rowSplitter = new char[] { '\n' };
var rows = (from row in outputText.Split(rowSplitter, StringSplitOptions.RemoveEmptyEntries)
let cols = row.Split(colSplitter)
from col in cols
select new { totalCols = cols.Count(), cols = cols }).ToList();
int[] maxColLengths = new int[rows.Max(o => o.totalCols)];
for (int i = 0; i < rows.Count; i++)
{
for (int j = 0; j < rows[i].cols.Count(); j++)
{
int curLength = rows[i].cols[j].Trim().Length;
if (curLength > maxColLengths[j])
maxColLengths[j] = curLength;
}
}
Console.WriteLine(string.Join(", ", maxColLengths));
}
的自定义结构,类似于实部和虚部。但我认为这不应该有所作为。
在我单独离开并行化一段时间之后,只想报告一些成功。代码发展了一年,现在我回到了并行化。这一次,我可以说OpenMP可以完成它并减少所需的停机时间。
虽然代码整体发展,但我上面展示的这个特定循环并没有真正改变;只有两件事:a)分辨率更高,因此它覆盖了大约10倍的点数; b)每个循环的计算次数也大约是10倍(甚至更多)。
我唯一解释为什么它现在有效并且在一年多前没有工作的原因是,当我上次尝试并行化代码时,它的计算成本不够高,并且加速被OpenMP开销。单个循环现在需要大约200-300ms,而所需的时间必须是上次单个数字ms。
在比较gcc和英特尔编译器(在矢量化时做的工作非常不同)时,我可以看到这样的效果: a)使用gcc,一个循环在没有OpenMP的情况下需要大约300ms,在两个核心上只需要52%的时间 - &gt;近乎完美的优化。 b)使用icpc,一个循环在没有OpenMP的情况下需要大约160ms,在两个核心上它需要60%的时间 - &gt;良好的优化,但有效率降低约20%。
当使用两个以上内核时,加速不够大,不值得。