Обсуждение участника:AKhimulya — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Многопоточная сортировка слиянием)
 
Строка 2: Строка 2:
  
 
Благодаря тому, что сортировка слиянием построена на принципе "Разделяй и властвуй", выполнение данного алгоритма можно весьма эффективно распараллелить. Теоретически можно достичь много лучшей асимптотики, однако на практике из-за огранечений по количеству потоков, которые могут выполняться независимо друг от друга, вычислительная сложность уменьшается на константу.
 
Благодаря тому, что сортировка слиянием построена на принципе "Разделяй и властвуй", выполнение данного алгоритма можно весьма эффективно распараллелить. Теоретически можно достичь много лучшей асимптотики, однако на практике из-за огранечений по количеству потоков, которые могут выполняться независимо друг от друга, вычислительная сложность уменьшается на константу.
===Алгоритм со слиянием за <math>\Omega(n)</math>===
+
===Сортировка с однопоточным слиянием===
 
Внесем в алгоритм сортировки слиянием следующую модификацию: будем сортировать левую и правую части массива параллельно.
 
Внесем в алгоритм сортировки слиянием следующую модификацию: будем сортировать левую и правую части массива параллельно.
  
Строка 14: Строка 14:
 
         merge(array, left, mid, right)
 
         merge(array, left, mid, right)
  
В данном алгоритме оператор '''spawn''' запускает новый поток, а оператор '''sync''' ожидает завершения этого потока. Функция merge аналогична функции merge из раздела [http://neerc.ifmo.ru/wiki/index.php?title=%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D1%81%D0%BB%D0%B8%D1%8F%D0%BD%D0%B8%D0%B5%D0%BC#.D0.A1.D0.BB.D0.B8.D1.8F.D0.BD.D0.B8.D0.B5_.D0.B4.D0.B2.D1.83.D1.85_.D0.BC.D0.B0.D1.81.D1.81.D0.B8.D0.B2.D0.BE.D0.B2 слияние двух массивов].
+
В данном алгоритме оператор '''spawn''' запускает новый поток, а оператор '''sync''' ожидает завершения этого потока. Функция merge аналогична функции merge из раздела [http://neerc.ifmo.ru/wiki/index.php?title=%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D1%81%D0%BB%D0%B8%D1%8F%D0%BD%D0%B8%D0%B5%D0%BC#.D0.A1.D0.BB.D0.B8.D1.8F.D0.BD.D0.B8.D0.B5_.D0.B4.D0.B2.D1.83.D1.85_.D0.BC.D0.B0.D1.81.D1.81.D0.B8.D0.B2.D0.BE.D0.B2 слияние двух массивов].<br>
Несмотря на наличие двух рекурсивных вызовов, при оценке будем считать, что совершается один вызов, т.к. оба вызова выполняются параллельно с одинаковой асимптотикой. Оценим время работы данного алгоритма:  <math>T(n) = T(n / 2) + \Omega(n) = \Omega(n)</math>. Данная асимптотика достигается при возможности запускать неограниченное количество потоков независимо друг от друга.
+
Несмотря на наличие двух рекурсивных вызовов, при оценке будем считать, что совершается один вызов, т.к. оба вызова выполняются параллельно с одинаковой асимптотикой. Оценим время работы данного алгоритма:  <math>T(n) = T(n / 2) + \Theta(n) = \Theta(n)</math>. Данная асимптотика достигается при возможности запускать неограниченное количество потоков независимо друг от друга.<br>
 
На практике на однопроцессорных компьютерах имеет смысл запускать алгоритм, ограничив количество допустимое количество потоков. Изменим код в соответствии с этими требованиями:
 
На практике на однопроцессорных компьютерах имеет смысл запускать алгоритм, ограничив количество допустимое количество потоков. Изменим код в соответствии с этими требованиями:
  
Строка 34: Строка 34:
  
 
Где threadCount - глобальный счетчик, а maxThreads - ограничение по количеству потоков.
 
Где threadCount - глобальный счетчик, а maxThreads - ограничение по количеству потоков.
Данный алгоритм будет иметь асимптотику: <math>\Omega((n / maxThreads) * \log((n / maxThreads)))</math>.
+
Данный алгоритм будет иметь асимптотику: <math>\Theta((n / maxThreads) * \log((n / maxThreads)))</math>.
  
===Алгоритм с многопоточным слиянием===
+
===Многопоточное слияние===
Как видно из оценки первого алгоритма, слияние выполняется слишком долго при том, что существует возможность его ускорить. Рассмотрим алгоритм рекурсивного слияния массивов <math>T[left1 \dots right1]</math> и <math>T[left2 \dots right2]</math> в массив <math>A[left3 \dots right3]</math>:
+
Как видно из оценки первого алгоритма, слияние выполняется слишком долго при том, что существует возможность его ускорить. Рассмотрим алгоритм рекурсивного слияния массивов <math>T[left_{1} \dots right_{1}]</math> и <math>T[left_{2} \dots right_{2}]</math> в массив <math>A[left_{3} \dots right_{3}]</math>:
# Убедимся, что размер <math>T[left1 \dots right1]</math> больше либо равен размеру <math>T[left2 \dots right2]</math>
+
# Убедимся, что размер <math>T[left_{1} \dots right_{1}]</math> больше либо равен размеру <math>T[left_{2} \dots right_{2}]</math>
# Найдем <math>x = T[mid1]</math> - середину первого массива (<math>x</math> также является и медианой этого массива)
+
# Вычислим <math>x = T[mid_{1}]</math> - середину первого массива (<math>x</math> также является и медианой этого массива)
# При помощи бинарного поиска найдем <math>mid2</math> такое, что <math>\forall y \in T[left2 \dots mid2 - 1]: y < x</math>
+
# При помощи бинарного поиска найдем <math>mid_{2}</math> такое, что <math>\forall y \in T[left_{2} \dots mid_{2} - 1]: y < x</math>
# <math>mid3 = left3 + (mid1 - left1) + (mid2 - left2)</math>
+
# <math>mid_{3} = left_{3} + (mid_{1} - left_{1}) + (mid_{2} - left_{2})</math>
# <math>A[mid3] = x</math>
+
# <math>A[mid_{3}] = x</math>
# Сольем <math>T[right1 \dots mid1 - 1]</math> и <math>T[rigth2 \dots mid2]</math> в <math>A[right3 \dots mid3 - 1]</math>
+
# Сольем <math>T[right_{1} \dots mid_{1} - 1]</math> и <math>T[right_{2} \dots mid_{2}]</math> в <math>A[right_{3} \dots mid_{3} - 1]</math>
# Сольем <math>T[mid1 + 1 \dots right1]</math> и <math>T[mid2 \dots right2]</math> в <math>A[mid3 + 1 \dots right3]</math>
+
# Сольем <math>T[mid_{1} + 1 \dots right_{1}]</math> и <math>T[mid_{2} \dots right_{2}]</math> в <math>A[mid_{3} + 1 \dots right_{3}]</math>
Приведем псевдокод данного алгоритма:
+
Рассмотрим псевдокод данного алгоритма:
  
 
     // если <math>right <= left</math> возвращает <math>left</math>
 
     // если <math>right <= left</math> возвращает <math>left</math>
Строка 52: Строка 52:
 
     binarySearch(x, array, left, right)
 
     binarySearch(x, array, left, right)
 
      
 
      
     // слияние <math>T[left1 \dots right1]</math> и <math>T[left2 \dots right2]</math> в <math>A[left3 \dots right1 - left1 + right2 - left2]</math>
+
     // слияние <math>T[left_{1} \dots right_{1}]</math> и <math>T[left_{2} \dots right_{2}]</math> в <math>A[left_{3} \dots right_{1} - left_{1} + right_{2} - left_{2}]</math>
     mergeMT(T, left1, right1, left2, right2, A, left3):
+
     mergeMT(T, <math>left_{1}</math>, <math>right_{1}</math>, <math>left_{2}</math>, <math>right_{2}</math>, A, <math>left_{3}</math>):
     n1 = right1 - left1 + 1
+
     <math>n_{1}</math> = <math>right_{1}</math> - <math>left_{1}</math> + 1
     n2 = right2 - left2 + 1
+
     <math>n_{2}</math> = <math>right_{2}</math> - <math>left_{2}</math> + 1
     if n1 < n2:
+
     if <math>n_{1}</math> < <math>n_{2}</math>:
         swap(left1, left2)
+
         swap(<math>left_{1}</math>, <math>left_{2}</math>)
         swap(right1, right2)
+
         swap(<math>right_{1}</math>, <math>right_{2}</math>)
         swap(n1, n2)
+
         swap(<math>n_{1}</math>, <math>n_{2}</math>)
 
      
 
      
     if n1 == 0:
+
     if <math>n_{1}</math> == 0:
 
         return
 
         return
 
     else
 
     else
         mid1 = (left1 + right1) / 2
+
         <math>mid_{1}</math> = (<math>left_{1}</math> + <math>right_{1}</math>) / 2
         mid2 = binarySearch(T[mid1], T, left2, right2)
+
         <math>mid_{2}</math> = binarySearch(T[<math>mid_{1}]</math>, T, <math>left_{2}</math>, <math>right_{2}</math>)
         mid3 = left3 + (mid1 - left1) + (mid2 - left2)
+
         <math>mid_{3}</math> = <math>left_{3}</math> + (<math>mid_{1}</math> - <math>left_{1}</math>) + (<math>mid_{2}</math> - <math>left_{2}</math>)
         A[mid3] = T[mid1]
+
         A[<math>mid_{3}</math>] = T[<math>mid_{1}</math>]
         '''spawn''' mergeMT(T, left1, mid1 - 1, left2, mid2 - 1, A, left3)
+
         '''spawn''' mergeMT(T, <math>left_{1}</math>, <math>mid_{1}</math> - 1, <math>left_{2}</math>, <math>mid_{2}</math> - 1, A, <math>left_{3}</math>)
         mergeMT(T, mid1 + 1, right1, mid2, right2, A, mid3 + 1)
+
         mergeMT(T, <math>mid_{1}</math> + 1, <math>right_{1}</math>, <math>mid_{2}</math>, <math>right_{2}</math>, A, <math>mid_{3}</math> + 1)
 
         '''sync'''
 
         '''sync'''
 +
 +
Оценим время выполнения данного алгоритма сверху. Оба массива содержат <math>n_{1} + n_{2} = n</math> элементов. К моменту рекурсивных вызовов <math>n_{2} <= n_{1}</math>, значит, <math>n_{2} = 2 * n_{2} / 2 <= (n_{1} + n_{2}) / 2 = n / 2</math>. В худшем случае один из двух рекурсивных вызовов сольет <math>n_{1} / 2</math> элементов <math>T[left_{1} \dots right_{1}]</math> с <math>n_{2}</math> элементами <math>T[left_{2} \dots right_{2}]</math> и тогда количество элементов первых двух массивов в рекурсивном вызове будет равно <math>n_{1} / 2 + n_{2} <= n_{1} / 2 + n_{2} / 2 + n_{2} / 2 = (n_{1} + n_{2}) / 2 + n_{2} / 2 <= n / 2 + n / 4 = 3 * n / 4</math>. Так как рекурсивные вызовы функции выполняются параллельно, время их выполнения будет равно времени выполнения самого долгого вызова. В худшем случае это <math>T(3 * n / 4)</math>. Тогда сумарное время работы алгоритма слияния будет равно <math>T(n) = T(3 * n  / 4) + \Theta(\log(n)) = \O(\log^2(n))</math>, т.к. асимпототика каждого вызова функции - <math>\Theta(\log(n))</math>, т.е. время, затрачиваемое на бинарный поиск.
 +
 +
===Сортировка с многопоточным слиянием===

Версия 16:11, 18 мая 2014

Многопоточная сортировка слиянием

Благодаря тому, что сортировка слиянием построена на принципе "Разделяй и властвуй", выполнение данного алгоритма можно весьма эффективно распараллелить. Теоретически можно достичь много лучшей асимптотики, однако на практике из-за огранечений по количеству потоков, которые могут выполняться независимо друг от друга, вычислительная сложность уменьшается на константу.

Сортировка с однопоточным слиянием

Внесем в алгоритм сортировки слиянием следующую модификацию: будем сортировать левую и правую части массива параллельно.

   mergeSortMT(array, left, right):
       mid = (left + right) / 2
   
       spawn mergeSortMT(array, left, mid)
       mergeSortMT(array, mid + 1, right)
       sync
   
       merge(array, left, mid, right)

В данном алгоритме оператор spawn запускает новый поток, а оператор sync ожидает завершения этого потока. Функция merge аналогична функции merge из раздела слияние двух массивов.
Несмотря на наличие двух рекурсивных вызовов, при оценке будем считать, что совершается один вызов, т.к. оба вызова выполняются параллельно с одинаковой асимптотикой. Оценим время работы данного алгоритма: [math]T(n) = T(n / 2) + \Theta(n) = \Theta(n)[/math]. Данная асимптотика достигается при возможности запускать неограниченное количество потоков независимо друг от друга.
На практике на однопроцессорных компьютерах имеет смысл запускать алгоритм, ограничив количество допустимое количество потоков. Изменим код в соответствии с этими требованиями:

   mergeSortMTBounded(array, left, right):
       mid = (left + right) / 2
   
       if threadCount [math]\lt [/math] maxThreads:
           threadCount++
           spawn mergeSortMTBounded(array, left, mid)
       else:
           mergeSortMTBounded(array, left, mid)
       mergeSortMTBounded(array, mid + 1, right)
       if threadCount [math]\leqslant[/math] maxThreads:
           sync
           threadCount--
   
       merge(array, left, mid, right)

Где threadCount - глобальный счетчик, а maxThreads - ограничение по количеству потоков. Данный алгоритм будет иметь асимптотику: [math]\Theta((n / maxThreads) * \log((n / maxThreads)))[/math].

Многопоточное слияние

Как видно из оценки первого алгоритма, слияние выполняется слишком долго при том, что существует возможность его ускорить. Рассмотрим алгоритм рекурсивного слияния массивов [math]T[left_{1} \dots right_{1}][/math] и [math]T[left_{2} \dots right_{2}][/math] в массив [math]A[left_{3} \dots right_{3}][/math]:

  1. Убедимся, что размер [math]T[left_{1} \dots right_{1}][/math] больше либо равен размеру [math]T[left_{2} \dots right_{2}][/math]
  2. Вычислим [math]x = T[mid_{1}][/math] - середину первого массива ([math]x[/math] также является и медианой этого массива)
  3. При помощи бинарного поиска найдем [math]mid_{2}[/math] такое, что [math]\forall y \in T[left_{2} \dots mid_{2} - 1]: y \lt x[/math]
  4. [math]mid_{3} = left_{3} + (mid_{1} - left_{1}) + (mid_{2} - left_{2})[/math]
  5. [math]A[mid_{3}] = x[/math]
  6. Сольем [math]T[right_{1} \dots mid_{1} - 1][/math] и [math]T[right_{2} \dots mid_{2}][/math] в [math]A[right_{3} \dots mid_{3} - 1][/math]
  7. Сольем [math]T[mid_{1} + 1 \dots right_{1}][/math] и [math]T[mid_{2} \dots right_{2}][/math] в [math]A[mid_{3} + 1 \dots right_{3}][/math]

Рассмотрим псевдокод данного алгоритма:

   // если [math]right \lt = left[/math] возвращает [math]left[/math]
   // если [math]x \lt = T[left][/math], возвращает [math]left[/math]
   // иначе возвращает наибольший индекс [math]i[/math] из отрезка [math][left; right][/math] такой, что [math]array[i - 1] \lt  x[/math]
   binarySearch(x, array, left, right)
   
   // слияние [math]T[left_{1} \dots right_{1}][/math] и [math]T[left_{2} \dots right_{2}][/math] в [math]A[left_{3} \dots right_{1} - left_{1} + right_{2} - left_{2}][/math]
   mergeMT(T, [math]left_{1}[/math], [math]right_{1}[/math], [math]left_{2}[/math], [math]right_{2}[/math], A, [math]left_{3}[/math]):
   [math]n_{1}[/math] = [math]right_{1}[/math] - [math]left_{1}[/math] + 1
   [math]n_{2}[/math] = [math]right_{2}[/math] - [math]left_{2}[/math] + 1
   if [math]n_{1}[/math] < [math]n_{2}[/math]:
       swap([math]left_{1}[/math], [math]left_{2}[/math])
       swap([math]right_{1}[/math], [math]right_{2}[/math])
       swap([math]n_{1}[/math], [math]n_{2}[/math])
   
   if [math]n_{1}[/math] == 0:
       return
   else
       [math]mid_{1}[/math] = ([math]left_{1}[/math] + [math]right_{1}[/math]) / 2
       [math]mid_{2}[/math] = binarySearch(T[[math]mid_{1}][/math], T, [math]left_{2}[/math], [math]right_{2}[/math])
       [math]mid_{3}[/math] = [math]left_{3}[/math] + ([math]mid_{1}[/math] - [math]left_{1}[/math]) + ([math]mid_{2}[/math] - [math]left_{2}[/math])
       A[[math]mid_{3}[/math]] = T[[math]mid_{1}[/math]]
       spawn mergeMT(T, [math]left_{1}[/math], [math]mid_{1}[/math] - 1, [math]left_{2}[/math], [math]mid_{2}[/math] - 1, A, [math]left_{3}[/math])
       mergeMT(T, [math]mid_{1}[/math] + 1, [math]right_{1}[/math], [math]mid_{2}[/math], [math]right_{2}[/math], A, [math]mid_{3}[/math] + 1)
       sync

Оценим время выполнения данного алгоритма сверху. Оба массива содержат [math]n_{1} + n_{2} = n[/math] элементов. К моменту рекурсивных вызовов [math]n_{2} \lt = n_{1}[/math], значит, [math]n_{2} = 2 * n_{2} / 2 \lt = (n_{1} + n_{2}) / 2 = n / 2[/math]. В худшем случае один из двух рекурсивных вызовов сольет [math]n_{1} / 2[/math] элементов [math]T[left_{1} \dots right_{1}][/math] с [math]n_{2}[/math] элементами [math]T[left_{2} \dots right_{2}][/math] и тогда количество элементов первых двух массивов в рекурсивном вызове будет равно [math]n_{1} / 2 + n_{2} \lt = n_{1} / 2 + n_{2} / 2 + n_{2} / 2 = (n_{1} + n_{2}) / 2 + n_{2} / 2 \lt = n / 2 + n / 4 = 3 * n / 4[/math]. Так как рекурсивные вызовы функции выполняются параллельно, время их выполнения будет равно времени выполнения самого долгого вызова. В худшем случае это [math]T(3 * n / 4)[/math]. Тогда сумарное время работы алгоритма слияния будет равно [math]T(n) = T(3 * n / 4) + \Theta(\log(n)) = \O(\log^2(n))[/math], т.к. асимпототика каждого вызова функции - [math]\Theta(\log(n))[/math], т.е. время, затрачиваемое на бинарный поиск.

Сортировка с многопоточным слиянием