【问题标题】:What operations cause parallel code to run slowly?哪些操作会导致并行代码运行缓慢?
【发布时间】:2010-12-08 18:17:40
【问题描述】:

阅读:The Hitchhiker's Guide to Concurrency,更具体地说,关于Amdahl's Law 的部分 - 并行程序的运行速度与其最慢的部分一样快,并且程序从一开始就并行越快,它就会越快在引入更多内核之后,我发现自己想知道:如何确保从一开始就编写尽可能并行的代码?如何确保我的代码能够从添加多个内核中获得最大可能的好处?而且,什么样的操作会导致代码不并行,或者并行代码变慢?代码示例当然会受到赞赏。

【问题讨论】:

  • 这个问题对程序员来说可能会更好,因为它有点开放。
  • 为什么选择 Erlang 作为标签?完全没有理由用那种语言来限制你的问题。

标签: erlang parallel-processing


【解决方案1】:

并非所有代码天生就能够并行运行。

并行执行意味着同时,同时。如果代码不依赖于与它一起运行的其他代码的最终结果,则它只能同时运行。如果您正在做这样的等式:

((((x+y)+z)+a)*b)

每个括号必须在下一个阶段之前完成,所以操作不能顺序进行。为了使程序并行化,重要的是要确定何时可以将大型任务分解为可以同时完成的部分。

考虑一个总和,我需要将 100,000 个数字相加。

sum = 0;
for (i = 0; i < 100000; i++) {
   sum += numbers[i];
}

加法是可交换传递的,a + b + c + d可以拆分为(a + b + c) + da + (b + c) + d(a + b) + (c + d)等。最后一种情况是工作的均匀分布,一半在一个括号中,一半在一个括号中另一个。

所以假设我们做了这样的大任务:

sumA = 0;
for (i = 0; i < 100000 / 2; i++) {
   sumA += numbers[i];
}

sumB = 0;
for (i = 100000 / 2; i < 100000; i++) {
   sumB += numbers[i];
}

sum = sumA + sumB;

一分为二,两个循环可以同时运行,仍然得到相同的答案,我们只需要在最后重新组合。

在并行编程中,这是关键,将工作分成每个工作人员(cpu/节点/机器)的部分,然后收集结果并组装最终结果。我们称之为分散和聚集。

许多计算任务可以拆分,有些则不能。一个适合拆分的程序是并行的理想程序,一个不是,不是。还要考虑分散和收集、拆分数据、重新组合(可能传输它)的大量开销 - 并不总是值得划分任务。

所以,回答你的问题。与其说你能做些什么让你的程序并行,不如说你的程序是否自然能够如此。

【讨论】:

  • 当然,((((x+y)+z)+a)*b)可以并行化,等同于b*(a+x)+b*(y+z )。我指出这一点,不是因为我不同意您的第一句话,而是因为您选择的示例很好地说明了这一点,即如果分析得当,许多显然是串行的计算可以并行进行。
  • @Mark:是的,公平竞争,可以重新排列,我想要括号中的任意内容,它本质上是连续的,并且在不考虑运算符或其他任何内容的情况下编写它 - 括号很重要部分。应该使用通用运算符符号或函数 - 但不想让示例 看起来 比需要的更复杂。因此,超级简单的例子。
  • 强制串行计算的主要示例通常是在数值算法中迭代逼近一个值。您需要前面的步骤来计算下一步,并且您必须等到前面的步骤。重要的是不仅有几种可能的结果。因为如果有 2 个结果,那么当您知道哪个结果是正确的时,您可以推测性地计算两者并选择正确的一个。
  • @I GIVE CRAP ANSWERS: 你的名字不合适。迭代/递归代码是第一种让我觉得可能只是串行的类型,并非总是如此,但更有可能。
  • Orbling:状态机在收到每条消息时从一种状态移动到另一种状态是所谓的尴尬串行问题的另一个例子。
【解决方案2】:

使用纯函数式语言(没有副作用)是一个好的开始(例如:Haskell、Lisp 等)

如果您处于更 OO 的环境中,请尽可能编写不可变类(在 Java 中:使用 final 字段,并让这些字段本身属于不可变类型),并封装您的可变字段,这样您就不必担心它们会被另一个线程上运行的代码更改。

最后,即使您的代码非常可并行化,它仍然会运行缓慢!在项目接近尾声时,分析您的应用并首先处理最慢的部分。

祝你好运!

【讨论】:

  • +1 指出并行化并不能解决您的所有问题,而对算法瓶颈的分析和最好的设计阶段分析是第一站和后备。
  • “使用纯函数式语言是一个好的开始”。实际上 Lisp 是不纯的,Haskell 在多核并行的背景下是个笑话。
  • 也许 Erlang 是更好的选择?我没有意识到 Lisp 是不纯的。
【解决方案3】:

计算机科学中的许多答案概念;视情况而定。

首先,Erlang 创建并行应用程序的方法是使用 EVM 轻量级进程。 EVM 调度程序为每个进程分配不同内核上的执行时间。更多进程意味着允许进程并行执行的更多可能性但是这并不意味着应用程序会从中受益。

基本上:http://www.erlang.org/doc/efficiency_guide/processes.html

要通过使用 SMP 模拟器获得性能,您的应用程序在大多数情况下必须有多个可运行的 Erlang 进程。否则,Erlang 模拟器仍然只能运行一个 Erlang 进程,但您仍然必须支付锁定的开销。尽管我们尝试尽可能减少锁定开销,但它永远不会完全为零。

您的应用程序的其他属性也必须考虑在内。 Erlang/OTP 的一个主要好处是进程之间可用的隔离。例如,Web 服务器或电信节点可以通过生成进程对每个传入请求/呼叫进行并行处理。但其他主要好处是“免费”的进程(传入请求)隔离、监督和容错行为。 Erlang/OTP 在这些情况下表现良好。

另一方面,只有顺序操作的应用程序(即 lists:foldl/3)可能不会通过生成进程而受益。由于产生进程开销,它可能反而会失去性能。

还要考虑系统并行执行的其他部分可能会影响到哪些方面。通过产生许多都将访问相同资源的进程,您可能正在查看即 IO 问题。我编写了运行速度超快的并行代码,但由于访问外部资源成为限制,我不得不移除并行性。

闲聊之后,我将尝试回答您的问题。

  • 并行化是通过生成多个可以与另一个进程同时执行的进程来实现的。
  • 生成进程时会产生成本,即使它很便宜。
  • 为每个操作生成进程可能根本不会使顺序操作受益,相反,它可能会损害性能。
  • 在 Erlang/OTP 中生成进程时可能会获得其他主要好处。确保它们与您的应用程序一起工作。
  • 从并行进程访问相同类型的资源时,请注意“扩展问题”。
  • 通过为您的操作生成或不生成进程来测试有无并行执行。
  • 再测试一下。

【讨论】:

    【解决方案4】:

    请记住,应用程序的序列化部分通常取决于问题的大小。同样通常,序列化部分是固定的,或者随着问题大小缓慢增长,因此总体而言,随着问题大小的增加,序列化部分的百分比影响会减小。另见Gustafson's law

    通常,您的应用程序的序列化部分是显而易见的。一种常见的模式是处理请求的单个服务器进程,例如。来自网络套接字,它将工作分配给工作进程。整个系统的运行速度不会超过单个进程从网络中读取数据并将任务分配给工作人员的速度。要增加并行度,您需要有多个并行服务器。

    另一个类似的例子是让单个进程控制共享资源。为了请求此共享资源的子资源,来自多个源的所有请求都必须序列化。这里增加并行性的一种方法是拥有多个控制器,每个控制器都是完整资源的某个子部分,当请求访问时,源将随机选择要请求的控制器(或其他一些半均匀分布请求的方式)。这本质上是Sharding 资源。

    【讨论】:

    • +1 大规模任务往往具有更大的并行空间。
    【解决方案5】:

    如何确保我从一开始就编写尽可能并行的代码?

    一般来说,问题与数据依赖性有关。如果一个操作需要数据才能开始执行自身,它依赖另一个操作。这里有一些重要的技巧。假设您知道一项操作只能有两种可能的结果。然后你可以开始两个计算,然后“选择”正确的一个。事实上,CPU 设计经常在计算单元中利用这个想法(参见例如Carry Select Adders 上的维基百科文章)。另一个技巧是当你知道结果将是一个特定的值时,比如 99% 的确定性。然后你可以推测性地执行下一个操作,并希望你的预测是真的。如果失败,您可以随时回滚计算并重做。这也在现代 CPU 中完成,例如,参见带有推测执行和重新排序缓冲的分支预测。

    此时,您应该清楚现代 CPU 架构已经采用了大量的并行化技巧。问题是他们已经挤出了我们希望获得的所有本地并行化,现在我们需要将这些想法“提升到一级”到我们的程序中。

    如何确保我的代码能够从添加多个内核中获得最大可能的好处?

    扼杀并行计算的是依赖关系。如果您依赖于计算的另一部分,您将需要在并行执行线程之间通信。这会导致 stalls,因为您正在等待其他部分发送消息。一个经常使用的技巧是延迟隐藏:您不必等待消息到达,而是在中间做一些其他事情,希望数据在您需要时已经完全传输。但更好的是,如果您可以安排您的代码,使其根本不需要通信。

    这就是为什么函数式编程被视为一种强大的并行工具。在 FP 中,没有共享状态,因此代码已经处于很容易将小包计算分叉到不同 CPU 的状态。由于关键字par,在我看来,Haskell 是最成熟的语言。

    一般来说,如果你的程序在数据流中的依赖很少,那么在添加多核的时候很容易做快。您希望尽可能避免数据流中的序列化阻塞点。

    Erlang 我不愿提及 Erlang 是因为 Erlang 间接获得了并行性。在 Erlang 中,我们将程序描述为一组并发活动,即一组协同工作以解决问题的进程。进程通过消息传递相互隔离和通信。 Erlang 的主要独特优势是容错能力:由于隔离,一个进程中的错误不可能影响另一个进程 - 因此您可以构建在单个进程进入僵死僵尸状态的情况下恢复的软件。

    现在这个模型在很大程度上倾向于一种明显的并行评估策略:当一个核心处理一个进程时,我们可以让多余的核心抓住其他进程进行处理。同样,如果进程不相互依赖,等待消息到达,那么添加更多内核将产生加速。

    虽然它不是神奇的银弹。如果您通过单个进程将所有消息序列化为“阻塞点”,那么您的代码将不会是并行的,Amdahl 或 Gustafsson 将发挥作用,而您将得到一个串行程序。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-03-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多