如果我想并行化算法的执行,那么我应该拆分的小块代码是什么?
一个典型的例子是排序算法。对于什么元素大小或典型执行时间,在多个线程之间拆分排序是有意义的?或者什么时候等待另一个线程的开销大于单个线程上的执行时间?
有没有简单的规则?这取决于操作系统吗?
答案 0 :(得分:4)
关键规则是“仅当分叉开销远小于分叉工作量时才进行分叉”。因为分叉开销是您使用的特定技术的属性,所以工作的努力也是如此,您在某种意义上必须根据经验确定。您可能最终会在代码中使用一些阈值调整常量来表示这种权衡。
你在实践中会发现,找到可分离的大块工作实际上很难。如果你使工作块很小,它就没有很多依赖关系,你可以在它的所有输入数据流准备就绪后安排它。但是小块通常意味着小工作,而分叉开销通常会抵消收益。如果你试图让这些块很大,那么它们有很多依赖性,你无法将它们分解出来以便安排它们。
有些人很幸运,可以找到这么大的一块;我们称这些人为大多数物理学家和/或Fortran程序员,并且他们正在利用通过将世界划分为尽可能多的小块而引起的数据并行性。
我所知道的唯一合适的治疗方法是使用一种非常快速的分叉机制,这样你就可以找到最小的实用大块。不幸的是,提供这样做的并行库是......库,动态调用,具有相应的动态调用开销。包含并行原语的典型库需要100到数千个周期才能实现“fork”;如果你的大量工作是100台机器指令,这是个坏消息。
我强烈认为,为了获得如此快速的分叉机制,语言编译器必须知道你正在进行分叉,例如,“fork”(但是拼写:-)已经是该语言中的关键字。然后编译器可以看到分支,并预先分配所需的一切,以最大限度地缩短完成此任务所需的时间,并生成特殊代码来管理分叉(和加入)步骤。
我设计的PARLANSE语言以及我们在Semantic Designs中使用的语言就是这样一种语言。 它在语法上是一种类似Lisp的语言(但不是语义)。它的并行运算符拼写为“(|| ...)”。您可以在我们每天使用的Quicksort模块中看到它,如下所示。 您还可以查看根据经验确定的显式QuickSortParallelThreshold值。 此Quicksort在Intel x86系统上线性扩展到8个核心。
(define QuickSort
(module
(;; (define Value nu)
(compileifthen (~ (defined QuickSortWithParlanseBuiltInOrderingOfNu))
(define QuickSortWithParlanseBuiltInOrderingOfNu ~f) ; use PARLANSE comparison operators
)compileifthen
(compileifthen (~ (defined QuickSortParallelThreshold))
(define QuickSortParallelThreshold 100)
)compileifthen
(compileifthen (~ (defined QuickSortThreshold))
(compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu
(define QuickSortThreshold 16)
(define QuickSortThreshold 8)
)compileifthenelse
)compileifthen
(compileifthenelse (~ (defined QuickSortWithCompareByReference))
(define QuickSortWithCompareByReference ~f)
(compileifthen QuickSortWithParlanseBuiltInOrderingOfNu
(define QuickSortWithCompareByReference ~f)
)compileifthen
)compileifthenelse
(define SortRange
(action (procedure (structure (compileifthen (~ QuickSortWithParlanseBuiltInOrderingOfNu)
(compileifthenelse (~ QuickSortWithCompareByReference)
[compare (function (sort integer (range -1 +1)) (structure [value1 Value] [value2 Value]))]
[compare (function (sort integer (range -1 +1)) (structure [value1 (reference Value)] [value2 (reference Value)]))]
)compileifthenelse
)compileifthen
[a (reference (array Value 1 dynamic))]
[from natural]
[to natural]
)structure
)procedure
(local (;; (define quicksort
(action (procedure (structure [l integer] [r integer])))
)define
(define quicksort
(action (procedure (structure [l integer] [r integer]))
(ifthenelse (<= (- r l) (coerce integer QuickSortThreshold))
(do [i integer] (++ l) r +1
(local (= [exch Value] a:i)
(block exit_if_inserted
(;; (do [j integer] (-- i) l -1
(ifthenelse (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu
(> a:j exch)
(compileifthenelse (~ QuickSortWithCompareByReference)
(== (compare a:j exch) +1)
(== (compare (. a:j) (. exch)) +1)
)compileifthenelse
)compileifthenelse
(= a:(++ j) a:j)
(;; (= a:(++ j) exch)
(exitblock exit_if_inserted)
);;
)ifthenelse
)do
(= a:l exch)
);;
)block
)local
)do
(local (;; (= [i integer] l)
(= [j integer] r)
(= [p integer] l)
(= [q integer] r)
[exch Value]
);;
(;;
`use middle element as pivot':
(local (= [m integer] (// (+ l r) +2))
(;; (= exch a:m)
(= a:m a:r)
(= a:r exch)
);;
)local
`4-way partitioning = < > =':
(loop exit_if_partitioned
(;;
`find element greater than pivot':
(loop exit_if_greater_than_found
(;; (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu
(ifthenelse (< a:i a:r)
(consume ~t)
(ifthenelse (> a:i a:r)
(exitblock exit_if_greater_than_found)
(;; (ifthen (>= i j)
(exitblock exit_if_partitioned)
)ifthen
(= exch a:p)
(= a:p a:i)
(= a:i exch)
(+= p 1)
);;
)ifthenelse
)ifthenelse
(case (compileifthenelse (~ QuickSortWithCompareByReference)
(compare a:i a:r)
(compare (. a:i) (. a:r))
)compileifthenelse
-1
(consume ~t)
+1
(exitblock exit_if_greater_than_found)
else (;; (ifthen (>= i j)
(exitblock exit_if_partitioned)
)ifthen
(= exch a:p)
(= a:p a:i)
(= a:i exch)
(+= p 1)
);;
)case
)compileifthenelse
(+= i 1)
);;
)loop
`find element less than to pivot':
(loop exit_if_less_than_found
(;; (-= j 1)
(ifthen (>= i j)
(exitblock exit_if_partitioned)
)ifthen
(compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu
(ifthenelse (< a:j a:r)
(exitblock exit_if_less_than_found)
(ifthenelse (> a:j a:r)
(consume ~t)
(;; (-= q 1)
(= exch a:j)
(= a:j a:q)
(= a:q exch)
);;
)ifthenelse
)ifthenelse
(case (compileifthenelse (~ QuickSortWithCompareByReference)
(compare a:j a:r)
(compare (. a:j) (. a:r))
)compileifthenelse
-1
(exitblock exit_if_less_than_found)
+1
(consume ~t)
else (;; (-= q 1)
(= exch a:j)
(= a:j a:q)
(= a:q exch)
);;
)case
)compileifthenelse
);;
)loop
`move found elements to proper partitions':
(;; (= exch a:i)
(= a:i a:j)
(= a:j exch)
);;
`increment index':
(+= i 1)
);;
)loop
`3-way partitioning < = >':
(;;
`move pivot to final location':
(;; (= exch a:i)
(= a:i a:r)
(= a:r exch)
(= j (-- i))
(= i (++ i))
);;
`move elements equal to pivot to final locations':
(;; (do [k integer] l (-- p) +1
(;; (= exch a:k)
(= a:k a:j)
(= a:j exch)
(-= j 1)
);;
)do
(do [k integer] (-- r) q -1
(;; (= exch a:i)
(= a:i a:k)
(= a:k exch)
(+= i 1)
);;
)do
);;
);;
`sort partitions not equal to pivot':
(ifthenelse (<= (- r l) (coerce integer QuickSortParallelThreshold))
(;; (quicksort l j)
(quicksort i r)
);;
(|| (quicksort l j)
(quicksort i r)
)||
)ifthenelse
);;
)local
)ifthenelse
)action
)define
);;
(;; (quicksort (coerce integer from) (coerce integer to))
(ifdebug (do [i integer] (coerce integer from) (-- (coerce integer to)) +1
(trust (compileifthenelse QuickSortWithParlanseBuiltInOrderingOfNu
(<= a:i a:(++ i))
(compileifthenelse (~ QuickSortWithCompareByReference)
(<= (compare a:i a:(++ i)) +0)
(<= (compare (. a:i) (. a:(++ i))) +0)
)compileifthenelse
)compileifthenelse
`QuickSort:Sort -> The array is not sorted.'
)trust
)do
)ifdebug
);;
)local
)action
)define
(define Sort
(action (procedure (structure (compileifthen (~ QuickSortWithParlanseBuiltInOrderingOfNu)
(compileifthenelse (~ QuickSortWithCompareByReference)
[compare (function (sort integer (range -1 +1)) (structure [value1 Value] [value2 Value]))]
[compare (function (sort integer (range -1 +1)) (structure [value1 (reference Value)] [value2 (reference Value)]))]
)compileifthenelse
)compileifthen
[a (reference (array Value 1 dynamic))]
)structure
)procedure
(compileifthenelse (~ QuickSortWithParlanseBuiltInOrderingOfNu)
(SortRange compare a (coerce natural (lowerbound (@ a) 1)) (coerce natural (upperbound (@ a) 1)))
(SortRange a (coerce natural (lowerbound (@ a) 1)) (coerce natural (upperbound (@ a) 1)))
)compileifthenelse
)action
)define
);;
)module
)define
答案 1 :(得分:2)
这取决于线程间通信的开销。我用图像处理测试了openMP,并且有一行像素很方便,并且提供了很好的加速。我的图像是百万像素,所以有1000个任务,这可能足以让今天的多核机器忙碌。您也不需要将自己限制在大约一秒钟左右的工作中。在这个例子中,工作的加速大小为10毫秒,清晰可见。
现在这是一个令人愉快的算法,因为它不是递归的,所以一个任务没有依赖关系,所有任务都自动大小相同。
由于任务规模不同,排序算法将更加困难。您希望能够尝试这一点,并且可能选择更容易进行并列化的排序。
答案 2 :(得分:1)
参加并发和并行编程的几门课程。学习普通老叉子等几种技术。忘记或“手动”多线程(Java线程或pthreads),MPI,OpenMP,BSP,甚至可能是CUDA或OpenCL。然后要么决定成为专家,要么让专家设计并实施高效,正确的并行算法。当需要两者时,“平行”部分很容易,“有效”和“正确”部分不是。甚至由专家设计和实现的Java并发Vector集合也没有免于第一版中的错误。在Java标准的第一个版本中,内存模型的纯粹定义并不明确!
最简单的规则:使用由专家设计和实施的即用型组件 ,并且不要试图同时实现设计自己的并行算法的正确性和效率,除非你是专家。
答案 3 :(得分:1)
以编程方式解决这个问题是并行计算的圣杯之一,并且有许多库可以逼近特定问题的最佳并行性(例如,Data Parallel Haskell)。
无论如何,要手动完成这项工作,您需要了解:
假设算法是可并行化的,那么您的目标是找到线程的数量和数据的相对块大小,这样您就可以充分利用硬件来生成解决方案。
如果没有大量的实验,这很难做到。我首选的方法是运行大量基准测试,并将性能数据作为以下一种或多种组合的函数来获取:
无论如何,这不是一项容易的任务,并且有一些工具和库可以帮助您尽可能地挤出可并行化问题的性能。通过充分了解您的数据,代码和运行时环境,您可以正确地做到这一点的唯一合理方法。