自动并行化

您对将尝试获取代码并自动将其拆分为线程的项目有何看法(可能是编译时,可能是在运行时)。

看看下面的代码:

for(int i=0;i<100;i++) sum1 += rand(100) for(int j=0;j<100;j++) sum2 += rand(100)/2 

这种代码可以自动分成两个并行运行的不同线程。 你认为它甚至可能吗? 我有一种感觉,从理论上说这是不可能的(它让我想起停止问题),但我无法certificate这种想法。

你认为这是一个有用的项目吗? 有什么类似的吗?

在一般情况下是否可以知道一段代码是否可以并行化并不重要,因为即使您的算法无法检测到所有可以并行化的情况,也许它可以检测到其中的一些。

这并不意味着它会有用。 考虑以下:

  1. 首先,要在编译时执行此操作,您必须检查可能在要并行化的构造内部可能到达的所有代码路径。 除了简单的计算之外,这对于任何事情都可能很棘手。
  2. 其次,你必须以某种方式决定什么是可并行化和什么不可并行化。 例如,你不能轻易地将一个将相同状态修改为多个线程的循环分解。 这可能是一项非常困难的任务,在许多情况下,您最终会不确定 – 实际上两个变量可能引用同一个对象。
  3. 即使你可以做到这一点,它最终会让用户感到困惑。 要解释为什么他的代码不可并行化以及应该如何更改它将是非常困难的。

我认为,如果你想在Java中实现这一点,你需要将它更多地作为一个库来编写,并让用户决定要并行化的内容(库函数和注释?只是大声思考)。 function语言更适合这种情况。

作为一个琐事:在并行编程课程中,我们必须检查代码并确定它是否可并行化。 我不记得具体内容(关于“最多一次”属性的内容?有人填写我的内容吗?),但故事的寓意是即使对于看似微不足道的案件也是如此。

这称为自动并行化。 如果您正在寻找一些程序,您可以使用它为您执行此操作,它尚不存在。 但它最终可能会。 这是一个难题,是一个积极研究的领域。 如果你仍然好奇……

可以自动将您的示例拆分为多个线程,但不是按照您的想法。 一些当前的技术尝试在其自己的线程中运行for -loop的每次迭代。 一个线程将得到偶数指数(i = 0,i = 2,…),另一个将得到奇数指数(i = 1,i = 3,…)。 一旦完成-loop,就可以启动下一个。 其他技术可能会变得更疯狂,在一个线程中执行i++增量,在单独的线程上执行rand()

正如其他人指出的那样,迭代之间存在真正的依赖关系,因为rand()具有内部状态。 这并不妨碍并行化。 编译器可以识别内存依赖性,并且rand()的修改状态可以从一个线程转发到另一个线程。 但它可能会限制你只有几个并行线程。 如果没有依赖关系,您可以在尽可能多的内核上运行它。

如果您真的对这个主题感兴趣并且不介意筛选研究论文:

  1. G. Ottoni通过解耦软件流水线自动提取线程 (2005)。
  2. 使用 A. Raman的软件multithreading事务 (2010)进行推测并行化 。

这实际上是不可能的。

问题是,您需要提前知道比编译器甚至运行时可用的更多信息,以便有效地并行化。

虽然可以并行化非常简单的循环,但即使这样,也存在风险。 例如,如果rand()是线程安全的,那么上面的代码只能并行化 – 并且许多随机数生成例程都不是。 (Java的Math.random()会为您同步 – 但是。)

至少在这一点上,尝试进行这种类型的自动并行化对任何“真实”应用程序都不实用。

这当然是可能的,但这是一项非常艰巨的任务。 几十年来,这一直是编译器研究的核心。 基本问题是我们不能创建一个可以为java代码找到最佳分区的工具(这相当于暂停问题)。

相反,我们需要放松我们的目标,从最佳分区到代码的某个分区。 这一般来说仍然很难。 那么我们需要找到简化问题的方法,一个是忘记一般代码并开始查看特定类型的程序。 如果你有简单的控制流(恒定有界for循环,有限的分支……)那么你可以做更多的事情。

另一个简化是减少您尝试保持忙碌的并行单元的数量。 如果将这两种简化放在一起,那么您将获得自动矢量化的最新技术(用于生成MMX / SSE样式代码的特定类型的并行化)。 进入那个阶段需要几十年,但如果你看看像英特尔这样的编译器,那么性能开始变得相当不错。

如果从单个线程内的向量指令移动到进程中的多个线程,那么在代码中的不同点之间移动数据的延迟会大大增加。 这意味着您的并行化必须要好得多才能赢得通信开销。 目前,这是研究中一个非常热门的话题,但是没有可用的自动用户目标工具。 如果你能写一个有效的,那对很多人来说都会很有趣。

对于您的具体示例,如果您假设rand()是并行版本,因此您可以从不同的线程中独立调用它,那么很容易看到代码可以拆分为两个。 编译器只需转换需要依赖性分析,看看两个循环都不使用来自或影响另一个循环。 因此,用户级代码中它们之间的顺序是可以拆分的错误依赖(即通过将每个放在一个单独的线程中)。

但这并不是你想要如何并行化代码。 看起来每个循环迭代都依赖于前一个,因为sum1 + = rand(100)与sum1 = sum1 + rand(100)相同,其中右侧的sum1是前一次迭代的值。 然而,唯一涉及的操作是加法,它是关联的,所以我们用许多不同的方式重写总和。

 sum1 = (((rand_0 + rand_1) + rand_2) + rand_3) .... sum1 = (rand_0 + rand_1) + (rand_2 + rand_3) ... 

第二个优点是括号中的每个单独添加可以与所有其他的并行计算。 一旦你有50个结果,那么它们可以组合成另外25个添加等等……你做更多的工作这样50 + 25 + 13 + 7 + 4 + 2 + 1 = 102个添加而不是原来的100个但是那里只有7个连续步骤,因此除了并行分叉/连接和通信开销之外,它运行速度快14倍。 这种添加树在并行体系结构中称为聚集操作,它往往是计算中昂贵的部分。

在诸如GPU之类的非常并行的体系结构上,以上描述将是并行化代码的最佳方式。 如果你在一个进程中使用线程,它将被开销所杀死。

总结 :不可能完美地完成,很难做得很好,有很多积极的研究来找出我们能做多少。

有些项目试图简化并行化 – 例如Cilk 。 然而,它并不总是那么好用。

编程语言是Java,Java是虚拟机。 因此,不应该能够在运行时在VM拥有的不同线程上执行代码。 由于所有的内存等都被处理,因为它不会导致任何损坏。 您可以将代码看作是一堆估算执行时间的指令,然后将它分发到一个线程数组上,每个线程都有一个同时执行堆栈。 虽然像OpenGL立即模式这样的图形需要维护顺序并且大部分都不应该是线程的,但这可能是危险的。