Gran pregunta
Esta implementación multiproceso de la función Fibonacci no es más rápida que la versión de un solo subproceso. Esa función solo se mostró en la publicación del blog como un ejemplo de juguete de cómo funcionan las nuevas capacidades de subprocesamiento, destacando que permite generar muchos subprocesos en diferentes funciones y el programador descubrirá una carga de trabajo óptima.
El problema es que @spawn
tiene una sobrecarga no trivial de alrededor 1µs
, por lo que si genera un hilo para realizar una tarea que requiere menos 1µs
, probablemente haya perjudicado su rendimiento. La definición recursiva de fib(n)
tiene una complejidad de orden exponencial en el tiempo 1.6180^n
[1], por lo que cuando llama fib(43)
, genera algunos 1.6180^43
hilos de orden . Si cada uno toma 1µs
engendrar, tomará alrededor de 16 minutos solo para generar y programar los hilos necesarios, y eso ni siquiera cuenta el tiempo que lleva hacer los cálculos reales y volver a fusionar / sincronizar hilos que lleva incluso más tiempo.
Cosas como esta donde genera un hilo para cada paso de un cálculo solo tiene sentido si cada paso del cálculo lleva mucho tiempo en comparación con la @spawn
sobrecarga.
Tenga en cuenta que se está trabajando para reducir la sobrecarga de @spawn
, pero por la propia física de los chips de silicona multinúcleo, dudo que alguna vez pueda ser lo suficientemente rápido para la fib
implementación anterior .
Si tiene curiosidad acerca de cómo podríamos modificar la fib
función de subprocesos para que sea realmente beneficiosa, lo más fácil sería generar un fib
subproceso si creemos que tomará mucho más tiempo que 1µs
ejecutarlo. En mi máquina (que se ejecuta en 16 núcleos físicos), obtengo
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)
Eso es un buen dos órdenes de magnitud sobre el costo de generar un hilo. Parece un buen límite para usar:
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
ahora, si sigo la metodología de referencia adecuada con BenchmarkTools.jl [2] encuentro
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 pregunta en los comentarios: Parece que este es un factor de 2 velocidades con 16 núcleos. ¿Es posible acercar algo a un factor de 16 de velocidad?
Sí lo es. El problema con la función anterior es que el cuerpo de la función es más grande que el de F
, con muchos condicionales, función / generación de subprocesos y todo eso. Te invito a comparar @code_llvm F(10)
@code_llvm fib(10)
. Esto significa que fib
es mucho más difícil para julia optimizar. Esta sobrecarga adicional hace una gran diferencia para los n
casos pequeños .
julia> @btime F(20);
28.844 μs (0 allocations: 0 bytes)
julia> @btime fib(20);
242.208 μs (20 allocations: 320 bytes)
¡Oh no! ¡todo ese código extra que nunca se toca n < 23
es ralentizarnos en un orden de magnitud! Sin embargo, hay una solución fácil: cuando n < 23
no recurras a fib
, en lugar de eso, llama al subproceso único 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
lo que da un resultado más cercano a lo que esperaríamos para tantos hilos.
[1] https://www.geeksforgeeks.org/time-complexity-recursive-fibonacci-program/
[2] La @btime
macro BenchmarkTools de BenchmarkTools.jl ejecutará funciones varias veces, omitiendo el tiempo de compilación y los resultados promedio.