什么操作导致并行代码运行缓慢?

时间:2010-12-08 18:17:41

标签: erlang parallel-processing

5 个答案:

答案 0 :(得分:5)

并非所有代码都能够并行运行。

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

((((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 / node / machine)的部分,然后收集结果并汇总最终结果。我们称之为散射和聚集。

许多计算任务可以分开,有些则不能。一个非常适合分裂的程序是理想的并行,一个不是,不是。还要考虑散射和收集,分割数据,重组数据(可能转移数据)有很大的开销 - 并不总是值得分割任务。

所以,回答你的问题。并不是说你可以做什么来使你的程序并行,因为你的程序是否自然能够如此。

答案 1 :(得分:2)

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

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

最后,即使您的代码非常可并行,它仍然可以运行缓慢!在项目结束时,首先对您的应用进行概要分析并处理最慢的部分。

祝你好运!

答案 2 :(得分:2)

许多计算机科学的答案概念;这取决于。

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

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

  

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

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

另一方面,仅具有顺序操作的应用程序(即列表:foldl / 3)可能不会通过产生进程使任何事情受益。由于产生过程开销,它可能会失去性能。

还要考虑系统并行执行的其他部分可能会产生什么影响。通过产生许多进程,所有进程都将访问您可能正在查看的相同资源,即IO问题。我编写的并行代码工作速度超快但被迫删除并行性,因为访问外部资源成了限制。

经过漫无边际的讨论,我会尽力回答你的问题。

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

答案 3 :(得分:2)

请记住,通常,应用程序的序列化部分取决于问题的大小。通常,序列化部分要么是固定的,要么是随着问题大小的增长而缓慢增长,因此当您增加问题大小时,串行部分的百分比影响会减小。另请参阅Gustafson's law

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

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

答案 4 :(得分:0)

如何确保从头开始编写尽可能平行的代码?

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

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

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

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

这就是函数式编程被视为并行的强大工具的原因。在FP中,没有共享状态,因此代码已经处于这样一种状态,即很容易将小的计算捆绑到不同的CPU。由于关键字par,Haskell是我认为这个想法最成熟的语言。

通常,如果您的程序在数据流中几乎没有依赖关系,那么在添加多个核心时很容易实现。您希望尽可能避免数据流中的序列化阻塞点。

Erlang 我提到Erlang的犹豫是Erlang间接得到了它的并行性。在Erlang中,我们将程序描述为一组并发活动,即一组进程,它们协同工作以解决问题。进程被隔离并通过消息传递进行通信。 Erlangs的主要独特优势在于容错:一个进程中的错误不可能因隔离而影响另一个进程 - 因此您可以构建在单个进程进入死亡行走僵尸状态时将恢复的软件。

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

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