Чудове запитання.
Ця багатопотокова реалізація функції Фібоначчі не швидша за однопоточну версію. Ця функція була показана лише в дописі в блозі як іграшковий приклад того, як працюють нові можливості нитки, підкреслюючи, що вона дозволяє нереститися безліччю ниток різних функцій, а планувальник визначить оптимальне навантаження.
Проблема полягає в тому, що @spawn
навколо є нетривіальний наклад 1µs
, тож якщо ви породили нитку, щоб виконати завдання, яке займає менше 1µs
, ви, ймовірно, зашкодили вашій продуктивності. Рекурсивне визначення fib(n)
має експоненціальну часову складність порядку 1.6180^n
[1], тож при виклику fib(43)
ви створюєте щось із 1.6180^43
ниток порядку . Якщо кожен з них потребує 1µs
нересту, знадобиться близько 16 хвилин, щоб нерестувати і запланувати необхідні теми, і це навіть не враховує час, який потрібно для проведення фактичних обчислень та повторного об'єднання / синхронізації потоків, що займає навіть більше часу.
Такі речі, коли ви породжуєте нитку для кожного кроку обчислення, мають сенс лише в тому випадку, якщо кожен етап обчислення займає тривалий час порівняно з @spawn
накладними.
Зауважте, що робота полягає в зменшенні витрат @spawn
, але, зважаючи на фізику багатоядерних силіконових мікросхем, я сумніваюся, що це може бути досить швидко для вищезазначеної fib
реалізації.
Якщо вам цікаво, як ми могли б змінити fib
функцію fib
потоку, щоб насправді вона була корисною, найпростіше зробити це лише породити нитку, якщо ми думаємо, що це зайнятиме значно більше часу, ніж 1µs
запуск. На моїй машині (працює на 16 фізичних ядрах) я потрапляю
function F(n)
if n < 2
return n
else
return F(n-1)+F(n-2)
end
end
julia> @btime F(23);
122.920 μs (0 allocations: 0 bytes)
тож добрі два порядки перевищують вартість нересту нитки. Це, здається, добре використовувати:
function fib(n::Int)
if n < 2
return n
elseif n > 23
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
else
return fib(n-1) + fib(n-2)
end
end
Тепер, якщо я дотримуюся належної методології орієнтиру з BenchmarkTools.jl [2], я знаходжу
julia> using BenchmarkTools
julia> @btime fib(43)
971.842 ms (1496518 allocations: 33.64 MiB)
433494437
julia> @btime F(43)
1.866 s (0 allocations: 0 bytes)
433494437
@Anush запитує в коментарях: Це коефіцієнт у 2 швидкості, використовуючи 16 ядер, здається. Чи можна наблизитись до коефіцієнта 16 швидкості?
Так. Проблема з вищевказаною функцією полягає в тому, що тіло функції більше, ніж у F
, з великою кількістю умовних умов, нересту функцій / ниток і все таке. Запрошую вас порівняти @code_llvm F(10)
@code_llvm fib(10)
. Це означає, що fib
Юлії набагато важче оптимізувати. Цей додатковий накладний обсяг дозволяє зробити світ різницею для невеликих n
випадків.
julia> @btime F(20);
28.844 μs (0 allocations: 0 bytes)
julia> @btime fib(20);
242.208 μs (20 allocations: 320 bytes)
О ні! весь той зайвий код, який ніколи не зачіпається n < 23
, сповільнює нас на порядок! Хоча є легке виправлення: коли n < 23
, не повторюйтесь fib
, замість цього зателефонуйте до однієї потоку F
.
function fib(n::Int)
if n > 23
t = @spawn fib(n - 2)
return fib(n - 1) + fetch(t)
else
return F(n)
end
end
julia> @btime fib(43)
138.876 ms (185594 allocations: 13.64 MiB)
433494437
що дає результат ближче до того, що ми очікували б для такої кількості ниток.
[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibach-program/
[2] @btime
Макрос BenchmarkTools від BenchmarkTools.jl буде виконувати функції кілька разів, пропускаючи час компіляції та середні результати.