Я экспериментирую с параллельными формами схемы Guile, и у меня есть следующий код:
(use-modules (srfi srfi-1)
(ice-9 pretty-print)
(ice-9 receive))
(define (busy-work limit)
(if (> limit 0)
(begin (sqrt (+ (expt limit limit) 1))
(busy-work (- limit 1)))
'done))
(define (busy-work-2 lst)
(cond [(null? lst) 'done]
[else
(expt (car lst) (car lst))
(busy-work-2 (cdr lst))]))
(define (time thunk)
(define starting-time (current-time))
(define res (thunk))
(define ending-time (current-time))
(display "elapsed time: ")
(display (- ending-time starting-time))
(display "s")
(newline)
res)
(define (partition-4 numbers)
(define (loop numbers rc0 rc1 rc2 rc3)
(cond [(null? numbers) (list (reverse rc0)
(reverse rc1)
(reverse rc2)
(reverse rc3))]
[else
(let* ([number (car numbers)]
[residue (remainder number 4)])
(cond [(= residue 0) (loop (cdr numbers)
(cons number rc0)
rc1
rc2
rc3)]
[(= residue 1) (loop (cdr numbers)
rc0
(cons number rc1)
rc2
rc3)]
[(= residue 2) (loop (cdr numbers)
rc0
rc1
(cons number rc2)
rc3)]
[(= residue 3) (loop (cdr numbers)
rc0
rc1
rc2
(cons number rc3))]))]))
(loop numbers '() '() '() '()))
(или в моем экспериментальном хранилище на https://github.com/ZelphirKaltstahl/guile-scheme-tutorials/blob/5321470f8f3cbbdb7f64d4ed60e4b1eaf8d8f444/parallellism/utils.scm)
2 процедуры busy-work
и busy-work-2
- это чисто сжатые числа со списками чисел, где, насколько я знаю, ни один расчет не зависит от другого. Я знаю, что измерение времени может быть не совсем точным.
Тем не менее, последовательно я не получаю ожидаемое ускорение от использования большего количества потоков (ядер, как я могу видеть в использовании ядра в моем индикаторе CPU).
Вот несколько примеров, из которых я ожидаю, что 2 потока будут в два раза быстрее выполняться с задачей, а 1 ядро и 4 ядра - в два раза быстрее, чем 2 ядра. Ну, более или менее, по крайней мере, потому что я делю списки таким образом, чтобы это работало более или менее равномерно.
Использование 4 ядер и parallel
(let ([residue-classes (partition-4 (iota 30000))])
(time
(lambda ()
(parallel (busy-work-2 (car residue-classes))
(busy-work-2 (cadr residue-classes))
(busy-work-2 (caddr residue-classes))
(busy-work-2 (cadddr residue-classes))))))
Это заканчивается примерно за 10 секунд на моей машине. Иногда 9 с, иногда 10 с.
Использование par-map
, которое использует 4 потока (ядра)
(let ([residue-classes (partition-4 (iota 30000))])
(time
(lambda ()
(par-map busy-work-2
residue-classes))))
Это заканчивается примерно за 10 секунд на моей машине. Иногда 9 с, иногда 10 с. Так же, как с parallel
.
Использование n-par-map
с 4-мя потоками (на моей машине)
(let ([residue-classes (partition-4 (iota 30000))])
(time
(lambda ()
(n-par-map (current-processor-count)
busy-work-2
residue-classes))))
Также 10 с. Вот в руководстве (https://www.gnu.org/software/guile/manual/html_node/Parallel-Forms.html) написано:
В отличие от вышеперечисленных, функции, описанные ниже, принимают в качестве аргумента несколько потоков. Это делает их по сути непереносимыми, поскольку указанное количество потоков может отличаться от количества доступных ядер ЦП, возвращаемого счетчиком текущего процессора (см. Процессы). Кроме того, эти функции создают указанное количество потоков при их вызове и завершают их после завершения, что делает их довольно дорогими.
Поэтому их следует избегать.
Хотя я считаю, что это объяснение не имеет смысла на 100% (почему бы n-par-map
не использовать те же предварительно созданные потоки, как parallel
, если их достаточно? Как и 4
, как на моем машина?), я не вижу никаких больших накладных расходов, и снова я вижу, что она заканчивается примерно в 10 с. Я предполагаю, что создание потока занимает так мало времени, что его просто не замечают по сравнению со всеми вычислениями при обработке чисел.
Использование n-par-map
с двумя потоками (ядрами)
(let ([residue-classes (partition-4 (iota 30000))])
(time
(lambda ()
(n-par-map 2
busy-work-2
residue-classes))))
Ожидание: может закончиться в 20-х годах.
Результат: заканчивается за 12 секунд.
Теперь, конечно, я думаю: «Ну, должно быть, в сериях с четырьмя ядрами должны быть большие издержки!».
Вопрос: Но откуда взялись эти издержки, когда я делаю чисто числовое вычисление без взаимозависимости каких-либо результатов? Использует ли он некоторую общую память, чтобы доступ к памяти стал узким местом?