【问题标题】:Dividing loop iterations among threads在线程之间划分循环迭代
【发布时间】:2009-02-19 09:52:02
【问题描述】:

我最近编写了一个小型数字运算程序,它基本上在 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 个内核,为什么不让这个程序多线程,这样我可以将它的运行速度提高一倍?

现在,我的循环总共运行了大约 10 亿次计算,我需要一些方法在线程之间拆分它们。我想我应该将计算分组为“任务” - 说最外层循环的每次迭代都是一个任务 - 并将任务分发给线程。我考虑过

  • 只给线程#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);
}

(虽然我很欣赏所有的 cmets,即使是那些不适用于确定性 bin_index 的 cmets)

【问题讨论】:

  • 您能否更明确地说明您操作的数据?数据依赖性在并行编程中更为重要。开发 bin_index 中的内容
  • 我添加了一些关于我正在尝试编写的具体示例的更多细节,希望对您有所帮助...

标签: multithreading loops


【解决方案1】:

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

第二种方法稍微复杂一些,但如果任务足够细粒度(任务数远大于线程数),负载会更均匀。

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

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

【讨论】:

  • +1 for the '"histogram[bin_index(i1, i2, i3, i4)] += 1" could be interrupted by another thread' 段落。
  • 作为组合线程本地直方图的替代方案,理论上您还可以拥有一个与直方图数组大小相同的锁或互斥锁数组,以避免不必要的块。这对于大量线程来说会更节省内存。
  • 我不同意直方图的东西。如果在数组的不同索引处写入(或读取),则没有问题,这里似乎就是这种情况。中断问题在这里不是问题。
  • @Jerome - 你说的是“histogram[bin_index(i1, i2, i3, i4)] += 1" 归结为原子操作。情况可能并非如此,具体取决于直方图的类型和 bin_index(i1, i2, i3, i4) 的副作用。您正在更改未指定类型的数组的内容。
  • @smacl:在这种情况下,直方图的长度约为 10000,这是很多互斥锁 ;-) 有趣的想法。
【解决方案2】:

第一种方法就足够了。这里不需要复杂化。如果您开始使用互斥锁,您可能会很难检测到错误。

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

【讨论】:

  • 我认为这行不通,因为两个线程可能同时更新同一个直方图元素。同样, bin_index(i1, i2, i3, i4) 可以访问直方图或有其他副作用。
  • 他们是否会尝试这样做取决于 bin_index() 的工作方式。
【解决方案3】:

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

您也可以使用英特尔的Thread Building Blocks 库。

【讨论】:

  • 感谢您的链接,我将不得不看看。
【解决方案4】:

如果您从未编写过多线程应用程序,我可以让您从 OpenMP 开始:

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

在你的例子中,你应该只需要添加这个 pragma:

#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;
}

使用这个 pragma,编译器将添加一些指令来创建线程、启动它们、围绕对 histogram 变量的访问添加一些互斥锁等...有很多选项,但定义明确的 pragma 可以完成所有工作为你。基本上,简单性取决于数据依赖性。

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

【讨论】:

    【解决方案5】:

    我会这样做:

    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;
    }
    

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

    【讨论】:

    • +1 - 这与我最终所做的非常相似 ;-)
    【解决方案6】:

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

    【讨论】:

      【解决方案7】:

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

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

      【讨论】:

      • 听起来是学习 Haskell 的一个很好的借口 ;-) 与 C 相比,Haskell 或 OCaml 的速度如何?
      【解决方案8】:

      我同意 Sharptooth 的观点,即您的第一种方法似乎是唯一可行的方法。

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

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

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

      编辑:

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

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

      【讨论】:

        猜你喜欢
        • 2013-01-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-02
        • 2015-01-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多