Использование .hyper
и .race
приводит к ускорению только при параллельной реализации следующей операции. На момент написания в Rakudo не было параллельной sort
реализации, а это означает, что она вернется к использованию реализации обычной сортировки. Таким образом, это объясняет, почему native-hyper-sort
не выходит быстрее в настоящее время (однако это почти наверняка произойдет в будущем).
Идея в simple-hyper-sort
заключается в правильном направлении: разбить данные наподсписки, сортировать подсписки, а затем объединить их. Поэтому мы можем распараллелить сортировку подсписков. Как вы уже заметили, это достижение выигрыша зависит от того, что сама операция слияния является достаточно быстрой, и поэтому нам необходимо тщательно оптимизировать ее.
Намного проще написать сжатый текст (не говоря уже о правильном). !) операция слияния, если требуется объединить только два подсписка. Таким образом, нам нужно структурировать проблему таким образом, чтобы это нам давалось. Это указывает на другой подход:
- Разбить список пополам
start
задача сортировать каждую половину await
две задачи - Слияние результатов двух задач
Обратите внимание, что шаг 2 включает в себя рекурсию. Мы прекращаем повторение, когда размер раздела слишком мал, и используем встроенные sort
на таких разделах. (Мы можем определить «слишком маленький», разделив размер входного списка на количество ядер ЦП, как показано в вашем примере.)
Таким образом, мы получаем решение, подобное этому:
sub parallel-merge-sort {
my $to-sort = "tosort.txt".IO.open( :r );
my $lines = $to-sort.lines;
return do-sort $lines, ceiling($lines.elems / $*KERNEL.cpu-cores);
sub do-sort(@in, $limit) {
if @in.elems < $limit {
@in.sort
}
else {
my $pivot = @in.elems div 2;
merge |await
(start do-sort @in[0..$pivot], $limit),
(start do-sort @in[$pivot^..@in.end], $limit)
}
}
sub merge(@a, @b) {
my @result;
my int $a-idx = 0;
my int $a-elems = +@a;
my int $b-idx = 0;
my int $b-elems = +@b;
my int $r-idx = 0;
while $a-idx < $a-elems && $b-idx < $b-elems {
my $a := @a[$a-idx];
my $b := @b[$b-idx];
if $a before $b {
$a-idx++;
@result[$r-idx++] := $a;
}
else {
$b-idx++;
@result[$r-idx++] := $b;
}
}
if $a-idx < $a-elems {
@result[$r-idx++] := $_ for @a[$a-idx..*];
}
elsif $b-idx < $b-elems {
@result[$r-idx++] := $_ for @b[$b-idx..*];
}
return @result;
}
}
Я не потратил ужасно много времени на оптимизацию этого (не профилировал и т. Д.), Но позаботился об использовании нативов и привязок, чтобы уменьшить распределение. На Моей Машине, однако, это дает ускорение по сравнению с последовательной сортировкой.
Еще одно простое ускорение, которое мы можем получить - за счет чуть большей сложности в коде, - это от осознания того, что мы надеваемне нужно нарезать входные данные в do-sort
до того момента, когда мы действительно должны отправить его во встроенный sort
:
sub do-sort(@in, $limit, $from = 0, $to = @in.end) {
my $elems = $to - $from;
if $elems < $limit {
@in[$from..$to].sort
}
else {
my $pivot = $from + $elems div 2;
merge |await
(start do-sort @in, $limit, $from, $pivot),
(start do-sort @in, $limit, $pivot + 1, $to)
}
}
, что экономит некоторую работу;к этому моменту я измеряю коэффициент двухкратного ускорения на машине, на которой тестирую, что не удивительно, но, учитывая, что у нас есть принудительный последовательный шаг O (n), и куча более распараллеленных O (n)пошагово, по алгоритму последовательной сортировки, возможно, это не так разочаровывает.