Category Archives: Julia

Construction vs conversion in Julia

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2021/03/26/conversion.html

Introduction

In the recent 0.22.6 release of the DataFrames.jl package we have
deprecated a bunch of convert methods for objects defined in it.

In this post I want to comment on when convert is used and when it is
appropriate to define a convert method for custom types.

The history

DataFrames.jl is an old package, with its 0.0.0 release made on Feb 14, 2013.

During this time the Julia language has evolved. Even in Julia 0.6 convert
method was a default fallback for construction as you can see here:

However, in some cases you could consider adding methods to Base.convert
instead of defining a constructor, because Julia falls back to calling
convert() if no matching constructor is found. For example, if no constructor
T(args...) = ... exists Base.convert(::Type{T}, args...) = ... is called.

This is a past of pre-1.0 Julia design (if you are interested in some more
history you can start with this issue). The things have changed now,
but until DataFrames.jl 0.22.6 release we had some convert methods that were
inspired by this rule, and we even had constructors that fell back to convert
explicitly.

So what is the current rule for using convert?

The present

Fortunately, as of Julia 1.6 the manual is quite clear that
convert should be only defined in cases when it is safe to perform a
conversion even when the user does not ask for it explicitly. Before we move
forward let us see it in action:

julia> struct Example
       a::Int
       b::Complex{Int}
       end

julia> Example(1.0, 1.0)
Example(1, 1 + 0im)

You can see that 1.0 (of type Float64) got implicitly converted to 1 (of
type Int). Similarly 1.0 got converted to 1 + 0im (Complex{Int}).
These conversions are performed implicitly to ensure programmer convenience.

However, the following fails:

julia> example(a::Int, b::Complex{Int}) = (a, b)
example (generic function with 1 method)

julia> example(1.0, 1.0)
ERROR: MethodError: no method matching example(::Float64, ::Float64)

So when does the implicit conversion happen? Again the maunal explains it here:

  • Assigning to an array converts to the array’s element type.
  • Assigning to a field of an object converts to the declared type of the field.
  • Constructing an object with new converts to the object’s declared field types.
  • Assigning to a variable with a declared type (e.g. local x::T) converts to that type.
  • A function with a declared return type converts its return value to that type.
  • Passing a value to ccall converts it to the corresponding argument type.

So a short, simplifying, rule is that the conversion happens when you want to make
an assignment to a value that has a pre-specified type.

Let us see it at work:

julia> d1 = Set{Int}()
Set{Int64}()

julia> push!(d1, 1.0)
Set{Int64} with 1 element:
  1

julia> d2 = BitSet()
BitSet([])

julia> push!(d2, 1.0)
ERROR: MethodError: no method matching push!(::BitSet, ::Float64)

You can push 1.0 to Set{Int}, because its push! method makes no
restriction on the type of value that is pushed to the Set{Int}. Then,
internally, Set{Int} converts the passed value to Int before storing it. You
can make sure that this happens by writing:

julia> push!(d1, "a")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
Closest candidates are:
  convert(::Type{T}, ::Ptr) where T<:Integer at pointer.jl:23
  convert(::Type{T}, ::T) where T<:Number at number.jl:6
  convert(::Type{T}, ::Number) where T<:Number at number.jl:7
  ...
Stacktrace:
 [1] setindex!(h::Dict{Int64, Nothing}, v0::Nothing, key0::String)
   @ Base ./dict.jl:374
 [2] push!(s::Set{Int64}, x::String)
   @ Base ./set.jl:57
 [3] top-level scope
   @ REPL[32]:1

and if you check the setindex! implementation you will see the conversion:

function setindex!(h::Dict{K,V}, v0, key0) where V where K
    key = convert(K, key0)
    if !isequal(key, key0)
        throw(ArgumentError("$(limitrepr(key0)) is not a valid key for type $K"))
    end
    setindex!(h, v0, key)
end

So why you are not allowed to push! a float to a BitSet? The reason is that
its push! method is defined more restrictively like this:

@inline push!(s::BitSet, n::Integer) = _setint!(s, _check_bitset_bounds(n), true)

and since no conversion happens when passing parameters to functions an error is
thrown.

Conclusion

To wrap up let us go back to the manual here:

since convert can be called implicitly, its methods are restricted to cases
that are considered “safe” or “unsurprising”. convert will only convert between
types that represent the same basic kind of thing (e.g. different
representations of numbers, or different string encodings). It is also usually
lossless; converting a value to a different type and back again should result in
the exact same value.

In essence this means that, unless you are developing a low level infrastructure
package (like defining new numeric type), you are not likely to need to define
convert methods at all. Just define proper constructors for your types.

In DataFrames.jl we left only three conversions.

The first is from SubDataFrame to DataFrame. It is clearly a lose-less
conversion that sometimes might be useful (e.g. you have a container of
DataFrame objects and want to be able to implicitly add SubDataFrame to it).

The second and third conversions are from DataFrameRow and GroupKey to
NamedTuple. The reason behind allowing them is that we try to make both these
types to work like NamedTuple (except for the fact that they are views).
Again in this case the conversion is lose-less.

As a final note — I think Julia 1.6 manual is really a great resource
(half-way of writing this post I considered to stop it as the manual
is so clear about the rules).

First Steps #1: Installing Julia

By: Josh Day

Re-posted from: https://www.juliafordatascience.com/first-steps-1-installing-julia/

💻 Installing Julia

First Steps #1: Installing Julia

The recommended method of installing Julia is through the official binaries available at https://julialang.org/downloads/.  Simply choose the proper link for your platform.

My Platform is…

🍎 macOS

  1. Double-click the .dmg file and drag "Julia-1.6" into "Applications".
  2. In the Applications folder, double-click "Julia-1.6" and voila 🎉!
  3. Optional: Add julia to your PATH environmental variable.  This means you can start Julia by typing julia in a terminal (like Terminal.app that ships with every Mac).  Copy and paste these two commands into your shell:
rm -f /usr/local/bin/julia

ln -s /Applications/Julia-1.6.app/Contents/Resources/julia/bin/julia /usr/local/bin/julia
Adding julia to macOS PATH

🪟 Windows

  1. Double-click the .exe file and follow the instructions.
  2. Find Julia via the start menu or double-click the Desktop shortcut (if you chose to add one during step 1) and voila 🎉!
  3. Optional: Add julia to your PATH environmental variable.  This means you can start Julia by typing julia in a terminal (it is recommended to use a modern terminal such as Windows Terminal from the Microsoft App Store.  The easiest method is to check the "Add Julia to PATH" option in the installer.  If you skipped that, the instructions slightly differ for Windows 7 and 8 vs. Windows 10.

🐧 Linux/FreeBSD

Odds are you expect less hand-holding 😃.  Click the above link ☝️.

🎁 Packages for Data Science

Installing Packages

Julia comes with an amazing package manager.  Pkg will be covered in a different post.  For now we'll just say that to add a package (StatsBase, for example), you'll use:

julia> using Pkg

julia> Pkg.add("StatsBase")
Installing StatsBase

Core Data Science Packages

There are many fantastic data science tools in Julia's package ecosystem.  Many of them will be covered in future posts.  For now, we recommend getting started with the following:

  • StatsBase: Basic Statistics for Julia.
  • DataFrames: In-memory tabular data in Julia.
  • Plots: Powerful convenience for Julia visualizations and data analysis.
  • Pluto: Simple reactive notebooks for Julia.

To install these packages, run the following in Julia:

using Pkg

for pkg in ["StatsBase", "DataFrames", "Plots", "Pluto"]
    Pkg.add(pkg)
end
Installing Core Data Science Packages

That's It!

You're ready to go 🚀.

Next up is First Steps #2: The REPL

What you can learn from accessing an element of an array in Julia

By: Mosè Giordano

Re-posted from: http://giordano.github.io/blog/2021-03-25-access-array/

This innocent question from a newcomer in the official Slack
workspace
of the Julia programming language

How can I extract the Float64 element from a 1×1 Array{Float64, 1}?

spurred a series of more or less serious answers:

  • use the only function: only(a)
  • get the element with index 1: a[1]
  • omit all indices: a[]
  • use the first function: first(a)
  • get the element with index 1, 1: a[1, 1]
  • use the last function: last(a)
  • get the element with index 1, 1, 1, 1, 1, …: a[ones(Int, 100)...] (the
    ... operator is called
    splatting)
  • get the last element with the special end index (end here is automatically
    lowered to lastindex(a)): a[end]
  • get the element in the first row and last column: a[begin, end]
  • reduce the array with the
    sum function: reduce(sum, a)
  • get the element with index floor(Int, -real(exp(π * im))): a[floor(Int, -real(exp(π * im)))], or the variant x[floor(Int, -real(cispi(1)))]
  • dereference the pointer
    of a
    (remember?):
    unsafe_load(pointer(a))
  • use the StarWarsArrays.jl
    package and get the fourth element (mentioned in the post about random
    indexing
    ):
    StarWarsArray(a)[4]
  • use the
    getindex
    function (a[n, ...] is a syntactic sugar for getindex(a, n, ...)):
    getindex(a, firstindex(a))
  • get a random element from the array: rand(a)

Benchmarking

We can compare the performance of all these versions. To do this, we can use
the BenchmarkTools.jl package
(benchmarks kindly provided by @Seelengrab):

using BenchmarkTools # to install it: ]add BenchmarkTools
using StarWarsArrays # to install it: ]add https://github.com/giordano/StarWarsArrays.jl
using Random # it's a standard library

only_bench(x)         = only(x)
one_idx_bench(x)      = x[1]
omit_bench(x)         = x[]
first_bench(x)        = first(x)
two_ones_idx_bench(x) = x[1, 1]
splat_bench(x)        = x[ones(Int, 100)...]
end_bench(x)          = x[end]
begin_end_bench(x)    = x[begin, end]
reduce_sum_bench(x)   = reduce(sum, x)
floor_bench(x)        = x[floor(Int, -real(exp(π * im)))]
cispi_bench(x)        = x[floor(Int, -real(cispi(1)))]
dereference_bench(x)  = unsafe_load(pointer(x))
getindex_bench(x)     = getindex(x, firstindex(x))
starwars_bench(x)     = StarWarsArray(x)[4] # ಠ_ಠ
rand_bench(x)         = rand(x)

for f in [
    only_bench,
    one_idx_bench,
    omit_bench,
    first_bench,
    two_ones_idx_bench,
    splat_bench,
    end_bench,
    begin_end_bench,
    reduce_sum_bench,
    floor_bench,
    cispi_bench,
    dereference_bench,
    getindex_bench,
    starwars_bench,
    rand_bench,
]
    println(f, ':')
    @btime $f(x) setup=(x=rand(1,1))
end

On my computer I get

only_bench:
  2.800 ns (0 allocations: 0 bytes)
one_idx_bench:
  1.631 ns (0 allocations: 0 bytes)
omit_bench:
  2.796 ns (0 allocations: 0 bytes)
first_bench:
  1.630 ns (0 allocations: 0 bytes)
two_ones_idx_bench:
  1.685 ns (0 allocations: 0 bytes)
splat_bench:
  4.524 μs (5 allocations: 1.75 KiB)
end_bench:
  1.378 ns (0 allocations: 0 bytes)
begin_end_bench:
  1.644 ns (0 allocations: 0 bytes)
reduce_sum_bench:
  1.901 ns (0 allocations: 0 bytes)
floor_bench:
  1.630 ns (0 allocations: 0 bytes)
cispi_bench:
  1.631 ns (0 allocations: 0 bytes)
dereference_bench:
  1.369 ns (0 allocations: 0 bytes)
getindex_bench:
  1.371 ns (0 allocations: 0 bytes)
starwars_bench:
  1.371 ns (0 allocations: 0 bytes)
rand_bench:
  9.599 ns (0 allocations: 0 bytes)

Unsurprisingly, the fastest solution is dereferencing a pointer, which compiles
down to the following code

julia> x = rand(1,1);

julia> @code_native debuginfo=:none dereference_bench(x)
        .text
        movq    (%rdi), %rax
        vmovsd  (%rax), %xmm0                   # xmm0 = mem[0],zero
        retq
        nopl    (%rax,%rax)

This is pretty much what you’d expect from the corresponding C
code
.

What’s remarkable is that the
StarWarsArrays.jl package
achieves about the same performance: accessing an element of a custom data
structure using a funny custom indexing, and written without having performance
in mind, is just as efficient as dereferencing a pointer.

However, subnanosecond differences aren’t credible: most functions that clocked
in the range 1.3-1.6 nanoseconds have basically the same performance. If you
run the benchmarks again you’ll find that their performance get more or less
close to that of dereferencing. The difference with dereferencing is that all
other functions do bounds
checking
, which on average lead
to slightly larger runtime. However, in high-performance computing you usually
want to make sure to be within bounds at a higher level and skip bounds checks
when accessing individual elements. If you’re 100% sure that the element of the
array you want to access is within its bounds, you can use the macro
@inbounds to
skip runtime bounds checks.

To see the performance when skipping bounds checks in the above functions, you
can either use @inbounds in their definitions (e.g., one_idx_bench(x) =
@inbounds x[1]
, etc…), or start Julia with the --check-bounds=no flag (this
flag is not recommended for general use, as it will skip all bounds checks,
not only where you know it’s safe to do so). With --check-bounds=no I get

only_bench:
  2.725 ns (0 allocations: 0 bytes)
one_idx_bench:
  1.369 ns (0 allocations: 0 bytes)
omit_bench:
  1.369 ns (0 allocations: 0 bytes)
first_bench:
  1.369 ns (0 allocations: 0 bytes)
two_ones_idx_bench:
  1.369 ns (0 allocations: 0 bytes)
splat_bench:
  4.510 μs (5 allocations: 1.75 KiB)
end_bench:
  1.369 ns (0 allocations: 0 bytes)
begin_end_bench:
  1.630 ns (0 allocations: 0 bytes)
reduce_sum_bench:
  1.960 ns (0 allocations: 0 bytes)
floor_bench:
  1.408 ns (0 allocations: 0 bytes)
cispi_bench:
  1.369 ns (0 allocations: 0 bytes)
dereference_bench:
  1.369 ns (0 allocations: 0 bytes)
getindex_bench:
  1.368 ns (0 allocations: 0 bytes)
starwars_bench:
  1.369 ns (0 allocations: 0 bytes)
rand_bench:
  9.606 ns (0 allocations: 0 bytes)

As expected, most functions now more consistently perform as well as
dereferencing. This is confirmed by the fact they are all compiled down to the
same efficient code, even the most bizarre ones like floor_bench and
starwars_bench:

julia> @code_native debuginfo=:none omit_bench(x)
        .text
        movq    (%rdi), %rax
        vmovsd  (%rax), %xmm0                   # xmm0 = mem[0],zero
        retq
        nopl    (%rax,%rax)

julia> @code_native debuginfo=:none floor_bench(x)
        .text
        movq    (%rdi), %rax
        vmovsd  (%rax), %xmm0                   # xmm0 = mem[0],zero
        retq
        nopl    (%rax,%rax)

julia> @code_native debuginfo=:none getindex_bench(x)
        .text
        movq    (%rdi), %rax
        vmovsd  (%rax), %xmm0                   # xmm0 = mem[0],zero
        retq
        nopl    (%rax,%rax)

julia> @code_native debuginfo=:none starwars_bench(x)
        .text
        movq    (%rdi), %rax
        vmovsd  (%rax), %xmm0                   # xmm0 = mem[0],zero
        retq
        nopl    (%rax,%rax)

Lessons learned

  • To seriously answer the question that led to this digression, I think the
    first answers are all good ways to access the only element of a 1×1 array:
    only(a) (which however always does a runtime check to make sure that’s the
    only element of the array, incurring a performance hit), a[1], a[begin]
    a[1, 1], a[], first(a)
  • Custom Julia data types are efficient! The fact that a custom data type with
    custom indexing like
    StarWarsArray has basically
    the same performance as dereferencing a pointer (exactly same native code
    when skipping all bounds checks) is a testament to the power and expresiveness
    of the language: you can write your code in a high-level form, and have it
    compiled down to very efficient code
  • Splatting a long array can be bad for performance, both at compile- and
    run-time. Splatting is a convenient syntax, but use it with great care,
    especially when you have many elements
  • Reduction has relatively little overhead
  • rand(a) performed quite bad, but actually most of the time is spent in
    accessing the global default random number generator (RNG): if you care about
    performance (and reproducibility, too) you should always pass in the RNG as
    argument (see also The need for rand
    speed
    by Bogumił
    Kamiński):

      julia> @btime rand(x) setup=(x=rand(1,1))
      9.596 ns (0 allocations: 0 bytes)
    0.08218288442687438
    
    julia> @btime rand($(Random.default_rng()), x) setup=(x=rand(1,1))
      4.770 ns (0 allocations: 0 bytes)
    0.8112313537560083
    

    In the second benchmark we explicitly passed the default RNG to rand and can
    see that avoiding accessing the global RNG saves more than 50% of runtime.
    Performance is still far from ideal though. As an aside, drawing a random
    element from a short tuple (<= 4 elements) is optimised, and we can get again
    the same performance as derefercing a pointer for a 1-element tuple:

    julia> @btime rand($(Random.default_rng()), Ref(x)[]) setup=(x=(rand(),))
      1.369 ns (0 allocations: 0 bytes)
    0.4308805082102649
    
    julia> @code_native debuginfo=:none rand(Random.default_rng(), (rand(),))
            .text
            vmovsd  (%rsi), %xmm0                   # xmm0 = mem[0],zero
            retq
            nopw    %cs:(%rax,%rax)