Mathematica added a built-in Julia interface in version 12 last year. I had a try and found the interface quite convenient. Here’s a short example, using Julia to speed up calculation of Fibonacci numbers, using an inefficient recursive algorithm just for illustration.
First, here’s the pure Mathematica implementation for calculating the 30th Fibonacci number,
juliaSession = StartExternalSession["Julia"]; (* Start Julia session *)
fibJulia = ExternalEvaluate[juliaSession,
"fib(n) = n<=1 ? n : fib(n-2) + fib(n-1)"
];
(* defined Julia function callable from Mathematica *)
fibJulia[5]; (* JIT warmup for Julia *)
AbsoluteTiming[fibJulia[30]] (* Actual calculation *)
The output is
{0.00839, 832040}
Only 8 milliseconds was used (including FFI latency), more than 300 times faster than the pure Mathematica version.
Finally, the Julia session is closed by
DeleteObject[juliaSession]
(Julia packages ZMQ and JSON are required for the interface to work, as stated in the Wolfram documentation.)
fib(::Val{N}) where {N} = N <= 1 ? N : fib(Val(N-2)) + fib(Val(N-1))
Is a generic method for the value typeN. I.e. each value of N will get its specific compiled version to dispatch to. The compiler can probably determine the output for each value of N staticly at which point you’re “abusing” the multiple dispatch as a lookup table for allready computed values (i.e. memoization).
PS: But that doesn’t explain the fast first execution
fib(N) is slow because we have an exponential number of function calls.
It calls fib(N-2) twice (fib(3)), fib(N-3) three (fib(4)) times, fib(N-4) five (fib(5)) times, fib(N-5) eight times, …, fib(1) a total of fib(N + 1 - 1) times.
If we make N big enough that just evaluating fib normally takes much longer than compiling, e.g. 47 below:
julia> fib(::Val{N}) where {N} = N <= 1 ? N : fib(Val(N-2)) + fib(Val(N-1))
fib (generic function with 1 method)
julia> fib(N::Integer) = fib(Val(N))
fib (generic function with 2 methods)
julia> fibint(n) = n <= 1 ? n : fibint(n-2) + fibint(n-1)
fibint (generic function with 1 method)
julia> @time @eval fib(47)
0.020000 seconds (188.22 k allocations: 13.272 MiB, 111.50% compilation time)
2971215073
julia> @time fibint(47)
13.292764 seconds
2971215073
julia> @code_typed fib(Val(47))
CodeInfo(
1 ─ goto #3 if not false
2 ─ nothing::Nothing
3 ┄ return 2971215073
) => Int64
then the fact that the Val version only calls each fib once makes it much faster to compile.
Or, put another way, the first time calling fib(47) is much faster with the Val version, because instead of calling fib(30) a total of fib(18) == 2584 times, the Val version only has to call fib(30) once (and that fib(30) is itself faster the first time because it only has to call fib(12) once, etc).