Hi I am just starting to use Julia and need to do a small presentation about this topic. I read some introduction and watch tutorial about this language but still did not have a clue. Hope to have some advice here. Thanks.
This question is too broad without any context. Every language has its own sets of strengths and weaknesses. You may want to elaborate your use cases and this friendly community can help you rationalize whether Julia is a good fit or not.
Sorry about the confusing description. I am try to write a parser for the compressed data. I read this from the Julia home page “Julia programs are organized around multiple dispatch, which allows built-in and user-defined functions to be overloaded for different combinations of argument types.” I can not really understand this part. Thank you for your patient reply.
julia> f(x,y) = "Hi!"
f (generic function with 1 method)
julia> f(x::Int,y) = "Hello!"
f (generic function with 2 methods)
julia> f(x::Real,y::Integer...) = fill(x, y...)
f (generic function with 3 methods)
julia> f(y::Int,x::Real) = f(x,y)
f (generic function with 4 methods)
julia> f("Howdy", exp)
"Hi!"
julia> f(4, cos)
"Hello!"
julia> f(3, 2.7)
3-element Array{Float64,1}:
2.7
2.7
2.7
julia> f(2.7,4)
4-element Array{Float64,1}:
2.7
2.7
2.7
2.7
julia> f(9.2, 2, 6)
2×6 Array{Float64,2}:
9.2 9.2 9.2 9.2 9.2 9.2
9.2 9.2 9.2 9.2 9.2 9.2
This example looks a little silly, but multiple dispatch is extremely powerful.
It lets you make convenient APIs, by matching function names and operators to the ideas instead of the specific operations.
It also lets code be extremely fast. C++ and Fortran are fast because because the compiler gets to make optimized code for specific data types. By simply compiling a separate version of a function depending on input types, Julia gets those same benefits while remaining dynamic.
Take a look at this function:
julia> function g(x, y, z)
output = zero(promote_type(eltype.((x,y,z))...))
for yᵢ ∈ y
output += x*yᵢ + z
end
output
end
See what happens when we call it with two Float64
s and an Int32
:
julia> g(2.3, 1.2, Int32(3))
5.76
julia> @code_warntype g(2.3, 1.2, Int32(3))
Variables:
#self# <optimized out>
x::Float64
y::Float64
z::Int32
yᵢ::Float64
#temp#::Bool
output::Float64
Body:
begin
output::Float64 = (Base.sitofp)(Float64, 0)::Float64 # line 3:
#temp#::Bool = false
4:
unless (Base.not_int)(#temp#::Bool)::Bool goto 13
SSAValue(2) = y::Float64
yᵢ::Float64 = SSAValue(2)
#temp#::Bool = true # line 4:
output::Float64 = (Base.add_float)(output::Float64, (Base.add_float)((Base.mul_float)(x::Float64, yᵢ::Float64)::Float64, (Base.sitofp)(Float64, z::Int32)::Float64)::Float64)::Float64
11:
goto 4
13: # line 6:
return output::Float64
end::Float64
Julia also realizes things like the length of a Float64 is 1, so the for loop actually disappears::
@code_llvm g(2.3, 1.2, Int32(3))
define double @julia_g_63073(double, double, i32) #0 !dbg !5 {
top:
%3 = fmul double %0, %1
%4 = sitofp i32 %2 to double
%5 = fadd double %3, %4
%6 = fadd double %5, 0.000000e+00
ret double %6
}
This turns into almost the exact same llvm as if we were explicit about types for that exact combination, as we would be in Fortran:
julia> gcpp(x::Float64, y::Float64, z::Int32) = x*y + z
gcpp (generic function with 1 method)
julia> @code_llvm gcpp(2.3, 1.2, Int32(3))
define double @julia_gcpp_63288(double, double, i32) #0 !dbg !5 {
top:
%3 = fmul double %0, %1
%4 = sitofp i32 %2 to double
%5 = fadd double %3, %4
ret double %5
}
Which is almost the same code. We’re just left with an extra adding 0.0.
We can mix up the input types, and it will just keep creating optimized code for that specific version. Here, I’m calling g with an unsigned 32 bit integer z
, array of two Float32s y
, and the irrational pi
as z:
julia> g(Cuint(9), [1.2f0 -4.2f0], π)
-20.716812f0
julia> @code_warntype g(Cuint(9), [1.2f0 -4.2f0], π)
Variables:
#self# <optimized out>
x::UInt32
y::Array{Float32,2}
z <optimized out>
yᵢ::Float32
#temp#::Int64
output::Float32
Body:
begin
output::Float32 = (Base.sitofp)(Float32, 0)::Float32 # line 3:
#temp#::Int64 = 1
4:
unless (Base.not_int)((#temp#::Int64 === (Base.add_int)((Base.arraylen)(y::Array{Float32,2})::Int64, 1)::Int64)::Bool)::Bool goto 14
SSAValue(2) = (Base.arrayref)(y::Array{Float32,2}, #temp#::Int64)::Float32
SSAValue(3) = (Base.add_int)(#temp#::Int64, 1)::Int64
yᵢ::Float32 = SSAValue(2)
#temp#::Int64 = SSAValue(3) # line 4:
output::Float32 = (Base.add_float)(output::Float32, (Base.add_float)((Base.mul_float)((Base.uitofp)(Float32, x::UInt32)::Float32, yᵢ::Float32)::Float32, 3.1415927f0)::Float32)::Float32
12:
goto 4
14: # line 6:
return output::Float32
end::Float32
This is way easier than always explicitly listing types!
This really starts getting cool when you start considering tools like:
Isn’t adding 0.0 a little strange here?
This is an unfortunate combination of circumstances.
Julia is awesome for scientific computing! Java and Python, much less so. Compared to Python and Java, some of things you get in Julia are:
- Productivity in code development because of the easier syntax and because you don’t need to specify types of variables (Python offers that but not Java)
- Native C-like speed of programs because of type inference and specialized code compilation (Java offers native speed but not Python)
- Code will work for any input type that “makes sense” (thanks to multiple dispatch) unless you restrict it, so less coding actually gives you more features in Julia! This allows generic Julia functions to work for arbitrary precision inputs, forward differentiation, interval arithmetic among many other cool features.
- Mathematics-friendly syntax
- Straightforward parallel programming and GPU support
- Macros and generated functions, which give a ton of cool features such as: domain specific languages like JuMP, and fast static arrays using StaticArrays.jl.
- A nice package manager
- Oh and a really cool differential equations package!
I also find it really easy to optimize my code reducing memory allocations and using tricks like @inbounds
which give a nice easy boost to the speed of my programs. On the flip side, if you are using some tools from other languages, you may not find their equivalent yet in Julia (depends on the field) because the language is still young so the ecosystem is still developing, but in certain fields it is already somewhat mature. But on the double flip side, you have cool tools like ccall
for C and Fortran, PyCall.jl for Python, Cxx.jl for C++, RCall.jl for R, JavaCall.jl for Java, and MATLAB.jl which let you use your favourite tools from any of these languages in the middle of your Julia code! Mind-blowing right!?
That’s from my limited experience in Java, Python and Julia.
Yeah, I’m a little surprised that wasn’t optimized away. I thought LLVM may get rid of it later, but the adding zero version was actually slower (roughly 3ns vs 2ns).
Maybe wrapping the function body in @fastmath begin end
would help.
EDIT:
function gfm(x, y, z)
@fastmath begin
output = zero(promote_type(eltype.((x,y,z))...))
for yᵢ ∈ y
output += x*yᵢ + z
end
end
output
end
function g(x, y, z)
output = zero(promote_type(eltype.((x,y,z))...))
for yᵢ ∈ y
output += x*yᵢ + z
end
output
end
Which yields:
julia> gfm(2.3, 1.2, Int32(3))
5.76
julia> @code_llvm gfm(2.3, 1.2, Int32(3))
define double @julia_gfm_62867(double, double, i32) #0 !dbg !5 {
top:
%3 = sitofp i32 %2 to double
%4 = fmul fast double %1, %0
%5 = fadd fast double %3, %4
ret double %5
}
julia> @code_llvm g(2.3, 1.2, Int32(3))
define double @julia_g_62925(double, double, i32) #0 !dbg !5 {
top:
%3 = fmul double %0, %1
%4 = sitofp i32 %2 to double
%5 = fadd double %3, %4
%6 = fadd double %5, 0.000000e+00
ret double %6
}
Yielding almost the same code:
julia> gexplicit(x::Float64, y::Float64, z::Int32) = x*y + z
gexplicit (generic function with 1 method)
julia> @code_llvm gexplicit(2.3, 1.2, Int32(3))
define double @julia_gexplicit_63247(double, double, i32) #0 !dbg !5 {
top:
%3 = fmul double %0, %1
%4 = sitofp i32 %2 to double
%5 = fadd double %3, %4
ret double %5
}
Unfortunately – I suspect it’s because of the the fast double
operations – it is the slowest version of the three, despite being almost identical to the fastest:
julia> @benchmark gfm(2.3, 1.2, Int32(3))
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 2.781 ns (0.00% GC)
median time: 3.718 ns (0.00% GC)
mean time: 3.626 ns (0.00% GC)
maximum time: 23.280 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
julia>
julia> @benchmark g(2.3, 1.2, Int32(3))
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 3.210 ns (0.00% GC)
median time: 3.211 ns (0.00% GC)
mean time: 3.484 ns (0.00% GC)
maximum time: 19.007 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
julia> @benchmark gexplicit(2.3, 1.2, Int32(3))
BenchmarkTools.Trial:
memory estimate: 0 bytes
allocs estimate: 0
--------------
minimum time: 1.750 ns (0.00% GC)
median time: 2.142 ns (0.00% GC)
mean time: 2.223 ns (0.00% GC)
maximum time: 20.829 ns (0.00% GC)
--------------
samples: 10000
evals/sample: 1000
Can we get the more aggressive optimizations of @fastmath
, without the special “fast” functions?
Some meta compiler hint, perhaps?
https://docs.julialang.org/en/latest/devdocs/meta/
EDIT:
But I seem to have had a misunderstanding. Skimming over this:
https://github.com/JuliaLang/julia/blob/master/base/fastmath.jl
It doesn’t look like it’s actually doing anything other than substituting the expressions for the “fast” versions?