Videos related to this segment (click the title to watch)
01 JULIA1 - 6A: Metaprogramming and macros (23.0:46.0)
01 JULIA1 - 6B: Interoperability with other languages (23.0:6.0)
01 JULIA1 - 6C: Performances and errors: profiling, debugging, introspection and exceptions (27.0:33.0)
01 JULIA1 - 6D: Parallel computation: multithreading, multiprocessing (20.0:3.0)

0106 Further Topics

Some stuff to set-up the environment..

julia> cd(@__DIR__)
julia> using Pkg
julia> Pkg.activate(".") Activating project at `~/work/SPMLJ/SPMLJ/buildedDoc/01_-_JULIA1_-_Basic_Julia_programming`
julia> # If using a Julia version different than 1.10 please uncomment and run the following line (reproducibility guarantee will however be lost) # Pkg.resolve() # Pkg.instantiate() # run this if you didn't in Segment 01.01 using Random
julia> Random.seed!(123)Random.TaskLocalRNG()
julia> using InteractiveUtils # loaded automatically when working... interactively

Metaprogramming and macros

"running" some code includes the following passages (roughly):

  • parsing of the text defining the code and its translation in hierarchical expressions to the Abstract syntax Tree (AST) (syntax errors are caught at this time)
  • on the first instance required ("just in time") compilation of the AST expressions into object code (using the LLVM compiler)
  • execution of the compiled object code

"Macros" in many other languages (e.g. C or C++) refer to the possibility to "pre-process" the textual representation of the code statements before it is parsed. In Julia instead it refers to the possibility to alter the expression once has already been parsed in the AST, allowing a greater expressivity as we are no longer limited by the parsing syntax

The AST is organised in a hierarchical tree of expressions where each element (including the operators) is a symbol For variables, you can use symbols to refer to the actual identifiers instead of the variable's value

Expressions themselves are objects representing unevaluated computer expressions

Expressions and symbols

julia> expr1 = Meta.parse("a = b + 2") # What the parser does when reading the source code. b doesn't need to actually be defined, it's just a namebinding without the reference to any object, not even `nothing`:(a = b + 2)
julia> typeof(expr1) # expressions are first class objectsExpr
julia> expr2 = :(a = b + 1):(a = b + 1)
julia> expr3 = quote a = b + 1 endquote #= REPL[4]:1 =# a = b + 1 end
julia> expr3quote #= REPL[4]:1 =# a = b + 1 end
julia> dump(expr1) # The AST ! Note this is already a nested statement, an assignment of the result of an expression (the sum call between the symbol `:b` and 1) to the symbol `a`Expr head: Symbol = args: Array{Any}((2,)) 1: Symbol a 2: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol + 2: Symbol b 3: Int64 2
julia> expr4 = Expr(:(=),:a,Expr(:call,:+,:b,1)) # The AST using the "Expr" constructor:(a = b + 1)
julia> symbol1 = :(a) # as for expressions:a
julia> symbol2 = Meta.parse("a") # as for expressions:a
julia> symbol3 = Symbol("a") # specific for symbols only:a
julia> othsymbol = Symbol("aaa",10,"bbb"):aaa10bbb
julia> typeof(symbol1)Symbol

I can access any parts of my expression before evaluating it (indeed, that's what macro will do...)

julia> myASymbol = expr1.args[2].args[1]:+
julia> expr1.args[2].args[1] = :(*):*
julia> b = 22
julia> # a # error, a not defined eval(expr1)4
julia> a # here now is defined and it has an object associated... 4!4
Danger

The capability to evaluate expressions is very powerful but due to obvious security implications never evaluate expressions you aren't sure of their provenience. For example if you develop a Julia web app (e.g. using Genie.jl) never evaluate user provided expressions.

Note that evaluation of expressions happens always at global scope, even if it is done inside a function:

julia> function foo()
           locVar = 1
           expr = :(locVar + 1)
           return eval(expr)
       endfoo (generic function with 1 method)

To refer to the value of a variable rather than the identifier itself within an expression, interpolate the variable using the dollar sign:

julia> expr = :($a + b) # here the identifier 'a' has been replaced with its numerical value, `4`:(4 + b)
julia> dump(expr)Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol + 2: Int64 4 3: Symbol b
julia> eval(expr)6
julia> a = 1010
julia> eval(expr) # no changes6
julia> b = 100100
julia> eval(expr) # here it changes, as it is at eval time that the identifier `b` is "replaced" with its value104

Macros

One of the best usage of macros is they allow package developers to provide a very flexible API to their package, suited for the specific needs of the package, making life easier for the users. Compare for example the API of JuMP with those of Pyomo to define model constraints !

Some examples of macros: MultiDimEquations.jl

  • from: @meq par1[d1 in DIM1, d2 in DIM2, dfix3] = par2[d1,d2]+par3[d1,d2]
  • to: [par1[d1,d2,dfix3] = par2[d1,d2]+par3[d1,d2] for d1 in DIM1, d2 in DIM2]

Pipe.jl:

  • from: @pipe 10 |> foo(_,a) |> foo2(b,_,c) |> foo3(_)
  • to: foo3(foo2(b,foo(10,a),c))

Broadcasting (Base):

  • from: @. a + b * D^2
  • to: a .+ b .* D.^2

Defining a macro...

Like functions, but both the arguments and the returned output are expressions

julia> macro customLoop(controlExpr,workExpr)
           return quote
             for i in $controlExpr
               $workExpr
             end
           end
       end@customLoop (macro with 1 method)

Invoking a macro....

julia> a = 55
julia> @customLoop 1:4 println(i) #note that "i" is in the macro1 2 3 4
julia> @customLoop 1:a println(i)1 2 3 4 5
julia> @customLoop 1:a if i > 3 println(i) end4 5
julia> @customLoop ["apple", "orange", "banana"] println(i)apple orange banana
julia> @customLoop ["apple", "orange", "banana"] begin print("i: "); println(i) endi: apple i: orange i: banana
julia> @macroexpand @customLoop 1:4 println(i) # print what the macro does with the specific expressions providedquote #= REPL[1]:3 =# for var"#559#i" = 1:4 #= REPL[1]:4 =# Main.var"Main".println(var"#559#i") #= REPL[1]:5 =# end end

String macros (aka "non-standard string literals") Invoked with the syntax xxx" ...text..." or xxx""" ...multi-line text...""" where xxx is the name of the macro and the macro must be defined as macro xxx_str. Used to perform textual modification on the given text, for example this prints the given text on a 8 characters

julia> macro print8_str(mystr)                                 # input here is a string, not an expression
           limits = collect(1:8:length(mystr))
           for (i,j) in enumerate(limits)
             st = j
             en = i==length(limits) ? length(mystr) : j+7
             println(mystr[st:en])
           end
       end@print8_str (macro with 1 method)
julia> print8"123456789012345678"12345678 90123456 78
julia> print8"""This is a text that once printed in 8 columns with terminal will be several lines. Ok, no grammar rules relating to carriage returns are employed here..."""This is a text t hat once printed in 8 co lumns wi th termi nal will be seve ral line s. Ok, n o gramma r rules relating to carr iage ret urns are employe d here.. .

While normally used to modify text, string macros are "true" macros:

julia> macro customLoop_str(str)
           exprs = Meta.parse(str)
           controlExpr,workExpr = exprs.args[1],exprs.args[2]
           return quote
             for i in $controlExpr
               $workExpr
             end
           end
       end@customLoop_str (macro with 1 method)
julia> customLoop"""1:4; println(i)"""1 2 3 4

Interfacing with other languages

There are 3 ways to interface Julia with programs or libraries written in other languages. At the lowest level, Julia allows to directly interface with C or Fortran libraries, and this means, aside using directly libraries written in C, to be able to interface with any programming language that offers also a C interface (R, Python...) Using this low level C Interface, users have created specific packages to interface many languages using a simple, Julian-way syntax. We will see these interfaces for R and Python. Finally, at the highest level, many common packages of other languages have been already "interfaced", so that the user can use the Julia Package without even knowing that this is an interface for another package, for example SymPy.jl is a large interface to the Python package SymPy.

Using C libraries

Let's start by seeing how to use a C library. For this example to work you will need to have the GCC compiler installed on your machine First let's write the header and source C files and write them to the disk:

julia> cheader = """
       extern int get5();
       extern double mySum(float x, float y);
       """"extern int get5();\nextern double mySum(float x, float y);\n"
julia> csource = """ int get5(){ return 5; } double mySum(float x, float y){ return x+y; } """"int get5(){\n return 5;\n}\n\ndouble mySum(float x, float y){\n return x+y;\n}\n"
julia> open(f->write(f,cheader),"myclib.h","w") # We open a stream to file with the "w" parameter as for "writing", and we pass the stream to the anonymous function to actually write to the stream. If this function is many lines of code, consider rewriting the `open` statement using a `do` block58
julia> open(f->write(f,csource),"myclib.c","w")79

Now let's run the command to compile the C code we saved as shared library using gcc, a C compiler. The following example assumes that GCC is installed in the machine where this example is run and available as gcc.

julia> compilationCommand1 = `gcc -o myclib.o -c myclib.c` # the actual compilation, note the backticks used to define a command`gcc -o myclib.o -c myclib.c`
julia> compilationCommand2 = `gcc -shared -o libmyclib.so myclib.o -lm -fPIC` # the linking into a shared library`gcc -shared -o libmyclib.so myclib.o -lm -fPIC`
julia> run(compilationCommand1)Process(`gcc -o myclib.o -c myclib.c`, ProcessExited(0))
julia> run(compilationCommand2)Process(`gcc -shared -o libmyclib.so myclib.o -lm -fPIC`, ProcessExited(0))

This should have created the C library libmyclib.so on disk. Let's gonna use it:

julia> const myclib = joinpath(@__DIR__, "libmyclib.so")  # we need the full path"/home/runner/work/SPMLJ/SPMLJ/buildedDoc/01_-_JULIA1_-_Basic_Julia_programming/libmyclib.so"

ccall arguments:

  1. A tuple with the function name to call and the library path. For both, if embedded in a variable, the variable must be set constant.
  2. The Julia type that maps to the C type returned by the function.
    • int → Int32 or Int64 (or the easy-to remember Cint alias)
    • float → Float32 (or the Cfloat alias)
    • double → Float64 (or the Cdouble alias)
  3. A tuple with the Julia types of the parameters passed to the C function
  4. Any other arguments are the values of the parameter passed
julia> a = ccall((:get5,myclib), Int32, ())5
julia> b = ccall((:mySum,myclib), Float64, (Float32,Float32), 2.5, 1.5)4.0

More details on calling C or Fortran code can be obtained in the official Julia documentation.

Using Python in Julia

The "default" way to use Python code in Julia is through the PythonCall.jl package. It automatically takes care of converting between Python types (including numpy arrays) and Julia types (types that can not be converted automatically are converted to the generic PyObject type). By default PythonCall will download and use a "private to Julia" (conda based) version of Python. Use the (session specific) environmental variables ENV["JULIA_CONDAPKG_BACKEND"] = "Null" and ENV["JULIA_PYTHONCALL_EXE"] = "/path/to//your/python" if you want to reuse a version already installed on your system. Python packages can be installed and managed in this private environment with the help of the CondaPkg.jl package.

julia> # using Pkg
       # Pkg.add("PythonCall")
       # Pkg.build("PythonCall")
       using PythonCall, CondaPkg, Pipe

Embed short python snippets in Julia

Use @pyexec for defining functions and running multi-line blocks, and @pyeval for evaluating expressions and calling functions.

julia> @pyexec """
       def python_sum(i, j):
           return i+j
       """ => python_sumPython: <function python_sum at 0x7fb1bff9a8e0>
julia> @pyexec """ def get_ith_element(n): a = [0,1,2,3,4,5,6,7,8,9] return a[n] """ => get_ith_elementPython: <function get_ith_element at 0x7fb1bff9ab60>
julia> @pyexec """ def get_nth_element(vec,n): return vec[n] """ => get_nth_elementPython: <function get_nth_element at 0x7fb1bff9ac00>

You can now call these functions:

julia> c = @pipe python_sum(3,4)         |> pyconvert(Int64,_)         # 77
julia> d = @pipe python_sum([3,4],[5,6]) |> pyconvert(Vector{Int64},_) # [8,10]2-element Vector{Int64}: 8 10
julia> e = @pipe get_ith_element(3) |> pyconvert(Int64,_) # 3 attention to the different convention for starting arrays!3

Note that while the input is automatically converted, the output (Python to Julia) still requires a manual conversion.

Alternatively, you can read a python script:

julia> pythonCode = """
       def sum_my_args (i, j):
         return i+j
       """"def sum_my_args (i, j):\n  return i+j\n"
julia> open(f->write(f,pythonCode),"python_script.py","w")37

or:

julia> sys = pyimport("sys"); sys.path.insert(0, pwd())Python: None
julia> ps = pyimport("python_script")Python: <module 'python_script' from '/home/runner/work/SPMLJ/SPMLJ/lessonsSources/01_-_JULIA1_-_Basic_Julia_programming/python_script.py'>
julia> @pipe ps.sum_my_args(3, 4) |> pyconvert(Int64,_)7

Use Python libraries

TODO: again, something that works but it doesn't work in our build system because of a separate module

Add a package to the local Python installation using Conda:

CondaPkg.add("pandas") # run this only once, it creates a CondaPkg.toml configuration file

const pd = pyimport("pandas") # Equiv. of Python import pandas as pd

Build a DataFrame from a Python dict of Python lists

peoplenames = ["Alice", "Bob", "Carol"] peopleages = [30,25,35] df = pd.DataFrame(pydict(name = peoplenames,age = peopleages))

df.loc[1, "age"] = 26 # row label 1 is "Bob", not "Alice" (0-based default index) df["agenextyear"] = df["age"] + 1 meanage = @pipe df["age"].mean() |> pyconvert(Float64,)

df.tocsv("people.csv", index = false) # Julia keyword args and false map to Python kwargs and False df2 = pd.readcsv("people.csv") println(df2)

Using Julia in Python

Installation of the Python package JuliaCall

JuliaCall can be installed using pip: $ python3 -m pip install --user juliacall

Running Julia libraries and code in Python

TODO: update this section to JuliaCall. The following is the old PyJulia approach

We can now open a Python terminal and import JuliaCall to work with our Julia version:

>>> from juliacall import Main as jl

We can now directly load a Julia module, including Main, the global namespace of Julia’s interpreter, with from julia import ModuleToLoad and access the module objects directly or using the Module.eval() interface.

Add a Julia package...
>>> from julia import Pkg
>>> Pkg.add("BetaML")

Of course we can add a package alternatively from within Julia

"Direct" calling of Julia functions...
>>> from julia import BetaML
>>> import numpy as np
>>> model = BetaML.RandomForestEstimator()
>>> fit!(model,[[1,10],[2,12],[12,1]],["a","a","b"])
>>> predictions = BetaML.predict(model,np.array([[2,9],[13,0]]))
>>> predictions
[{'b': 0.36666666666666664, 'a': 0.6333333333333333}, {'b': 0.7333333333333333, 'a': 0.26666666666666666}]
Access using the eval() interface...

If we are using the jl.eval() interface, the objects we use must be already known to Julia. To pass objects from Python to Julia, we can import the Julia Main module (the root module in Julia) and assign the needed variables, e.g.

>>> X_python = [1,2,3,2,4]
>>> from julia import Main
>>> Main.X_julia = X_python
>>> Main.eval('BetaML.gini(X_julia)')
0.7199999999999999
>>> Main.eval("""
...   function makeProd(x,y)
...       return x*y
...   end
...   """
... )
>>> Main.eval("makeProd(2,3)") # or Main.makeProd(2,3)

For large scripts instead of using eval() we can equivalently use Main.include("aJuliaScript.jl")

Using R in Julia

To use R from within Julia we use the RCall package.

julia> ENV["R_HOME"] = "*" #  # will force RCall to download and use a "private to Julia" (conda based) version of R. use "/path/to/R/directory" (e.g. `/usr/lib/R`) if you want to reuse a version already installed on your system"*"
julia> # using Pkg # Pkg.add("RCall") # Pkg.build("RCall") using RCall
julia> R""" sumMyArgs <- function(i,j) i+j getNthElement <- function(vec,n) { return(vec[n]) } """RCall.RObject{RCall.ClosSxp} function (vec, n) { return(vec[n]) }
julia> a = rcopy(R"sumMyArgs"(3,4)) # 7 - here we call the R object (a function) with Julia parameters7
julia> b = rcopy(R"getNthElement"([1,2,3],1)) # 1 - no differences in array indexing here1
julia> d = rcopy(R"as.integer(getNthElement(c(1,$a,3),2))") # 7 - here we interpolate the R call7
julia> d = convert(Int64,R"getNthElement(c(1,$a,3),2)")7

While we don't have here the problem of different array indexing convention (both Julia and R start indexing arrays at 1), we have the "problem" that the output returned by using R"..." is not yet an exploitable Julia object but it remains as an RObject that we can convert with rcopy() or explicitly with convert(T,obj). Also, R elements are all floats by default, so if we need an integer in Julia we need to explicitly convert it, either in R or in Julia.

If the R code is on a script, we don't have here a sort of @Rinclude macro, so let's implement it ourselves by loading the file content as a file and evaluating it using the function reval provided by RCall:

julia> macro Rinclude(fname)
           quote
               rCodeString = read($fname,String)
               reval(rCodeString)
               nothing
           end
       end@Rinclude (macro with 1 method)
julia> RCode = """ sumMyArgs <- function(i, j, z) i+j+z """"sumMyArgs <- function(i, j, z) i+j+z\n"
julia> open(f->write(f,RCode),"RScript.R","w")37
julia> @Rinclude("RScript.R")
julia> a = rcopy(R"sumMyArgs"(3,4,5)) # 1212

Using Julia in R

Installation of the R package JuliaCall

JuliaCall can be installed from CRAN:

> install.packages("JuliaCall")
> library(JuliaCall)
install_julia()

install_julia() will force the download of R and install a private copy of Julia. If you prefer to use instead an existing version of Julia and having R default to download a private version only if it can't find a version already installed, use julia_setup(installJulia = TRUE) instead of install_julia(), eventually passing the JULIA_HOME = "/path/to/julia/binary/executable/directory" (e.g. JULIA_HOME = "/home/myUser/lib/julia-1.7.0/bin") parameter to the julia_setup call.

JuliaCall depends for some things (like object conversion between Julia and R) from the Julia RCall package. If we don't already have it installed in Julia, it will try to install it automatically.

Running Julia libraries and code in R

On each R session we need to run the julia_setup function:

library(JuliaCall)
julia_setup() # If we have already downloaded a private version of Julia for R it will be retrieved automatically

We can now load a Julia module and access the module objects directly or using the Module.eval() interface.

Add a Julia package...
> julia_eval('using Pkg; Pkg.add("BetaML")')

Of course we can add a package alternatively from within Julia

Let's load some data from R and do some work with this data in Julia:

> library(datasets)
> X <- as.matrix(sapply(iris[,1:4], as.numeric))
> y <- sapply(iris[,5], as.integer)
Calling of Julia functions with julia_call...

With JuliaCall, differently than PyJulia, we can't call directly the Julia functions but we need to employ the R function julia_call("juliaFunction",args):

> julia_eval("using BetaML")
> yencoded <- julia_call("integerEncoder",y)
> ids      <- julia_call("shuffle",1:length(y))
> Xs       <- X[ids,]
> ys       <- yencoded[ids]
> cOut     <- julia_call("kmeans",Xs,3L)    # kmeans expects K to be an integer
> y_hat    <- sapply(cOut[1],as.integer)[,] # We need a vector, not a matrix
> acc      <- julia_call("accuracy",y_hat,ys)
> acc
[1] 0.8933333
Access using the eval() interface...

As alternative, we can embed Julia code directly in R using the julia_eval() function:

> kMeansR  <- julia_eval('
+      function accFromKmeans(x,k,y_true)
+        cOut = kmeans(x,Int(k))
+        acc = accuracy(cOut[1],y_true)
+        return acc
+      end
+ ')

We can then call the above function in R in one of the following three ways:

  1. kMeansR(Xs,3,ys)
  2. julia_assign("Xs_julia", Xs); julia_assign("ys_julia", ys); julia_eval("accFromKmeans(Xs_julia,3,ys_julia)")
  3. julia_call("accFromKmeans",Xs,3,ys).

While other "convenience" functions are provided by the package, using julia_call or julia_assign followed by julia_eval should suffice to accomplish most of the task we may need in Julia.

Some performance tips

TODO: check this section, as with Julia evolution some issues reported here are no longer true (thanks to better compiler heuristics)

Type stability

"Type stable" functions guarantee to the compiler that given a certain method (i.e. with the arguments being of a given type) the object returned by the function is also of a certain fixed type. Type stability is fundamental to allow type inference continue across the function call stack.

julia> function f1(x)    # Type unstable
           outVector = [1,2.0,"2"]
           if x < 0
               return outVector[1]
           elseif x == 0
               return outVector[2]
           else
               return outVector[3]
           end
       endf1 (generic function with 1 method)
julia> function f2(x) # Type stable outVector = [1,convert(Int64,2.0),parse(Int64,"2")] if x < 0 return outVector[1] elseif x == 0 return outVector[2] else return outVector[3] end endf2 (generic function with 1 method)
julia> a = f1(0)2.0
julia> b = f1(1)"2"
julia> typeof(a)Float64
julia> typeof(b)String
julia> c = f2(0)2
julia> d = f2(1)2
julia> typeof(c)Int64
julia> typeof(d)Int64
julia> using BenchmarkTools
julia> @btime f1(0) # 661 ns 6 allocations 53.873 ns (5 allocations: 160 bytes) 2.0
julia> @btime f2(0) # 55 ns 1 allocations 20.932 ns (1 allocation: 48 bytes) 2
julia> @code_warntype f1(0) # Body::AnyMethodInstance for Main.var"Main".f1(::Int64) from f1(x) @ Main.var"Main" REPL[1]:1 Arguments #self#::Core.Const(Main.var"Main".f1) x::Int64 Locals outVector::Vector{Any} Body::ANY 1 ─ (outVector = Base.vect(1, 2.0, "2")) │ %2 = Main.var"Main".:<::Core.Const(<) │ %3 = (%2)(x, 0)::Bool └── goto #3 if not %3 2 ─ %5 = outVector::Vector{Any} │ %6 = Base.getindex(%5, 1)::ANY └── return %6 3 ─ %8 = Main.var"Main".:(==)::Core.Const(==) │ %9 = (%8)(x, 0)::Bool └── goto #5 if not %9 4 ─ %11 = outVector::Vector{Any} │ %12 = Base.getindex(%11, 2)::ANY └── return %12 5 ─ %14 = outVector::Vector{Any} │ %15 = Base.getindex(%14, 3)::ANY └── return %15
julia> @code_warntype f2(0) # Body::Int64MethodInstance for Main.var"Main".f2(::Int64) from f2(x) @ Main.var"Main" REPL[2]:1 Arguments #self#::Core.Const(Main.var"Main".f2) x::Int64 Locals outVector::Vector{Int64} Body::Int64 1 ─ %1 = Main.var"Main".convert::Core.Const(convert) │ %2 = Main.var"Main".Int64::Core.Const(Int64) │ %3 = (%1)(%2, 2.0)::Core.Const(2) │ %4 = Main.var"Main".parse::Core.Const(parse) │ %5 = Main.var"Main".Int64::Core.Const(Int64) │ %6 = (%4)(%5, "2")::Int64 │ (outVector = Base.vect(1, %3, %6)) │ %8 = Main.var"Main".:<::Core.Const(<) │ %9 = (%8)(x, 0)::Bool └── goto #3 if not %9 2 ─ %11 = outVector::Vector{Int64} │ %12 = Base.getindex(%11, 1)::Int64 └── return %12 3 ─ %14 = Main.var"Main".:(==)::Core.Const(==) │ %15 = (%14)(x, 0)::Bool └── goto #5 if not %15 4 ─ %17 = outVector::Vector{Int64} │ %18 = Base.getindex(%17, 2)::Int64 └── return %18 5 ─ %20 = outVector::Vector{Int64} │ %21 = Base.getindex(%20, 3)::Int64 └── return %21

While in general it is NOT important to annotate function parameters for performance, it is important to annotate struct fields with concrete types

julia> abstract type Goo end
julia> struct Foo <: Goo x::Number end
julia> struct Boo <: Goo x::Int64 end
julia> function f1(o::Goo) return o.x +2 endf1 (generic function with 2 methods)
julia> fobj = Foo(1)Main.var"Main".Foo(1)
julia> bobj = Boo(1)Main.var"Main".Boo(1)
julia> @btime f1($fobj) # 17.1 ns 0 allocations 18.089 ns (0 allocations: 0 bytes) 3
julia> @btime f1($bobj) # 2.8 ns 0 allocations 2.785 ns (0 allocations: 0 bytes) 3

Here the same function under some argument types is type stable, under other argument types is not

julia> @code_warntype f1(fobj)MethodInstance for Main.var"Main".f1(::Main.var"Main".Foo)
  from f1(o::Main.var"Main".Goo) @ Main.var"Main" REPL[4]:1
Arguments
  #self#::Core.Const(Main.var"Main".f1)
  o::Main.var"Main".Foo
Body::ANY
1 ─ %1 = Main.var"Main".:+::Core.Const(+)
│   %2 = Base.getproperty(o, :x)::NUMBER
│   %3 = (%1)(%2, 2)::ANY
└──      return %3
julia> @code_warntype f1(bobj)MethodInstance for Main.var"Main".f1(::Main.var"Main".Boo) from f1(o::Main.var"Main".Goo) @ Main.var"Main" REPL[4]:1 Arguments #self#::Core.Const(Main.var"Main".f1) o::Main.var"Main".Boo Body::Int64 1 ─ %1 = Main.var"Main".:+::Core.Const(+) │ %2 = Base.getproperty(o, :x)::Int64 │ %3 = (%1)(%2, 2)::Int64 └── return %3

Avoid (non-constant) global variables

julia> g        = 22
julia> const cg = 1 # we can't change the _type_ of the object bound to a constant variable1
julia> const cg = 2 # we can rebind to another object but we need to still use the const keyword - attention that this may force recompiling of all code depending on cg !2
julia> const cg = 2.5 # this would error in Julia < 1.122.5
julia> # cg = 2 # this would error ! # cg = 2.5 # this would error ! f1(x,y) = x+yf1 (generic function with 3 methods)
julia> f2(x) = x + gf2 (generic function with 1 method)
julia> f3(x) = x + cgf3 (generic function with 1 method)
julia> @btime f1(3,2) 1.552 ns (0 allocations: 0 bytes) 5
julia> @btime f2(3) # 22 times slower !!! 29.915 ns (0 allocations: 0 bytes) 5
julia> @btime f3(3) # as f1 1.552 ns (0 allocations: 0 bytes) 5.5

Loop arrays with the inner loop by rows

Julia is column major (differently than Python) so arrays of bits types are contiguous in memory across the different rows of the same column

julia> a = rand(1000,1000);
julia> function f1(x) (R,C) = size(x) cum = 0.0 for r in 1:R for c in 1:C cum += x[r,c] end end return cum endf1 (generic function with 3 methods)
julia> function f2(x) (R,C) = size(x) cum = 0.0 for c in 1:C for r in 1:R cum += x[r,c] end end return cum endf2 (generic function with 1 method)
julia> @btime f1($a) # 469.631 μs 0 allocations 926.643 μs (0 allocations: 0 bytes) 500220.6882968228
julia> @btime f2($a) # 400.739 μs 0 allocations 925.951 μs (0 allocations: 0 bytes) 500220.6882968189

Use low-level optimisation when possible

julia> function f1(x)
           s = 0.0
           for i in 1:length(x)
               s += i * x[i]
           end
           return s
       endf1 (generic function with 3 methods)
julia> function f2(x) s = 0.0 for i in 1:length(x) @inbounds s += i * x[i] # remove bound checks end return s endf2 (generic function with 1 method)
julia> function f3(x) s = 0.0 @simd for i in 1:length(x) # tell compiler it is allowed to run the loop in whatever order, allowing in-thread parallelism of modern CPUs s += i * x[i] end return s endf3 (generic function with 1 method)
julia> x = rand(10000);
julia> @btime f1($x) 9.257 μs (0 allocations: 0 bytes) 2.5231610262164485e7
julia> @btime f2($x) 9.257 μs (0 allocations: 0 bytes) 2.5231610262164485e7
julia> @btime f3($x) 5.629 μs (0 allocations: 0 bytes) 2.5231610262164526e7
julia> X = rand(100,20);
julia> function f1(x) s = 0.0 for i in 1:size(x,1) s += sum(x[i,:]) end return s endf1 (generic function with 3 methods)
julia> function f2(x) s = 0.0 @views for i in 1:size(x,1) s += sum(x[i,:]) # the slice operator copies the data.. the views macro forces to have instead to have a view (reference) end return s endf2 (generic function with 1 method)
julia> @btime f1($X) 2.783 μs (200 allocations: 21.88 KiB) 1013.9993057955776
julia> @btime f2($X) 2.077 μs (0 allocations: 0 bytes) 1013.9993057955776
Warning

Attention that while the @views macro "save time" by not copying the data, the resulting array has a pretty messy layout. If you need to use it for many subsequent operations it may be more efficient to "pay" the copy cost once and then have an array with a nicely continuous block of memory..

julia> function f1(x,y)
           if x+y > 100
               return x + y + 2
           else
               return x + 1
           end
       endf1 (generic function with 3 methods)
julia> @inline function f2(x,y) # the function is "inlined", its whole definition copied at each calling place rather than being called if x+y > 100 return x + y + 2 else return x + 1 end endf2 (generic function with 2 methods)
julia> function f3(y) s = 0.0 for i in 2:y s += f1(i,i-1) s += f1(i,i) end return s endf3 (generic function with 1 method)
julia> function f4(y) s = 0.0 for i in 2:y s += f2(i,i-1) s += f2(i,i) end return s endf4 (generic function with 1 method)
julia> x = 10001000
julia> @btime f3($x) 1.852 μs (0 allocations: 0 bytes) 2.002396e6
julia> @btime f4($x) 1.835 μs (0 allocations: 0 bytes) 2.002396e6

But attention! Not always a good idea:

julia> function f3(y)
           s = 0.0
           for i in 2:y
              s += sum(f1(a,a-1) for a in 2:i)
           end
           return s
        endf3 (generic function with 1 method)
julia> function f4(y) s = 0.0 for i in 2:y s += sum(f2(a,a-1) for a in 2:i) end return s endf4 (generic function with 1 method)
julia> x = 10001000
julia> @btime f3($x) 81.381 μs (0 allocations: 0 bytes) 3.3359915e8
julia> @btime f4($x) 81.291 μs (0 allocations: 0 bytes) 3.3359915e8

Note that the Julia compiler already inlines small functions automatically when it thinks it will improve performances

Profiling the code to discover bottlenecks

We already see @btime and @benchmark from the package BenchmarkTools.jl Remember to quote the global variables used as parameter of your function with the dollar sign to have accurate benchmarking of the function execution. Julia provides the macro @time but we should run on a second call to a given function (with a certain parameter types) or it will include compilation time in its output:

julia> function fb(x)
           out = Union{Int64,Float64}[1,2.0,3]
           push!(out,4)
           if x > 10
               if ( x > 100)
                   return [out[1],out[2]] |>  sum
               else
                   return [out[2],out[3]] |>  sum
               end
           else
               return [out[1],out[3]] |>  sum
           end
       endfb (generic function with 1 method)
julia> @time fb(3) 0.000004 seconds (3 allocations: 208 bytes) 4
julia> @time fb(3) 0.000004 seconds (3 allocations: 208 bytes) 4

We can use @profile function(x,y) to use a sample-based profiling

julia> using Profile # in the stdlib
julia> function foo(n) a = rand(n,n) b = a + a c = b * b return c endfoo (generic function with 2 methods)
julia> @profile (for i = 1:100; foo(1000); end) # too fast otherwise
julia> Profile.print() # on my pc: 243 rand, 174 the sum, 439 the matrix productOverhead ╎ [+additional indent] Count File:Line Function ========================================================= ╎315 @Base/client.jl:550 _start() ╎ 315 @Base/client.jl:317 exec_options(opts::Base.JLOptions) ╎ 315 @Base/Base.jl:306 include(mod::Module, _path::String) ╎ 315 @Base/loading.jl:3012 _include(mapexpr::Function, mod::Module, _… ╎ 315 @Base/loading.jl:2952 include_string(mapexpr::typeof(identity),… ╎ 315 @Base/boot.jl:489 eval(m::Module, e::Any) ╎ ╎ 315 @Documenter/…docs.jl:274 kwcall(::@NamedTuple{sitename::Strin… ╎ ╎ 315 @Documenter/…ocs.jl:281 makedocs(; debug::Bool, format::Docu… ╎ ╎ 315 @Base/file.jl:112 cd(f::Documenter.var"#99#100"{Documenter.… ╎ ╎ 315 @Documenter/…cs.jl:282 #99 ╎ ╎ 315 @Base/env.jl:283 withenv(::Documenter.var"#101#102"{Docum… ╎ ╎ ╎ 315 @Documenter/…cs.jl:283 #101 ╎ ╎ ╎ 315 @Documenter/…s.jl:170 dispatch(::Type{Documenter.Builde… ╎ ╎ ╎ 315 @Documenter/…e.jl:224 runner(::Type{Documenter.Builder… ╎ ╎ ╎ 315 @Documenter/…e.jl:60 expand(doc::Documenter.Document) ╎ ╎ ╎ 315 @Documenter/….jl:170 dispatch(::Type{Documenter.Expa… ╎ ╎ ╎ ╎ 315 @Documenter/…jl:1017 runner(::Type{Documenter.Expan… ╎ ╎ ╎ ╎ 315 @IOCapture/….jl:100 kwcall(::@NamedTuple{rethrow::… ╎ ╎ ╎ ╎ 315 @IOCapture/….jl:167 capture(f::Documenter.var"#72… ╎ ╎ ╎ ╎ 315 @Base/…ging.jl:653 with_logger ╎ ╎ ╎ ╎ 315 @Base/…ging.jl:542 with_logstate(f::IOCapture.v… ╎ ╎ ╎ ╎ ╎ 315 @IOCapture/…jl:170 (::IOCapture.var"#12#13"{Ty… ╎ ╎ ╎ ╎ ╎ 315 @Documenter/…:1018 (::Documenter.var"#72#73"{… ╎ ╎ ╎ ╎ ╎ 315 @Base/file.jl:112 cd(f::Documenter.var"#74#7… ╎ ╎ ╎ ╎ ╎ 315 @Documenter/…:1019 #74 ╎ ╎ ╎ ╎ ╎ 315 @Base/…ot.jl:489 eval(m::Module, e::Any) ╎ ╎ ╎ ╎ ╎ ╎ 315 REPL[3]:1 top-level scope ╎ ╎ ╎ ╎ ╎ ╎ 315 @Profile/…jl:60 macro expansion ╎ ╎ ╎ ╎ ╎ ╎ 315 REPL[3]:1 macro expansion ╎ ╎ ╎ ╎ ╎ ╎ 86 REPL[2]:2 foo(n::Int64) ╎ ╎ ╎ ╎ ╎ ╎ 86 @Random/….jl:279 rand ╎ ╎ ╎ ╎ ╎ ╎ ╎ 86 @Random/….jl:291 rand ╎ ╎ ╎ ╎ ╎ ╎ ╎ 86 @Random/….jl:290 rand ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:668 Array ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:661 Array ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:651 Array ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:604 new_as_memoryr… 10╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:588 GenericMemory ╎ ╎ ╎ ╎ ╎ ╎ ╎ 76 @Random/….jl:269 rand! ╎ ╎ ╎ ╎ ╎ ╎ ╎ 76 @Random/….jl:298 rand! ╎ ╎ ╎ ╎ ╎ ╎ ╎ 76 @Random/….jl:170 xoshiro_bulk ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 3 @Random/….jl:258 xoshiro_bulk_s… 3╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 3 @Random/….jl:87 _plus ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 3 @Random/….jl:260 xoshiro_bulk_s… 3╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 3 @Random/….jl:93 _xor ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 70 @Random/….jl:266 xoshiro_bulk_s… ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 16 @Base/…er.jl:178 unsafe_store! 16╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 16 @Base/…er.jl:178 unsafe_store! 54╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 54 @Random/….jl:124 _bits2float ╎ ╎ ╎ ╎ ╎ ╎ 173 REPL[2]:3 foo(n::Int64) ╎ ╎ ╎ ╎ ╎ ╎ 173 @Base/…th.jl:16 +(A::Matrix{Float64},… ╎ ╎ ╎ ╎ ╎ ╎ ╎ 173 @Base/…st.jl:883 broadcast_preservin… ╎ ╎ ╎ ╎ ╎ ╎ ╎ 173 @Base/…st.jl:894 materialize ╎ ╎ ╎ ╎ ╎ ╎ ╎ 173 @Base/…st.jl:919 copy ╎ ╎ ╎ ╎ ╎ ╎ ╎ 35 @Base/…st.jl:947 copyto! ╎ ╎ ╎ ╎ ╎ ╎ ╎ 35 @Base/…st.jl:994 copyto! ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 35 @Base/…op.jl:77 macro expansion ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 35 @Base/…st.jl:995 macro expansi… ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 11 @Base/…st.jl:616 getindex ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 11 @Base/…st.jl:620 _getindex ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 11 @Base/…st.jl:671 _broadcast… ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +1 11 @Base/…st.jl:695 _getindex ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +2 11 @Base/…st.jl:665 _broadcast… ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +3 11 @Base/…al.jl:743 getindex ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +4 11 @Base/…ay.jl:929 getindex ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +5 1 @Base/…ay.jl:1377 _to_linea… ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +6 1 @Base/…ay.jl:3052 _sub2ind ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +7 1 @Base/…ay.jl:3068 _sub2ind ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +8 1 @Base/…ay.jl:3084 _sub2ind_… 1╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +9 1 @Base/int.jl:87 + 10╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +5 10 @Base/…ls.jl:920 getindex ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 24 @Base/…al.jl:745 setindex! ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 24 @Base/…ay.jl:997 setindex! 24╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 24 @Base/…ay.jl:1003 _setindex! ╎ ╎ ╎ ╎ ╎ ╎ ╎ 138 @Base/…st.jl:227 similar ╎ ╎ ╎ ╎ ╎ ╎ ╎ 138 @Base/…st.jl:228 similar ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 138 @Base/…ay.jl:866 similar ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 138 @Base/…ay.jl:867 similar ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 138 @Base/…ot.jl:661 Array ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 138 @Base/…ot.jl:651 Array ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 138 @Base/…ot.jl:604 new_as_mem… 138╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ +1 138 @Base/…ot.jl:588 GenericMem… ╎ ╎ ╎ ╎ ╎ ╎ 56 REPL[2]:4 foo(n::Int64) ╎ ╎ ╎ ╎ ╎ ╎ 56 @LinearAlgebra/…:136 * ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ay.jl:377 similar ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:661 Array ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:651 Array ╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:604 new_as_memoryref 10╎ ╎ ╎ ╎ ╎ ╎ ╎ 10 @Base/…ot.jl:588 GenericMemory ╎ ╎ ╎ ╎ ╎ ╎ ╎ 46 @LinearAlgebra/…:265 mul! ╎ ╎ ╎ ╎ ╎ ╎ ╎ 46 @LinearAlgebra/…:297 mul! ╎ ╎ ╎ ╎ ╎ ╎ ╎ 46 @LinearAlgebra/…:328 _mul! ╎ ╎ ╎ ╎ ╎ ╎ ╎ 46 @LinearAlgebra/…:507 generic_matm… ╎ ╎ ╎ ╎ ╎ ╎ ╎ 46 @LinearAlgebra/…:533 _syrk_herk_… ╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 46 @LinearAlgebra/…:806 gemm_wrapp… 46╎ ╎ ╎ ╎ ╎ ╎ ╎ ╎ 46 @LinearAlgebra/…:1642 gemm!(tr… ╎5996 @Base/task.jl:868 task_done_hook(t::Task) ╎ 5996 @Base/task.jl:1228 wait() 5996╎ 5996 @Base/task.jl:1216 poptask(W::Base.IntrusiveLinkedListSynchronize… ╎2998 @Reseau/…_resolvers.jl:522 _addrinfo_worker_entry ╎ 2998 @Base/channels.jl:707 iterate ╎ 2998 @Base/channels.jl:709 iterate ╎ 2998 @Base/channels.jl:526 take! ╎ 2998 @Base/channels.jl:532 take_buffered ╎ 2998 @Base/condition.jl:136 wait ╎ ╎ 2998 @Base/condition.jl:141 wait(c::Base.GenericCondition{Reentran… ╎ ╎ 2998 @Base/task.jl:1228 wait() 2997╎ ╎ 2998 @Base/task.jl:1216 poptask(W::Base.IntrusiveLinkedListSynch… ╎2998 @Reseau/…_resolvers.jl:524 _addrinfo_worker_entry ╎ 2998 @Base/channels.jl:709 iterate ╎ 2998 @Base/channels.jl:526 take! ╎ 2998 @Base/channels.jl:532 take_buffered ╎ 2998 @Base/condition.jl:136 wait ╎ 2998 @Base/condition.jl:141 wait(c::Base.GenericCondition{Reentrant… ╎ ╎ 2998 @Base/task.jl:1228 wait() 2998╎ ╎ 2998 @Base/task.jl:1216 poptask(W::Base.IntrusiveLinkedListSynchr… ╎127 @Reseau/…poll/epoll.jl:274 _backend_poll_once! 127╎ 127 @Base/promotion.jl:637 == ╎1372 @Reseau/…ll/runtime.jl:473 _poller_thread_entry ╎ 1372 @Reseau/…ll/runtime.jl:451 _poller_thread_main! 1372╎ 1372 @Reseau/…oll/epoll.jl:268 _backend_poll_once! Total snapshots: 19487. Utilization: 38% across all threads and tasks. Use the `groupby` kwarg to break down by thread and/or task.
julia> Profile.clear()

Introspection and debugging

To discover problems on the code more in general we can use several introspection functions that Julia provides us (some of which we have already seen):

julia> # @less rand(3)  # Show the source code of the specific method invoked - use `q` to quit
       # @edit rand(3)  # Like @less but it opens the source code in an editor
       methods(foo)# 2 methods for generic function "foo" from Main.var"Main":
 [1] foo()
     @ REPL[1]:1
 [2] foo(n)
     @ REPL[2]:1
julia> @which foo(2) # which method am I using when I call foo with an integer?foo(n) @ Main.var"Main" REPL[2]:1
julia> typeof(a)Matrix{Float64} (alias for Array{Float64, 2})
julia> eltype(a)Float64
julia> isa(1.2, Number)true
julia> fieldnames(Foo)(:x,)
julia> dump(fobj)Main.var"Main".Foo x: Int64 1
julia> names(Main, all=false) # available (e.g. exported) identifiers of a given module36-element Vector{Symbol}: :BUILDED_DOC_HTML :BUILDED_DOC_PDF :Base :Core :LESSONS_ROOTDIR :LESSONS_ROOTDIR_TMP :LESSONS_SUBDIR :MAKE_PDF :Main :__atexample__named__0003 ⋮ :__atexample__named__q0110 :include_sandbox :lessons_rootdir_basename :lessons_rootdir_tmp_basename :link_example :literate_directory :makeList :preprocess :rdir
julia> sizeof(2) # bytes8
julia> typemin(Int64)-9223372036854775808
julia> typemax(Int64)9223372036854775807
julia> bitstring(2)"0000000000000000000000000000000000000000000000000000000000000010"

Various low-level interpretation of an expression

julia> @code_native foo(3)	.text
	.file	"foo"
	.section	.ltext,"axl",@progbits
	.globl	julia_foo_141784                # -- Begin function julia_foo_141784
	.p2align	4, 0x90
	.type	julia_foo_141784,@function
julia_foo_141784:                       # @julia_foo_141784
; Function Signature: foo(Int64)
; ┌ @ REPL[2]:1 within `foo`
# %bb.0:                                # %top
	#DEBUG_VALUE: foo:n <- $rdi
	push	rbp
	mov	rbp, rsp
	push	r15
	push	r14
	push	r13
	push	r12
	push	rbx
	sub	rsp, 72
	vxorps	xmm0, xmm0, xmm0
	vmovups	ymmword ptr [rbp - 80], ymm0
	#APP
	mov	rax, qword ptr fs:[0]
	#NO_APP
; │ @ REPL[2]:2 within `foo`
; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:279 within `rand` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:291 @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:290
; ││┌ @ boot.jl:668 within `Array` @ boot.jl:661 @ boot.jl:651
; │││┌ @ boot.jl:640 within `checked_dims`
; ││││┌ @ boot.jl:610 within `_checked_mul_dims`
	mov	r14, rdi
	imul	r14, rdi
	movabs	rcx, 9223372036854775806
	mov	r13, qword ptr [rax - 8]
	mov	qword ptr [rbp - 80], 8
	mov	rax, qword ptr [r13]
	mov	qword ptr [rbp - 72], rax
	lea	rax, [rbp - 80]
	mov	qword ptr [r13], rax
	seto	al
; │││││ @ boot.jl:613 within `_checked_mul_dims`
	cmp	rdi, rcx
; ││││└
; ││││ @ boot.jl:641 within `checked_dims`
	ja	.LBB0_18
# %bb.1:                                # %top
	test	al, al
	jne	.LBB0_18
# %bb.2:                                # %L13
	movabs	rcx, 1152921504606846976
	movabs	rax, offset jl_alloc_genericmemory_unchecked
	mov	r15, rdi
; │││└
; │││┌ @ boot.jl:604 within `new_as_memoryref`
; ││││┌ @ boot.jl:588 within `GenericMemory`
	test	r14, r14
	je	.LBB0_3
# %bb.12:                               # %nonemptymem
	cmp	r14, rcx
	jae	.LBB0_20
# %bb.13:                               # %pass
	mov	rdi, qword ptr [r13 + 16]
	lea	rsi, [8*r14]
	movabs	rdx, offset ".L+Core.GenericMemory#141790.jit"
	vzeroupper
	call	rax
	mov	rbx, rax
	mov	qword ptr [rax], r14
	jmp	.LBB0_14
.LBB0_3:
	movabs	rbx, offset ".Ljl_global#141789.jit"
.LBB0_14:                               # %retval
; ││││└
; ││││┌ @ boot.jl:593 within `memoryref`
	mov	r12, qword ptr [rbx + 8]
	mov	qword ptr [rbp - 64], rbx
	movabs	rcx, 140403294785184
; │││└└
	movabs	rax, offset ijl_gc_small_alloc
	mov	esi, 456
	mov	edx, 48
	mov	qword ptr [rbp - 88], r13       # 8-byte Spill
	mov	rdi, qword ptr [r13 + 16]
	vzeroupper
	call	rax
	movabs	rcx, 140403294785184
; ││└
; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:269 within `rand!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:298
; │││┌ @ int.jl:88 within `*`
	shl	r14, 3
; ││└└
; ││┌ @ boot.jl:668 within `Array` @ boot.jl:661 @ boot.jl:651
	mov	r13, rax
	mov	qword ptr [r13 - 8], rcx
	mov	qword ptr [r13], r12
	mov	qword ptr [r13 + 8], rbx
	mov	qword ptr [r13 + 16], r15
	mov	qword ptr [r13 + 24], r15
; ││└
; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:269 within `rand!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:298
; │││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:169 within `xoshiro_bulk`
; ││││┌ @ operators.jl:472 within `>=`
; │││││┌ @ int.jl:520 within `<=`
	cmp	r14, 64
; ││││└└
	jl	.LBB0_5
# %bb.4:                                # %L34
; ││││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:170 within `xoshiro_bulk`
	movabs	rax, offset j_xoshiro_bulk_simd_141792
	mov	qword ptr [rbp - 64], r13
	mov	rdi, r12
	mov	rsi, r14
	call	rax
; ││││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:171 within `xoshiro_bulk`
; ││││┌ @ int.jl:86 within `-`
	sub	r14, rax
; ││││└
; ││││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:172 within `xoshiro_bulk`
; ││││┌ @ pointer.jl:314 within `+`
	add	r12, rax
.LBB0_5:                                # %L38
; ││││└
; ││││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:174 within `xoshiro_bulk`
; ││││┌ @ operators.jl:321 within `!=`
; │││││┌ @ promotion.jl:637 within `==`
	test	r14, r14
; ││││└└
	je	.LBB0_7
# %bb.6:                                # %L43
; ││││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:175 within `xoshiro_bulk`
	movabs	rax, offset j_xoshiro_bulk_nosimd_141793
	mov	qword ptr [rbp - 64], r13
	mov	rdi, r12
	mov	rsi, r14
	call	rax
.LBB0_7:                                # %L45
; │└└└
; │ @ REPL[2]:3 within `foo`
	movabs	rax, offset "j_+_141794"
	mov	qword ptr [rbp - 64], r13
	mov	rdi, r13
	mov	rsi, r13
	call	rax
; │ @ REPL[2]:4 within `foo`
; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:136 within `*`
; ││┌ @ array.jl:191 within `size`
	mov	rdx, qword ptr [rax + 16]
	mov	rsi, qword ptr [rax + 24]
; │└└
; │ @ REPL[2]:3 within `foo`
	mov	r14, rax
	movabs	rcx, 9223372036854775806
; │ @ REPL[2]:4 within `foo`
; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:136 within `*`
; ││┌ @ array.jl:377 within `similar`
; │││┌ @ boot.jl:661 within `Array` @ boot.jl:651
; ││││┌ @ boot.jl:640 within `checked_dims`
; │││││┌ @ boot.jl:610 within `_checked_mul_dims`
	mov	r13, rdx
	imul	r13, rsi
	seto	al
; ││││││ @ boot.jl:614 within `_checked_mul_dims`
	cmp	rsi, rcx
; │││││└
; │││││ @ boot.jl:641 within `checked_dims`
	ja	.LBB0_19
# %bb.8:                                # %L45
	cmp	rdx, rcx
	ja	.LBB0_19
# %bb.9:                                # %L45
	test	al, al
	jne	.LBB0_19
# %bb.10:                               # %L68
	mov	qword ptr [rbp - 104], rdx      # 8-byte Spill
	mov	qword ptr [rbp - 96], rsi       # 8-byte Spill
; ││││└
; ││││┌ @ boot.jl:604 within `new_as_memoryref`
; │││││┌ @ boot.jl:588 within `GenericMemory`
	test	r13, r13
	je	.LBB0_11
# %bb.15:                               # %nonemptymem21
	movabs	rax, 1152921504606846976
	cmp	r13, rax
	jae	.LBB0_21
# %bb.16:                               # %pass24
	mov	r12, qword ptr [rbp - 88]       # 8-byte Reload
	lea	rsi, [8*r13]
	movabs	rdx, offset ".L+Core.GenericMemory#141790.jit"
	movabs	rax, offset jl_alloc_genericmemory_unchecked
	mov	rdi, qword ptr [r12 + 16]
	mov	qword ptr [rbp - 64], 0
	mov	qword ptr [rbp - 56], r14
	call	rax
	mov	rbx, rax
	mov	qword ptr [rax], r13
	jmp	.LBB0_17
.LBB0_11:
	mov	r12, qword ptr [rbp - 88]       # 8-byte Reload
	movabs	rbx, offset ".Ljl_global#141789.jit"
.LBB0_17:                               # %retval29
; │││││└
; │││││┌ @ boot.jl:593 within `memoryref`
	mov	r13, qword ptr [rbx + 8]
	mov	qword ptr [rbp - 56], r14
	mov	qword ptr [rbp - 64], rbx
	movabs	r15, 140403294785184
; ││││└└
	mov	esi, 456
	mov	edx, 48
	movabs	rax, offset ijl_gc_small_alloc
	mov	rdi, qword ptr [r12 + 16]
	mov	rcx, r15
	call	rax
	mov	rcx, qword ptr [rbp - 104]      # 8-byte Reload
	mov	rdi, qword ptr [rbp - 96]       # 8-byte Reload
; ││└└
; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:265 within `mul!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:297
; │││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:328 within `_mul!`
	movabs	r10, offset "j_generic_matmatmul_wrapper!_141795"
	mov	esi, 1308622848
	mov	edx, 1308622848
	mov	r9d, 1
; ││└└
; ││┌ @ array.jl:377 within `similar`
; │││┌ @ boot.jl:661 within `Array` @ boot.jl:651
	mov	qword ptr [rax - 8], r15
	mov	qword ptr [rax], r13
	mov	qword ptr [rax + 8], rbx
	mov	qword ptr [rbp - 64], rax
; ││└└
; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:265 within `mul!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:297
; │││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:328 within `_mul!`
	mov	dword ptr [rsp], 0
	mov	r8, r14
; ││└└
; ││┌ @ array.jl:377 within `similar`
; │││┌ @ boot.jl:661 within `Array` @ boot.jl:651
	mov	qword ptr [rax + 16], rcx
	mov	qword ptr [rax + 24], rdi
; ││└└
; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:265 within `mul!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:297
; │││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:328 within `_mul!`
	mov	rdi, rax
	mov	rcx, r14
	call	r10
	mov	rcx, qword ptr [rbp - 72]
	mov	qword ptr [r12], rcx
; │└└└
; │ @ REPL[2]:5 within `foo`
	add	rsp, 72
	pop	rbx
	pop	r12
	pop	r13
	pop	r14
	pop	r15
	pop	rbp
	ret
.LBB0_18:                               # %L9
; │ @ REPL[2]:2 within `foo`
; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:279 within `rand` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:291 @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:290
; ││┌ @ boot.jl:668 within `Array` @ boot.jl:661 @ boot.jl:651
; │││┌ @ boot.jl:641 within `checked_dims`
	movabs	rdi, offset ".Ljl_global#141787.jit"
	movabs	rax, offset j_ArgumentError_141786
	vzeroupper
	call	rax
	mov	qword ptr [rbp - 64], rax
	movabs	r14, 140403294785184
	mov	rbx, rax
	movabs	rax, offset ijl_gc_small_alloc
	mov	esi, 360
	mov	edx, 16
	mov	rdi, qword ptr [r13 + 16]
	add	r14, 27451280
	mov	rcx, r14
	call	rax
	movabs	rcx, offset ijl_throw
	mov	qword ptr [rax - 8], r14
	mov	rdi, rax
	mov	qword ptr [rax], rbx
	mov	qword ptr [rbp - 64], 0
	call	rcx
.LBB0_19:                               # %L64
; │└└└
; │ @ REPL[2]:4 within `foo`
; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:136 within `*`
; ││┌ @ array.jl:377 within `similar`
; │││┌ @ boot.jl:661 within `Array` @ boot.jl:651
; ││││┌ @ boot.jl:641 within `checked_dims`
	movabs	rdi, offset ".Ljl_global#141787.jit"
	movabs	rax, offset j_ArgumentError_141786
	mov	qword ptr [rbp - 64], 0
	call	rax
	mov	rcx, qword ptr [rbp - 88]       # 8-byte Reload
	mov	qword ptr [rbp - 64], rax
	movabs	r14, 140403294785184
	mov	rbx, rax
	mov	esi, 360
	mov	edx, 16
	movabs	rax, offset ijl_gc_small_alloc
	add	r14, 27451280
	mov	rdi, qword ptr [rcx + 16]
	mov	rcx, r14
	call	rax
	movabs	rcx, offset ijl_throw
	mov	qword ptr [rax - 8], r14
	mov	rdi, rax
	mov	qword ptr [rax], rbx
	mov	qword ptr [rbp - 64], 0
	call	rcx
.LBB0_20:                               # %fail
; │└└└└
; │ @ REPL[2]:2 within `foo`
; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:279 within `rand` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:291 @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:290
; ││┌ @ boot.jl:668 within `Array` @ boot.jl:661 @ boot.jl:651
; │││┌ @ boot.jl:604 within `new_as_memoryref`
; ││││┌ @ boot.jl:588 within `GenericMemory`
	movabs	rdi, offset ".L_j_str_invalid GenericMemory siz...#1"
	movabs	rax, offset jl_argument_error
	vzeroupper
	call	rax
.LBB0_21:                               # %fail23
; │└└└└
; │ @ REPL[2]:4 within `foo`
; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:136 within `*`
; ││┌ @ array.jl:377 within `similar`
; │││┌ @ boot.jl:661 within `Array` @ boot.jl:651
; ││││┌ @ boot.jl:604 within `new_as_memoryref`
; │││││┌ @ boot.jl:588 within `GenericMemory`
	movabs	rdi, offset ".L_j_str_invalid GenericMemory siz...#1"
	movabs	rax, offset jl_argument_error
	mov	qword ptr [rbp - 64], 0
	call	rax
.Lfunc_end0:
	.size	julia_foo_141784, .Lfunc_end0-julia_foo_141784
; └└└└└└
                                        # -- End function
	.type	".L_j_str_invalid GenericMemory siz...#1",@object # @"_j_str_invalid GenericMemory siz...#1"
	.section	.rodata.str1.1,"aMSl",@progbits,1
".L_j_str_invalid GenericMemory siz...#1":
	.asciz	"invalid GenericMemory size: the number of elements is either negative or too large for system address width"
	.size	".L_j_str_invalid GenericMemory siz...#1", 108

.set ".L+Core.Array#141791.jit", 140403294785184
	.size	".L+Core.Array#141791.jit", 8
.set ".L+Core.GenericMemory#141790.jit", 140403294814016
	.size	".L+Core.GenericMemory#141790.jit", 8
.set ".Ljl_global#141789.jit", 140403294814096
	.size	".Ljl_global#141789.jit", 8
.set ".L+Core.ArgumentError#141788.jit", 140403322236464
	.size	".L+Core.ArgumentError#141788.jit", 8
.set ".Ljl_global#141787.jit", 140403326806624
	.size	".Ljl_global#141787.jit", 8
	.section	".note.GNU-stack","",@progbits
julia> @code_llvm foo(3); Function Signature: foo(Int64) ; @ REPL[2]:1 within `foo` define nonnull ptr @julia_foo_141977(i64 signext %"n::Int64") #0 { top: %gcframe1 = alloca [4 x ptr], align 16 call void @llvm.memset.p0.i64(ptr align 16 %gcframe1, i8 0, i64 32, i1 true) %thread_ptr = call ptr asm "movq %fs:0, $0", "=r"() #16 %tls_ppgcstack = getelementptr inbounds i8, ptr %thread_ptr, i64 -8 %tls_pgcstack = load ptr, ptr %tls_ppgcstack, align 8 store i64 8, ptr %gcframe1, align 8 %frame.prev = getelementptr inbounds ptr, ptr %gcframe1, i64 1 %task.gcstack = load ptr, ptr %tls_pgcstack, align 8 store ptr %task.gcstack, ptr %frame.prev, align 8 store ptr %gcframe1, ptr %tls_pgcstack, align 8 ; @ REPL[2]:2 within `foo` ; ┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:279 within `rand` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:291 @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:290 ; │┌ @ boot.jl:668 within `Array` @ boot.jl:661 @ boot.jl:651 ; ││┌ @ boot.jl:640 within `checked_dims` ; │││┌ @ boot.jl:610 within `_checked_mul_dims` %0 = call { i64, i1 } @llvm.smul.with.overflow.i64(i64 %"n::Int64", i64 %"n::Int64") %1 = extractvalue { i64, i1 } %0, 0 %2 = extractvalue { i64, i1 } %0, 1 ; ││││ @ boot.jl:613 within `_checked_mul_dims` %3 = icmp ugt i64 %"n::Int64", 9223372036854775806 %4 = or i1 %3, %2 ; │││└ ; │││ @ boot.jl:641 within `checked_dims` br i1 %4, label %L9, label %L13 L9: ; preds = %top %5 = call [1 x ptr] @j_ArgumentError_141979(ptr nonnull @"jl_global#141980.jit") %gc_slot_addr_0 = getelementptr inbounds ptr, ptr %gcframe1, i64 2 %6 = extractvalue [1 x ptr] %5, 0 store ptr %6, ptr %gc_slot_addr_0, align 8 %ptls_field66 = getelementptr inbounds i8, ptr %tls_pgcstack, i64 16 %ptls_load67 = load ptr, ptr %ptls_field66, align 8 %"box::ArgumentError" = call noalias nonnull align 8 dereferenceable(16) ptr @ijl_gc_small_alloc(ptr %ptls_load67, i32 360, i32 16, i64 140403322236464) #12 %"box::ArgumentError.tag_addr" = getelementptr inbounds i64, ptr %"box::ArgumentError", i64 -1 store atomic i64 140403322236464, ptr %"box::ArgumentError.tag_addr" unordered, align 8 store ptr %6, ptr %"box::ArgumentError", align 8 store ptr null, ptr %gc_slot_addr_0, align 8 call void @ijl_throw(ptr nonnull %"box::ArgumentError") unreachable L13: ; preds = %top ; ││└ ; ││┌ @ boot.jl:604 within `new_as_memoryref` ; │││┌ @ boot.jl:588 within `GenericMemory` %memorynew_empty = icmp eq i64 %1, 0 br i1 %memorynew_empty, label %retval, label %nonemptymem L34: ; preds = %retval store ptr %"new::Array", ptr %gc_slot_addr_056, align 8 ; │└└└ ; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:269 within `rand!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:298 ; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:170 within `xoshiro_bulk` %7 = call i64 @j_xoshiro_bulk_simd_141985(ptr nonnull %memory_data, i64 signext %25) ; │││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:171 within `xoshiro_bulk` ; │││┌ @ int.jl:86 within `-` %8 = sub i64 %25, %7 ; │││└ ; │││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:172 within `xoshiro_bulk` ; │││┌ @ pointer.jl:314 within `+` %9 = getelementptr i8, ptr %memory_data, i64 %7 br label %L38 L38: ; preds = %retval, %L34 %value_phi = phi ptr [ %9, %L34 ], [ %memory_data, %retval ] %value_phi4 = phi i64 [ %8, %L34 ], [ %25, %retval ] ; │││└ ; │││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:174 within `xoshiro_bulk` ; │││┌ @ operators.jl:321 within `!=` ; ││││┌ @ promotion.jl:637 within `==` %.not = icmp eq i64 %value_phi4, 0 ; │││└└ br i1 %.not, label %L45, label %L43 L43: ; preds = %L38 store ptr %"new::Array", ptr %gc_slot_addr_056, align 8 ; │││ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:175 within `xoshiro_bulk` call void @j_xoshiro_bulk_nosimd_141986(ptr %value_phi, i64 signext %value_phi4) br label %L45 L45: ; preds = %L43, %L38 store ptr %"new::Array", ptr %gc_slot_addr_056, align 8 ; └└└ ; @ REPL[2]:3 within `foo` %10 = call nonnull ptr @"j_+_141987"(ptr nonnull %"new::Array", ptr nonnull %"new::Array") ; @ REPL[2]:4 within `foo` ; ┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:136 within `*` ; │┌ @ array.jl:191 within `size` %.size_ptr = getelementptr inbounds i8, ptr %10, i64 16 %.size.sroa.0.0.copyload = load i64, ptr %.size_ptr, align 8 %.size6.sroa.1.0..size_ptr5.sroa_idx = getelementptr inbounds i8, ptr %10, i64 24 %.size6.sroa.1.0.copyload = load i64, ptr %.size6.sroa.1.0..size_ptr5.sroa_idx, align 8 ; │└ ; │┌ @ array.jl:377 within `similar` ; ││┌ @ boot.jl:661 within `Array` @ boot.jl:651 ; │││┌ @ boot.jl:640 within `checked_dims` ; ││││┌ @ boot.jl:610 within `_checked_mul_dims` %11 = call { i64, i1 } @llvm.smul.with.overflow.i64(i64 %.size.sroa.0.0.copyload, i64 %.size6.sroa.1.0.copyload) %12 = extractvalue { i64, i1 } %11, 0 %13 = extractvalue { i64, i1 } %11, 1 ; │││││ @ boot.jl:613 within `_checked_mul_dims` %14 = icmp ugt i64 %.size.sroa.0.0.copyload, 9223372036854775806 %15 = or i1 %14, %13 ; │││││ @ boot.jl:614 within `_checked_mul_dims` %16 = icmp ugt i64 %.size6.sroa.1.0.copyload, 9223372036854775806 %17 = or i1 %16, %15 ; ││││└ ; ││││ @ boot.jl:641 within `checked_dims` br i1 %17, label %L64, label %L68 L64: ; preds = %L45 store ptr null, ptr %gc_slot_addr_056, align 8 %18 = call [1 x ptr] @j_ArgumentError_141979(ptr nonnull @"jl_global#141980.jit") %19 = extractvalue [1 x ptr] %18, 0 store ptr %19, ptr %gc_slot_addr_056, align 8 %ptls_load76 = load ptr, ptr %ptls_field80, align 8 %"box::ArgumentError14" = call noalias nonnull align 8 dereferenceable(16) ptr @ijl_gc_small_alloc(ptr %ptls_load76, i32 360, i32 16, i64 140403322236464) #12 %"box::ArgumentError14.tag_addr" = getelementptr inbounds i64, ptr %"box::ArgumentError14", i64 -1 store atomic i64 140403322236464, ptr %"box::ArgumentError14.tag_addr" unordered, align 8 store ptr %19, ptr %"box::ArgumentError14", align 8 store ptr null, ptr %gc_slot_addr_056, align 8 call void @ijl_throw(ptr nonnull %"box::ArgumentError14") unreachable L68: ; preds = %L45 ; │││└ ; │││┌ @ boot.jl:604 within `new_as_memoryref` ; ││││┌ @ boot.jl:588 within `GenericMemory` %memorynew_empty18 = icmp eq i64 %12, 0 br i1 %memorynew_empty18, label %retval29, label %nonemptymem21 nonemptymem: ; preds = %L13 ; └└└└└ ; @ REPL[2]:2 within `foo` ; ┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:279 within `rand` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:291 @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:290 ; │┌ @ boot.jl:668 within `Array` @ boot.jl:661 @ boot.jl:651 ; ││┌ @ boot.jl:604 within `new_as_memoryref` ; │││┌ @ boot.jl:588 within `GenericMemory` %20 = icmp ult i64 %1, 1152921504606846976 br i1 %20, label %pass, label %fail fail: ; preds = %nonemptymem call void @jl_argument_error(ptr nonnull @"_j_str_invalid GenericMemory siz...#1") unreachable pass: ; preds = %nonemptymem %21 = shl nuw nsw i64 %1, 3 %ptls_field = getelementptr inbounds i8, ptr %tls_pgcstack, i64 16 %ptls_load = load ptr, ptr %ptls_field, align 8 %"Memory{Float64}[]" = call noalias nonnull align 16 ptr @jl_alloc_genericmemory_unchecked(ptr %ptls_load, i64 %21, ptr nonnull @"+Core.GenericMemory#141983.jit") #17 store i64 %1, ptr %"Memory{Float64}[]", align 8 br label %retval retval: ; preds = %pass, %L13 %22 = phi ptr [ %"Memory{Float64}[]", %pass ], [ @"jl_global#141982.jit", %L13 ] ; │││└ ; │││┌ @ boot.jl:593 within `memoryref` %memory_data_ptr = getelementptr inbounds { i64, ptr }, ptr %22, i64 0, i32 1 %memory_data = load ptr, ptr %memory_data_ptr, align 8 %gc_slot_addr_056 = getelementptr inbounds ptr, ptr %gcframe1, i64 2 store ptr %22, ptr %gc_slot_addr_056, align 8 ; ││└└ %ptls_field80 = getelementptr inbounds i8, ptr %tls_pgcstack, i64 16 %ptls_load81 = load ptr, ptr %ptls_field80, align 8 %"new::Array" = call noalias nonnull align 8 dereferenceable(48) ptr @ijl_gc_small_alloc(ptr %ptls_load81, i32 456, i32 48, i64 140403294785184) #12 %"new::Array.tag_addr" = getelementptr inbounds i64, ptr %"new::Array", i64 -1 store atomic i64 140403294785184, ptr %"new::Array.tag_addr" unordered, align 8 %23 = getelementptr inbounds i8, ptr %"new::Array", i64 8 store ptr %memory_data, ptr %"new::Array", align 8 store ptr %22, ptr %23, align 8 %"new::Array.size_ptr" = getelementptr inbounds i8, ptr %"new::Array", i64 16 store i64 %"n::Int64", ptr %"new::Array.size_ptr", align 8 %"new::Tuple.sroa.2.0.new::Array.size_ptr.sroa_idx" = getelementptr inbounds i8, ptr %"new::Array", i64 24 store i64 %"n::Int64", ptr %"new::Tuple.sroa.2.0.new::Array.size_ptr.sroa_idx", align 8 ; │└ ; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/Random.jl:269 within `rand!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:298 ; ││┌ @ essentials.jl:12 within `length` ; │││┌ @ int.jl:88 within `*` %24 = shl i64 %"n::Int64", 3 ; ││└└ ; ││┌ @ int.jl:88 within `*` %25 = mul i64 %24, %"n::Int64" ; ││└ ; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/Random/src/XoshiroSimd.jl:169 within `xoshiro_bulk` ; │││┌ @ operators.jl:472 within `>=` ; ││││┌ @ int.jl:520 within `<=` %26 = icmp slt i64 %25, 64 ; │││└└ br i1 %26, label %L38, label %L34 nonemptymem21: ; preds = %L68 ; └└└ ; @ REPL[2]:4 within `foo` ; ┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:136 within `*` ; │┌ @ array.jl:377 within `similar` ; ││┌ @ boot.jl:661 within `Array` @ boot.jl:651 ; │││┌ @ boot.jl:604 within `new_as_memoryref` ; ││││┌ @ boot.jl:588 within `GenericMemory` %27 = icmp ult i64 %12, 1152921504606846976 br i1 %27, label %pass24, label %fail23 fail23: ; preds = %nonemptymem21 store ptr null, ptr %gc_slot_addr_056, align 8 call void @jl_argument_error(ptr nonnull @"_j_str_invalid GenericMemory siz...#1") unreachable pass24: ; preds = %nonemptymem21 %28 = shl nuw nsw i64 %12, 3 %ptls_load27 = load ptr, ptr %ptls_field80, align 8 store ptr null, ptr %gc_slot_addr_056, align 8 %gc_slot_addr_1 = getelementptr inbounds ptr, ptr %gcframe1, i64 3 store ptr %10, ptr %gc_slot_addr_1, align 8 %"Memory{Float64}[]28" = call noalias nonnull align 16 dereferenceable(16) ptr @jl_alloc_genericmemory_unchecked(ptr %ptls_load27, i64 %28, ptr nonnull @"+Core.GenericMemory#141983.jit") #17 store i64 %12, ptr %"Memory{Float64}[]28", align 8 br label %retval29 retval29: ; preds = %pass24, %L68 %29 = phi ptr [ %"Memory{Float64}[]28", %pass24 ], [ @"jl_global#141982.jit", %L68 ] ; ││││└ ; ││││┌ @ boot.jl:593 within `memoryref` %memory_data_ptr30 = getelementptr inbounds { i64, ptr }, ptr %29, i64 0, i32 1 %memory_data31 = load ptr, ptr %memory_data_ptr30, align 8 %gc_slot_addr_159 = getelementptr inbounds ptr, ptr %gcframe1, i64 3 store ptr %10, ptr %gc_slot_addr_159, align 8 store ptr %29, ptr %gc_slot_addr_056, align 8 ; │││└└ %ptls_load89 = load ptr, ptr %ptls_field80, align 8 %"new::Array37" = call noalias nonnull align 8 dereferenceable(48) ptr @ijl_gc_small_alloc(ptr %ptls_load89, i32 456, i32 48, i64 140403294785184) #12 %"new::Array37.tag_addr" = getelementptr inbounds i64, ptr %"new::Array37", i64 -1 store atomic i64 140403294785184, ptr %"new::Array37.tag_addr" unordered, align 8 %30 = getelementptr inbounds i8, ptr %"new::Array37", i64 8 store ptr %memory_data31, ptr %"new::Array37", align 8 store ptr %29, ptr %30, align 8 %"new::Array37.size_ptr" = getelementptr inbounds i8, ptr %"new::Array37", i64 16 store i64 %.size.sroa.0.0.copyload, ptr %"new::Array37.size_ptr", align 8 %"new::Tuple33.sroa.2.0.new::Array37.size_ptr.sroa_idx" = getelementptr inbounds i8, ptr %"new::Array37", i64 24 store i64 %.size6.sroa.1.0.copyload, ptr %"new::Tuple33.sroa.2.0.new::Array37.size_ptr.sroa_idx", align 8 store ptr %"new::Array37", ptr %gc_slot_addr_056, align 8 ; │└└ ; │┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:265 within `mul!` @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:297 ; ││┌ @ /cache/build/builder-amdci5-2/julialang/julia-ci/usr/share/julia/stdlib/v1.12/LinearAlgebra/src/matmul.jl:328 within `_mul!` %31 = call nonnull ptr @"j_generic_matmatmul_wrapper!_141988"(ptr nonnull %"new::Array37", i32 zeroext 1308622848, i32 zeroext 1308622848, ptr nonnull %10, ptr nonnull %10, i8 zeroext 1, i8 zeroext 0) %frame.prev92 = load ptr, ptr %frame.prev, align 8 store ptr %frame.prev92, ptr %tls_pgcstack, align 8 ; └└└ ; @ REPL[2]:5 within `foo` ret ptr %31 }
julia> @code_typed foo(3)CodeInfo( 1 ── %1 = intrinsic (Core.Intrinsics.checked_smul_int)(n, n)::Tuple{Int64, Bool} │ %2 = builtin Core.getfield(%1, 1)::Int64 │ %3 = builtin Core.getfield(%1, 2)::Bool │ %4 = intrinsic (Core.Intrinsics.ule_int)(Core.typemax_Int, n)::Bool │ %5 = intrinsic (Core.Intrinsics.or_int)(%3, %4)::Bool │ %6 = intrinsic (Core.Intrinsics.ule_int)(Core.typemax_Int, n)::Bool │ %7 = intrinsic (Core.Intrinsics.or_int)(%5, %6)::Bool └─── goto #3 if not %7 2 ── %9 = invoke Core.ArgumentError("invalid Array dimensions"::String)::ArgumentError │ builtin Core.throw(%9)::Union{} └─── unreachable 3 ── goto #4 4 ── %13 = builtin Core.memorynew(Memory{Float64}, %2)::Memory{Float64} │ %14 = builtin Core.memoryrefnew(%13)::MemoryRef{Float64} │ %15 = builtin Core.tuple(n, n)::Tuple{Int64, Int64} │ %16 = %new(Matrix{Float64}, %14, %15)::Matrix{Float64} └─── goto #5 5 ── goto #6 6 ── goto #7 7 ── %20 = $(Expr(:gc_preserve_begin, :(%16))) │ %21 = builtin Base.getfield(%16, :ref)::MemoryRef{Float64} │ %22 = builtin Base.getfield(%21, :ptr_or_offset)::Ptr{Nothing} │ %23 = intrinsic Base.bitcast(Ptr{Float64}, %22)::Ptr{Float64} │ %24 = intrinsic Base.bitcast(Ptr{UInt8}, %23)::Ptr{UInt8} │ %25 = builtin Base.getfield(%16, :size)::Tuple{Int64, Int64} │ %26 = $(Expr(:boundscheck, true))::Bool │ %27 = builtin Base.getfield(%25, 1, %26)::Int64 │ %28 = $(Expr(:boundscheck, true))::Bool │ %29 = builtin Base.getfield(%25, 2, %28)::Int64 │ %30 = intrinsic Base.mul_int(%27, %29)::Int64 │ %31 = intrinsic Base.mul_int(%30, 8)::Int64 │ %32 = intrinsic Base.sle_int(64, %31)::Bool └─── goto #9 if not %32 8 ── %34 = invoke Random.XoshiroSimd.xoshiro_bulk_simd($(QuoteNode(Random.TaskLocalRNG()))::Random.TaskLocalRNG, %24::Ptr{UInt8}, %31::Int64, Float64::Type{Float64}, $(QuoteNode(Val{8}()))::Val{8}, Random.XoshiroSimd._bits2float::typeof(Random.XoshiroSimd._bits2float))::Int64 │ %35 = intrinsic Base.sub_int(%31, %34)::Int64 │ %36 = intrinsic Base.bitcast(UInt64, %34)::UInt64 └─── %37 = intrinsic Base.add_ptr(%24, %36)::Ptr{UInt8} 9 ┄─ %38 = φ (#8 => %37, #7 => %24)::Ptr{UInt8} │ %39 = φ (#8 => %35, #7 => %31)::Int64 │ %40 = builtin (%39 === 0)::Bool │ %41 = intrinsic Base.not_int(%40)::Bool └─── goto #11 if not %41 10 ─ invoke Random.XoshiroSimd.xoshiro_bulk_nosimd($(QuoteNode(Random.TaskLocalRNG()))::Random.TaskLocalRNG, %38::Ptr{UInt8}, %39::Int64, Float64::Type{Float64}, Random.XoshiroSimd._bits2float::typeof(Random.XoshiroSimd._bits2float))::Any 11 ┄ goto #12 12 ─ $(Expr(:gc_preserve_end, :(%20))) └─── goto #13 13 ─ goto #14 14 ─ goto #15 15 ─ goto #16 16 ─ goto #17 17 ─ %51 = invoke Main.var"Main".:+(%16::Matrix{Float64}, %16::Matrix{Float64})::Matrix{Float64} │ %52 = builtin Base.getfield(%51, :size)::Tuple{Int64, Int64} │ %53 = builtin Base.getfield(%52, 1, false)::Int64 │ %54 = builtin Base.getfield(%51, :size)::Tuple{Int64, Int64} │ %55 = builtin Base.getfield(%54, 2, false)::Int64 │ %56 = intrinsic (Core.Intrinsics.checked_smul_int)(%53, %55)::Tuple{Int64, Bool} │ %57 = builtin Core.getfield(%56, 1)::Int64 │ %58 = builtin Core.getfield(%56, 2)::Bool │ %59 = intrinsic (Core.Intrinsics.ule_int)(Core.typemax_Int, %53)::Bool │ %60 = intrinsic (Core.Intrinsics.or_int)(%58, %59)::Bool │ %61 = intrinsic (Core.Intrinsics.ule_int)(Core.typemax_Int, %55)::Bool │ %62 = intrinsic (Core.Intrinsics.or_int)(%60, %61)::Bool └─── goto #19 if not %62 18 ─ %64 = invoke Core.ArgumentError("invalid Array dimensions"::String)::ArgumentError │ builtin Core.throw(%64)::Union{} └─── unreachable 19 ─ goto #20 20 ─ %68 = builtin Core.memorynew(Memory{Float64}, %57)::Memory{Float64} │ %69 = builtin Core.memoryrefnew(%68)::MemoryRef{Float64} │ %70 = builtin Core.tuple(%53, %55)::Tuple{Int64, Int64} │ %71 = %new(Matrix{Float64}, %69, %70)::Matrix{Float64} └─── goto #21 21 ─ goto #22 22 ─ goto #23 23 ─ %75 = invoke LinearAlgebra.generic_matmatmul_wrapper!(%71::Matrix{Float64}, 'N'::Char, 'N'::Char, %51::Matrix{Float64}, %51::Matrix{Float64}, true::Bool, false::Bool, $(QuoteNode(Val{LinearAlgebra.BlasFlag.GEMM}()))::Val{LinearAlgebra.BlasFlag.GEMM})::Matrix{Float64} └─── goto #24 24 ─ return %75 ) => Matrix{Float64}
julia> @code_lowered foo(3)CodeInfo( 1 ─ %1 = Main.var"Main".rand │ a = (%1)(n, n) │ %3 = Main.var"Main".:+ │ %4 = a │ %5 = a │ b = (%3)(%4, %5) │ %7 = Main.var"Main".:* │ %8 = b │ %9 = b │ c = (%7)(%8, %9) │ %11 = c └── return %11 )

We can use a debugger, like e.g. the one integrated in Juno or VSCode. Graphical debuggers allow to put a breakpoint on some specific line of code, run the code in debug mode (yes, it will be slower), let the program arrive to the breakpoint and inspect the state of the system at that point of the code, including local variables. In Julia we can also change the program interactively ! Other typical functions are running a single line, running inside a function, running until the current function return, ecc..

Runtime exceptions

As many (all?) languages, Julia when "finds" an error issues an exception, that if it is not caught at higher level in the call stack (i.e. recognised and handled) leads to an error and return to the prompt or termination of the script (and rarely with the Julia process crashing altogether).

The idea is that we try some potentially dangerous code and if some error is raised in this code we catch it and handle it.

julia> function customIndex(vect,idx;toReturn=0)
           try
               vect[idx]
           catch e
               if isa(e,BoundsError)
                   return toReturn
               end
               rethrow(e)
           end
       endcustomIndex (generic function with 1 method)
julia> a = [1,2,3]3-element Vector{Int64}: 1 2 3
julia> # a[4] # Error ("BoundsError" to be precise) customIndex(a,4)0

Note that handling exceptions is computationally expensive, so do not use exceptions in place of conditional statements

Parallel computation

Finally one note on parallel computation. We see only some basic usage of multithreading and multiprocesses in this course, but with Julia it is relatively easy to parallelize the code either using multiple threads or multiple processes. What's the difference ?

  • multithread
    • advantages: computationally "cheap" to create (the memory is shared)
    • disadvantages: limited to the number of cores within a CPU, require attention in not overwriting the same memory or doing it at the intended order ("data race"), we can't add threads dynamically (within a script)
  • multiprocesses
    • advantages: unlimited number, can be run in different CPUs of the same machine or different nodes of a cluster, even using SSH on different networks, we can add processes from within our code with addprocs(nToAdd)
    • disadvantages: the memory being copied (each process will have its own memory) are computationally expensive (you need to have a gain higher than the cost on setting a new process) and require attention to select which memory a given process will need to "bring with it" for its functionality

Note that if you are reading this document on the github pages, this script is compiled using GitHub actions where a single thread and process are available, so you will not see performance gains.

Multithreading

Warning

It is not possible to add threads dynamically, either we have to start Julia with the parameter -t (e.g. -t 8 or -t auto) in the command line or use the VSCode Julia extension setting Julia: Num Threads

julia> function inner(x)
           s = 0.0
           for i in 1:x
               for j in 1:i
                   if j%2 == 0
                       s += j
                   else
                       s -= j
                   end
               end
           end
           return s
       endinner (generic function with 1 method)
julia> function parentSingleThread(x,y) toTest = x .+ (1:y) out = zeros(length(toTest)) for i in 1:length(toTest) out[i] = inner(toTest[i]) end return out endparentSingleThread (generic function with 1 method)
julia> function parentThreaded(x,y) toTest = x .+ (1:y) out = zeros(length(toTest)) Threads.@threads for i in 1:length(toTest) out[i] = inner(toTest[i]) end return out endparentThreaded (generic function with 1 method)
julia> x = 100100
julia> y = 2020
julia> str = parentSingleThread(x,y)20-element Vector{Float64}: -51.0 0.0 -52.0 0.0 -53.0 0.0 -54.0 0.0 -55.0 0.0 -56.0 0.0 -57.0 0.0 -58.0 0.0 -59.0 0.0 -60.0 0.0
julia> mtr = parentThreaded(x,y)20-element Vector{Float64}: -51.0 0.0 -52.0 0.0 -53.0 0.0 -54.0 0.0 -55.0 0.0 -56.0 0.0 -57.0 0.0 -58.0 0.0 -59.0 0.0 -60.0 0.0
julia> str == mtr # truetrue
julia> Threads.nthreads() # 4 in my case4
julia> Threads.threadid()1
julia> @btime parentSingleThread(100,20) # 140 μs on my machine 114.282 μs (2 allocations: 224 bytes) 20-element Vector{Float64}: -51.0 0.0 -52.0 0.0 -53.0 0.0 -54.0 0.0 -55.0 0.0 -56.0 0.0 -57.0 0.0 -58.0 0.0 -59.0 0.0 -60.0 0.0
julia> @btime parentThreaded(100,20) # 47 μs 55.172 μs (24 allocations: 1.81 KiB) 20-element Vector{Float64}: -51.0 0.0 -52.0 0.0 -53.0 0.0 -54.0 0.0 -55.0 0.0 -56.0 0.0 -57.0 0.0 -58.0 0.0 -59.0 0.0 -60.0 0.0

Multiprocessing

NOTE The code on multiprocessing is commented out (not executed, so you don't see the output) as GitHub actions (that are used to run this code and render the web pages you are reading) have problems running multi-process functions:

using Distributed     # from the Standard Library
addprocs(3)           # 2,3,4

The first process is considered a sort of "master" process, the other ones are the "workers" We can add processes on other machines by providing the SSH connection details directly in the addprocs() call (Julia must be installed on those machines as well) We can alternatively start Julia directly with n worker processes using the argument -p n in the command line.

println("Worker pids: ")
for pid in workers()  # return a vector to the pids
    println(pid)      # 2,3,4
end
rmprocs(workers()[1])    #  remove process pid 2
println("Worker pids: ")
for pid in workers()
    println(pid) # 3,4 are left
end
@everywhere begin using Distributed end # this is needed only in GitHub action
@everywhere println(myid()) # 4,3

Run heavy tasks in parallel

using Distributed, BenchmarkTools
a = rand(1:35,100)
@everywhere function fib(n)
    if n == 0 return 0 end
    if n == 1 return 1 end
    return fib(n-1) + fib(n-2)
end

The macro @everywhere makes available the given function (or functions with @everywhere begin [shared function definitions] end or @everywhere include("sharedCode.jl")) to all the current workers.

result  = map(fib,a)

The pmap function ("parallel" map) automatically picks up the free processes, assigns them the job from the "input" array and merges the results in the returned array. Note that the order is preserved:

result2 = pmap(fib,a)
result == result2
@btime map(fib,$a)  # serialised:   median time: 514 ms    1 allocations
@btime pmap(fib,$a) # parallelized: median time: 265 ms 4220 allocations # the memory of `a` needs to be copied to all processes

Divide and Conquer

Rather than having a "heavy operation" and being interested in the individual results, here we have a "light" operation and we want to aggregate the results of the various computations using some aggregation function. We can then use @distributed (aggregationfunction) for [forConditions] macro:

using Distributed, BenchmarkTools
function f(n)   # our single-process benchmark
  s = 0.0
  for i = 1:n
    s += i/2
  end
    return s
end
function pf(n)
  s = @distributed (+) for i = 1:n # aggregate using sum on variable s
        i/2                        # the last element of the for cycle is used by the aggregator
  end
  return s
end
@btime  f(10000000) # median time: 11.1 ms   0 allocations
@btime pf(10000000) # median time:  5.7 ms 145 allocations

Note that also in this case the improvement is less than proportional with the number of processes we add

Details on parallel computation can be found in the official documentation, including information to run natively Julia on GPUs or TPUs.

View this file on Github.


This page was generated using Literate.jl.