在线程之间划分循环迭代

时间:2009-02-19 09:52:02

标签: multithreading loops

我最近编写了一个小数字运算程序,它基本上循环遍历N维网格,并在每个点执行一些计算。

for (int i1 = 0; i1 < N; i1++)
  for (int i2 = 0; i2 < N; i2++)
    for (int i3 = 0; i3 < N; i3++)
      for (int i4 = 0; i4 < N; i4++)
        histogram[bin_index(i1, i2, i3, i4)] += 1; // see bottom of question

它工作正常,yadda yadda yadda,可爱的图形结果;-)然后我想,我的计算机上有2个核心,为什么不让这个程序多线程,所以我可以运行它两倍的速度?

现在,我的循环总共运行了大约十亿次计算,我需要一些方法将它们分散在线程中。我想我应该将计算分组为“任务” - 比如最外层循环的每次迭代都是一项任务 - 然后将任务分发给线程。我考虑过了

  • 给予线程#n最外层循环的所有迭代i1 % nthreads == n - 基本上预先确定哪些任务进入哪些线程
  • 尝试设置一个互斥保护变量,该变量保存下一个需要执行的任务的参数(在这种情况下为i1) - 动态地为线程分配任务

有什么理由选择一种方法而不是另一种方法?还是我没想过的另一种方法?它甚至重要吗?

顺便说一下,我用C编写了这个特定的程序,但我想我也会在其他语言中再做同样的事情,所以答案不一定是C特定的。 (如果有人知道Linux的C库可以做这类事情,我很想知道它)

编辑:在这种情况下,bin_index是一个确定性函数,除了自己的局部变量外,它不会改变任何东西。像这样:

int bin_index(int i1, int i2, int i3, int i4) {
    // w, d, h are constant floats
    float x1 = i1 * w / N,  x2 = i2 * w / N, y1 = i3 * d / N, y2 = i4 * d / N;
    float l = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + h * h);
    float th = acos(h / l);
    // th_max is a constant float (previously computed as a function of w, d, h)
    return (int)(th / th_max);
}

(虽然我很感谢所有评论,甚至那些不适用于确定性bin_index的评论)

8 个答案:

答案 0 :(得分:2)

第一种方法就足够了。这里不需要复杂化。如果您开始使用互斥锁,则可能难以发现错误。

除非你真的看到你需要这个,否则不要开始复杂化。同步问题(特别是在许多线程而不是许多进程的情况下)可能真的很痛苦。

答案 1 :(得分:2)

根据我的理解,OpenMP仅仅是为了你想要做的事情,虽然我不得不承认我还没有用过它。基本上它似乎归结为只包括一个标题并添加一个pragma子句。

您也可以使用英特尔的Thread Building Blocks图书馆。

答案 2 :(得分:2)

第一种方法很简单。如果您期望负载在线程上均匀平衡,这也就足够了。在某些情况下,特别是如果bin_index的复杂性非常依赖于参数值,其中一个线程最终可能会比其他线程更重的任务。记住:当最后一个线程结束时,任务就完成了。

第二种方法有点复杂,但如果任务足够精细(任务数量远远大于线程数),则可以更均匀地平衡负载。

请注意,将计算放在单独的线程中可能会出现问题。当多个线程同时执行时,确保bin_index正常工作。注意使用全局或静态变量来获得中间结果。

另外,“histogram [bin_index(i1,i2,i3,i4)] + = 1”可能会被另一个线程中断,导致结果不正确(如果赋值取值,则递增并存储结果数组中的值)。您可以为每个线程引入局部直方图,并在所有线程完成后将结果组合到单个直方图中。您还可以确保只有一个线程同时修改直方图,但这可能会导致线程在大多数情况下相互阻塞。

答案 3 :(得分:2)

如果您从未编写过多线程应用程序,那么我将首先介绍OpenMP:

  • 该库现在默认包含在gcc中
  • 这很容易使用

在您的示例中,您只需添加此编译指示:

#pragma omp parallel shared(histogram)
{
for (int i1 = 0; i1 < N; i1++)
  for (int i2 = 0; i2 < N; i2++)
    for (int i3 = 0; i3 < N; i3++)
      for (int i4 = 0; i4 < N; i4++)
        histogram[bin_index(i1, i2, i3, i4)] += 1;
}

使用这个编译指示,编译器将添加一些指令来创建线程,启动它们,添加一些互斥体来访问histogram变量等...有很多选项,但定义良好的pragma做所有为你工作。基本上,简单性取决于数据依赖性。

当然,结果不应该是最佳的,就像你手动编码一样。但是如果你没有负载平衡问题,你可能会加速2倍。实际上这只是在矩阵中写入而没有空间依赖性。

答案 4 :(得分:1)

我会做这样的事情:

void HistogramThread(int i1, Action<int[]> HandleResults)
{
    int[] histogram = new int[HistogramSize];

    for (int i2 = 0; i2 < N; i2++)
       for (int i3 = 0; i3 < N; i3++)
          for (int i4 = 0; i4 < N; i4++)
             histogram[bin_index(i1, i2, i3, i4)] += 1;

    HandleResults(histogram);
}

int[] CalculateHistogram()
{
    int[] histogram = new int[HistogramSize];

    ThreadPool pool; // I don't know syntax off the top of my head
    for (int i1=0; i1<N; i1++)
    {
       pool.AddNewThread(HistogramThread, i1, delegate(int[] h)
       {
           lock (histogram)
           {
               for (int i=0; i<HistogramSize; i++)
                   histogram[i] += h[i];
           }
       });
    }
    pool.WaitForAllThreadsToFinish();

    return histogram;
}

这样你就不需要共享任何内存,直到最后。

答案 5 :(得分:0)

如果您曾在.NET中使用过,请使用Parallel Extensions

答案 6 :(得分:0)

如果你想编写多线程数字运算代码(并且你将来会做很多)我建议你看一下使用像OCaml或Haskell这样的函数式语言。

由于缺乏副作用和函数式语言缺乏共享状态(大多数情况下),使您的代码跨多个线程运行更容易。另外,您可能会发现最终会得到更少的代码。

答案 7 :(得分:0)

我同意Sharptooth的说法,你的第一种方法似乎是唯一可行的方法。

您的单线程应用程序不断分配给内存。要获得任何加速,您的多个线程还需要连续分配给内存。如果一次只分配一个线程,则根本不会获得加速。因此,如果您的任务受到保护,整个练习将失败。

这是一种危险的方法,因为您在没有防护的情况下分配给共享内存。但似乎值得冒险(如果x2加速很重要)。如果你可以确定bin_index(i1,i2,i3,i4)的所有值在你的循环划分中是不同的,那么它应该工作,因为数组赋值将分配给共享内存中的不同位置。不过,人们总是应该看起来像这样的方法。

我假设您还会生成一个测试例程来比较两个版本的结果。

编辑:

查看你的bin_index(i1,i2,i3,i4),我怀疑你的进程如果没有相当大的努力就无法并行化。

在循环中划分计算工作的唯一方法是再次确保线程将访问内存中的相同区域。但是,看起来bin_index(i1,i2,i3,i4)可能会经常重复值。您可以将迭代划分为bin_index高于截止值并且低于截止值的条件。或者您可以任意划分它,看看增量是否以原子方式实现。但是,如果您只能使用两个内核来开始使用,那么任何复杂的线程方法看起来都不太可能提供改进。