A method to remove the use of runtime eval and invokelatest, as well as support closures in generated functions! Come with an implemented proptype

Hi, every one, we’ve made something very, very useful and powerful today(

You can check GG.jl for what we’ve provided.
It’s originally motivated by solving an issue of @cscherrer 's Soss.jl: https://github.com/cscherrer/Soss.jl/issues/23

What we’re bringing about today are

  1. the capability of creating functions in runtime without eval or introducing a world age problem.
  2. the capability of defining closures in generated functions.

Following sections are the article about how we achieved above expected capabilities. And the latest article can be found at https://github.com/thautwarm/GG.jl/blob/master/slides/GG.md.


Background

Generated Functions in Julia

Julia can only create functions statically. Accurately, apart from using the @generated macro(generated functions), we can create functions

  • literally for each function, or
  • by calling macros

These 2 means only work when building a module, however
once a module got built, no way to create a new function
in current world age.

However, there’re still cases that we have to generate functions
according to runtime data, hence Julia has provided a solution called
the generated function.

The name of the generated function doesn’t suggest its power. I used to introduce this to
the fans of DrRacket, they immediately responsed, “generating functions is trivial, exactly the most common sight in LISP idioms”. Though it was not their fault, they just tried to understand it literally, but I
do feel like to laugh them at the street :slight_smile:

In other dynamic languages, when we generate functions from runtime data, we must have following steps:

  • invoke the generator with runtime data as its parameters, and bind the expression to a symbol to represent the generated function.
    The generator might build an AST(abstract syntax tree) which represents the generated
    function, and compile/execute it to a runtime object.

  • calling the generated function at each expected callsite.

However, generated functions in Julia are more convenient, we only need to return the AST of the generated function, and compiler will do the remaining tasks for us.

This, Julia’s generated function, becomes extremely useful when we need a polymorphic generated function.

Polymorphisms can be achieved by generics or templates. The former one, will use the same code
to process parameters of various types, while the latter provides specialized code for each group
of the parameter types. Usually, the former is more dynamic, slow(for requiring runtime coercions) and limited(might disallow polymorphisms for specific types, like value types in Java), while the latter is
static, fast, flexible.

Julia’s generated function achieves polymorphism via templates, and it can take advantage of
static information to decide which specialized codes will be used in the callsite. Thus, we simply
achieve the free-lunch polymorphic generated functions, without manually managing the construction of each specialized function, or choosing a mechanism to decide which specialized function to use in the callsite.

Julia’s generated function is indeed a miracle, personally I’d call it Zero-Cost-Staging.

The Restrictions of Generated Function

The generated functions perform code generation based on the types of parameters.

function add(x::T, y::T)
    if T <: Number
        :(x + y)
    else if T <: AbstractString
        :(x * y) # '*' is string concatenation in Julia
    else
        throw("fatal")
    end
end

“Typeability”? What Can Functions Be Generated From?

The first restriction is, we can only generate functions based on typeable data.

Typeable here is not an official term, I use this to indicate the data
that can be held in the type parameters, you can roughly treat it as immutable, but
the scope of the former is smaller than the latter.

However, although Julia is a dynamic language,
a Julia type should be immutable datum. Which means
we cannot do code generation based on things like
Strings(until Julia 1.2.x), mutable states, etc.

However, this is not a challenge at all. In terms of
every domain specific problem, we can demonstrate a
form to express all non-typeable components
. Since that,
we can easily transform a non-typeable data to typeable
representations, and the reverse is also true.

Fortunately, this problem got solved when working with Chad for his
Soss.jl. We wanted to avoid
evaluating ASTs in runtime and using invokelastest, and we then
achieved it by holding something equivalent to AST of the expected
generated function in type parameters.

For instance, if we want to make Julia’s AST types typeable, there’s
a solution,

abstract type TypeLevel end
struct TLCons{Hd, Tl} <: TypeLevel end
struct TLNil <: TypeLevel end
struct TLVal{Val} <: TypeLevel end
struct TLSExp{Fn, Args} <: TypeLevel end

function typelevellist(l)
    foldr(l, init=TLNil) do each, prev
        TLCons{each, prev}
    end
end

function expr2typelevel(x)::Type{TypeLevel}
    # omit
end

function interpret(t::Type{TypeLevel})
    # omit
end

and you can check the whole implementation here.

Purity? What Can Generated Codes Be?

The generated codes cannot be impure(with side effects), but the compiler is really conservative.

Say, we cannot use loal functions…


julia> @generated f() = :(() -> 1)
f (generic function with 1 method)

julia> f()
ERROR: generated function body is not pure. this likely means it contains a closure or comprehension.
Stacktrace:
 [1] top-level scope at none:0

Currently, the generate code cannot be a function, nor a generator, which is clarified in the document of
Julia v1.1.1.

Due to an implementation limitation, this also means that they currently cannot define a closure or generator.

So this is still unresolved problem, and exactly is what we will mainly discuss in this article.

The Runtime Function Generation

Closure Conversions

In the sense of intuition, when we want to do what a specific function will do,
we’re considering constructing this function. However, it’s not the only manner.

Firstly let me simply introduce the closure conversions,

Given following codes,

function f(x, y)
    function g(z)
        x + z
    end
    g
end

We can find there’s a free variable x in the inner function g. To eliminate
free variables, if we presume

  • all free variables are readonly and,
  • the closure function isn’t recursive,
    we can introduce a structure(Closure) to represent a closure:
struct Closure{F, Free}
    frees :: Free
end

function global_g((x, ), z)
    x + z
end

function (closure::Closure{F, X})(args...; kwargs...) where {F, X}
    F(closure.frees, args...; kwargs...)
end

function f(x, y)
   Closure{global_g, typeof(x)}(x)
end

The automation of above transformations is very simple to implement,
if you already have tools to analyse the scopes.

Say, we can have such a function called scoping,

scoping(
    :(function f(x, y)
        function g(z)
            x + z
        end
    end)
) == Expr(
    :scope,
    (:x, :y, :g), # locals/bounds
    (),           # free variables
    (),           # globals
    inner_expr
)

And inside the inner_expr, for function g, we
will have a scope expression:

Expr(
    :scope,
    (:z, ), # bounds
    (:x, ), # free variable
    (),     # globals
    inner_expr2
)

Now, we point out that every scope expression can
produce one or more global function pointers by following
procedures:

  • if a scope expression contains no free variables, do nothing,
  • otherwise, for expression Expr(:scope, bounds, frees, globs, expr),
    we generate a closure structure with frees:
struct Closure{F, Free}
    frees :: Free
end

function (closure::Closure{F, X})(args...; kwargs...) where {F, X}
    F(closure.frees, args...; kwargs...)
end

function split_args_kwargs(args)
    i_ = findfirst(x -> Meta.isexpr(x, :parameter), args)
    i = i_ === nothing ? 0 : i_
    (args[i+1:end], args[1:i])
end

# impl for closure conversion
function mk_closure_static(expr, toplevel::Vector{Expr})
    rec(expr) = mk_closure_static(expr, toplevel)
    @match expr begin
        # main logic
        Expr(:scope, _, frees, _, inner_expr) =>
            let closure_arg = :($(frees...), ),
                name = "",
                args   = Symbol[]

                @match inner_expr begin
                    Expr(:function, :($name($(args...), )), body)            ||
                    # (a, b, c, ...) -> body / function (a, b, c, ...) body end
                    Expr(:-> || :function, Expr(:tuple, args...), body)      ||
                    # a -> body
                    Expr(:-> || :function, a::Symbol, body) && Do(args=[a])  =>
                        let glob_name   = gensym(name),
                            (args, kwargs) = split_args_kwargs(args),
                            body   = rec(body)

                            (fn_expr, ret) = if isempty(frees)
                                fn_expr = Expr(
                                    :function,
                                    :($glob_name($(args...); $(kwargs...))),
                                    body
                                )
                                (fn_expr, :glob_name)
                            else
                                fn_expr = Expr(
                                    :function,
                                    :($glob_name($closure_arg, $(args...); $(kwargs...))),
                                    body
                                )
                                ret = :(let frees = $closure_arg
                                    $Closure{$glob_name, typeof(frees)}(frees)
                                end)
                                (fn_expr, ret)
                            end

                            push!(toplevel, fn_expr)

                            if name == "" # anonymous function
                                ret
                            else
                                :($name = $glob_name)
                            end
                        end

                    _ => throw("unsupported closures")
                end
            end
        Expr(hd, tl...) => Expr(hd, map(rec, tl)...)
        a               => a
    end
end

function closure_conv(block)
    defs = Expr[]
    push!(defs, mk_closure_static(scoping(block), defs))
    Expr(:block, defs...)
end


macro closure_conv(block)
    closure_conv(block) |> esc
end

To support mutable free variables, we should capture the structure
where free variables are stored, it’s a bit internal.

To support self/mutual recursions, we can make Closure mutable,
and make the free variables wrapped in Ref:

function ...
    function g(x)
        do_some(g, x)
    end
end

Above codes can be statically transformed to

function glob_g((refg,), x)
    g = refg.x
    do_some(g, x)
end

function ...
    let refg = Ref{Closure}(),
        frees = (refg, ),
        closure = Closure{glob_x, typeof(frees)}(frees)
        refg.x = closure
        closure
    end
end

Until now, we don’t have such a scoping function in the community,
and it’s somewhat a little heavy assignment.

We’re now looking for people to implement this together.

Generating Functions From Typeable Data In Runtime

In this sub-section, we’ll introduce a method equivalently powerful as eval
but without a world age problem.

Our main goal is to allow closures in generated functions,
which is performing runtime code generation.

Above techniques(closure conversions) are purely static, thus useless to our goal.

However, we propose a type to achieve generating non-closure functions
in runtime:

struct RuntimeFn{Args, Kwargs, Body} end

where both Args and Kwargs are typeable representations of
the ASTs that represent arguments,
and Body is a typeable representation of Julia AST.

At here, we’ll use the TypeLevel mentioned above to represent Body.

Then comes one of key ideas:

using Parameters
@generated function (::RuntimeFn{Args, Kwargs, Body})(args...; kwargs...) where {Args, Kwargs, Body}
    args_ = interpret(Args)
    kwargs_ = interpret(Kwargs)
    body = interpret(Body)
    quote
        $args_ = args
        @unpack $kwargs_ = kwargs
        $body
    end
end

args = expr2typelevel(:(x, y))
kwargs = expr2typelevel(:())
body = expr2typelevel(:(x + y))
fn = RuntimeFn{args, kwargs, body}()

fn(1, 2) # => 3

Here comes the runtime generations of functions!

From now on, no need for eval and invokelatest!

Closure Conversions For Generated Functions

Now things have got clarified! Since we can already generate non-closure functions
from arbitrary typeable data in runtime, we can then perform closure conversions
based on runtime generated non-closure functions, instead of generating static top
level(global scope) functions.

Following implementation will support closures in generated functions,
when no default arguments used.

function closure_conv_staged(expr)
    rec = closure_conv_staged
    @match expr begin
        # main logic
        Expr(:scope, _, frees, _, inner_expr) =>
            let closure_arg = Expr(:tuple, frees...),
                name = "",
                args   = Symbol[]
                @match inner_expr begin
                    Expr(:function, :($name($(args...), )), body)            ||
                    # (a, b, c, ...) -> body / function (a, b, c, ...) body end
                    Expr(:-> || :function, Expr(:tuple, args...), body)      ||
                    # a -> body
                    Expr(:-> || :function, a::Symbol, body) && Do(args=[a])  =>
                        let (args, kwargs) = split_args_kwargs(args),
                            body   = rec(body),
                            kwargs = map(x -> x.args[1], kwargs)
                            Kwargs = expr2typelevel(Expr(:tuple, kwargs...))
                            Body   = expr2typelevel(body)
                            if isempty(frees)
                                Args = expr2typelevel(Expr(:tuple, args...))
                                RuntimeFn{Args, Kwargs, Body}()
                            else
                                Args = expr2typelevel(Expr(:tuple, closure_arg, args...))
                                non_closure_fn = RuntimeFn{Args, Kwargs, Body}()
                                ret = :(let frees = $closure_arg
                                    $Closure{$non_closure_fn, typeof(frees)}(frees)
                                end)
                                if name == "" # anonymous function
                                    ret
                                else
                                    :($name = $ret)
                                end
                            end
                        end
                    _ => throw("unsupported closures")
                end
            end
        Expr(hd, tl...) => Expr(hd, map(rec, tl)...)
        a               => a
    end
end

The use is simple:

function gg(x)
    closure_conv_staged(scoping(x))
end

@generated function f(x)
    quote
        () -> x + 1
    end |> gg
end

Currently, what we only lack of is the implementation of scoping, and
making it is expected to cost a few days. However, for prototyping, we
can simply the cases, by peforming explicit capturing.

In our prototype, we use following notations to express a closure function,
which looks pretty similar to those in C++:

# x, y is free variables
[x, y](a) ->  x*(a + y)
[](a) -> a

In our test cases, you can find out codes that look like

@generated function f(x)
    quote
        [x](a ) ->  x + a
    end |> gg
end
@test f(1)(2) == 3
14 Likes

The support of closures in generated function part is reasonable since it’s an implemention limitation anyway and to hash the type into the type is exactly what Jameson mentioned a long time ago (a year?) to make it compatible with the runtime.

1 Like

Well, I pondered for quite a while and just don’t know how to response…
At least I think my prototype does help to solving my friend’s issue, which is enough to me. No matter if the method is brand-new, I enjoyed the process of working it out.
I think there should be somewhere for you core devs to post the more advanced problems of various topics. It’s always difficult to find out your so many previous conclusions in the internet if you don’t assembly them.

7 Likes

@thautwarm is being kind in using “we” a lot in her description of the solution, so I think some context is helpful.

I’ve struggled with the right way to get around eval/invokelatest since very early in my work on Soss (first commit December 2017). At JuliaCon 2019 I learned the several others were facing this same problem, as discussed here.

@mohamed82008 had previously suggested to me to encode my entire model in the type in order to use generated functions, but I couldn’t quite figure out how to get it to work. And @jpfairbanks had a similar suggestion for encoding algebraic manipulations as types. [We had been joking around and I thought he was trolling me, but now it doesn’t seem so infeasible].

Anyway, even if generated functions had supported closures from the beginning, it wouldn’t have been clear to me how to take advantage of this. So the detailed write-up above is really helpful. Really, my part in GG.jl so far has been to provide an interesting problem to work on :slight_smile:

2 Likes

Thanks Chad. The discussion about getting around eval/invokelatest is really meaningful. We should make clear what is necessary to do to achieve the goal.

Glad to know several kind people have suggested you to encode you model in the type. Fortunately, we now can just encode the julia AST in the type(via expr2typelevel in GG.jl, and I’ll explain this to you later), thus the existing code(exactly how your Model gets transformed to julia AST) doesn’t need to change.

Really, my part in GG.jl so far has been to provide an interesting problem to work on

No worries… The 2 core ideas of GG.jl are not as complicated as your statistic models…

2 Likes

I think we are already using this trick in Zygote in order to create closure for SSA, since you can’t have closure there. FYI: https://github.com/FluxML/Zygote.jl/blob/master/src/compiler/interface2.jl

Or you can check my blog post:

2 Likes

Wow, this is great! It could also be a playground for possible Julia features/extensions, not just “workaround.” For example, I’ve been wanting something like

function f(x)
    const y = compute(x)
    a -> y + a
end

to communicate with compiler that captured variable won’t change so that it does not have to box it (I know FastClosures.jl but I think “local const” will be more clear about the intent and avoid issue like this). I suppose it’s possible to do this in GG.jl?

A bit tangent, but since you are talking about Racket… There is Hackett that combines Haskell’s type system and Racket’s macro system. IIRC, this talk by Alexis King explains the idea and difficulty of the interaction of type inference and metaprogramming. It reminded me a lot of Julia’s @generated function.

3 Likes

This is part of the task of porting the lowering code from femtolisp into julia which I have started over at https://github.com/JuliaLang/julia/pull/32201. It’s a big job to do it all, but a working approximation may not be too hard. I had to do some of the same kind of variable analysis in FastClosures, but there are some inherent limitations to that approach as @tkf points out.

If anyone would like to collaborate on converting lowering into julia that would be great. One approach would be to take that PR out of Base and into a package for easier collaboration. Then aim to get some of the tooling committed to Base in the short term so we can more easily call into the flisp code for testing.

In terms of the flisp code, I think variable analysis is related to passes 2,3 in julia-syntax.scm, though I haven’t read this code yet. Pass 4 may also be relevant:

As part of porting lowering, it would be interesting to know which APIs people would like to introspect the lowering passes. For one thing, I think it would be useful to have @code_desugared which would remove syntactic sugar without converting code to linearized IR. The desugared code is still hierarchical and can be easier to read than the fully linearized code. It would also be handy to have an API which can somehow tell you about the use of variables at different points in the code (which is what you want for this project).

5 Likes

Thanks for your kind words. In fact what you want is partly implemented in GG.jl.
The mk_function api can make a function in runtime, and you can insert julia objects to be the default values of function arguments(but now only works for keyword arguments).

y = compute(x)
f = mk_function(:(a, ), :(y = $y, ), :(y + a))

When you invoke f without keyword arguments, the generated code is exactly:

quote
   let  (a, ) = args, # not sure but feel like this line can be optimized
        y = $y,
        y + a
   end
end
1 Like

Your works should be greatly appreciated, and I need time to digest what you mentioned.

In fact I have some experience with the scoping analysis in programming languages like Python, several ML-dialects. I do have some tools/ideas to handle this sort of tasks, but after I dipped into your PR and the referenced flisp files, I found the stuffs really complicated…

I need more time to digest it.

1 Like

How does it compare to https://github.com/yuyichao/FunctionWrappers.jl in terms of performance and types of functions allowed?

1 Like

I don’t think Yuyichao’s FunctionWrapper.jl has much overlap with GG.jl.
For what I’m concerned, his package provides the structutal function types like what in statically typed languages, which is tramendously essential for it provides the capability of polymorphisms among function types:

map(f::FunctionWrapper{R, Tuple{T}}, c::Vector{T}) :: Vector{R} = ...
# which ends the awkward scene of `map(f, [])`

However I’ve heard that it could wrap the function in other world age, so far working well along with eval.

While GG.jl can remove the use of eval and make functions via runtime Julia Expe data(ASTs).

Also, GG.jl has no efficiency cost if the baseline is a native generated function with same semantics defined in global scope/top level of a module:

julia> using GG; using BenchmarkTools
julia> struct D end
julia> @generated (::D)(x, y) = :(x + y)
julia> fd = D();
julia> fg = mk_function(:(x, y), :(), :(x + y));
julia> fd(1, 2); fg(1, 2); # compile for the first time
julia> @btime fd(1, 2)
  13.203 ns (0 allocations: 0 bytes)
3
julia> @btime fg(1, 2)
  14.003 ns (0 allocations: 0 bytes)
3
julia> @code_llvm fd(1, 2)
#=
;  @ REPL[4]:1 within `D'
define i64 @julia_D_12305(i64, i64) {
top:
; ┌ @ REPL[4]:1 within `macro expansion'
; │┌ @ int.jl:53 within `+'
    %2 = add i64 %1, %0
; │└
   ret i64 %2
; └
}
=#
julia> @code_llvm fg(1, 2)
#=
;  @ /home/redbq/github2/GG/src/closure_conv.jl:77 within `RuntimeFn'
define i64 @julia_RuntimeFn_12309(i64, i64) {
top:
; ┌ @ /home/redbq/github2/GG/src/closure_conv.jl:81 within `macro expansion'
; │┌ @ int.jl:53 within `+'
    %2 = add i64 %1, %0
; │└
   ret i64 %2
; └
}
=#

EDIT: Due to my implementation limitation(you know now GG.jl is a proof of concepts), some forms of function definition(annotations or specification of return type) are not allowed.

Even at global scope you can make this approach very fast with const:

julia> f = mk_function(:(x, y), :(), :(x + y))
RuntimeFn{GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLCons{GG.TLVal{:x},GG.TLCons{GG.TLVal{:y},GG.TLNil}}}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLNil}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:call},GG.TLCons{GG.TLVal{:+},GG.TLCons{GG.TLVal{:x},GG.TLCons{GG.TLVal{:y},GG.TLNil}}}}}}()

julia> const g = mk_function(:(x, y), :(), :(x + y))
RuntimeFn{GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLCons{GG.TLVal{:x},GG.TLCons{GG.TLVal{:y},GG.TLNil}}}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLNil}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:call},GG.TLCons{GG.TLVal{:+},GG.TLCons{GG.TLVal{:x},GG.TLCons{GG.TLVal{:y},GG.TLNil}}}}}}()

julia> @btime f(1,2)
  235.405 ns (1 allocation: 32 bytes)
3

julia> @btime g(1,2)
  0.017 ns (0 allocations: 0 bytes)
3

julia> @btime 1+2
  0.017 ns (0 allocations: 0 bytes)
3
1 Like

Wow, I don’t know this before! Makes a lot of sense to me!

EDIT:
After pondering for a while I think it might not be very useful. I think it’s caused by inlining, but when
things get really dynamic, inlining might not work.

Not sure, can any one answer this?

Right, in previous tests I had thought it’s a bit slow, but I think this was just an issue with global scope. const removes this, but you mostly wouldn’t really use it this way. I think a more realistic benchmark builds code and calls it in the same world age. Hopefully using it in Soss will also give us some more insight into corner cases.

1 Like

@cscherrer

Haha, I figured it out, and it seems that GG.jl is soooo fast:

julia> seq = 1:1000;

julia> apply1 = mk_function(:(x, ), :(), :(2x + 1))
RuntimeFn{GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLCons{GG.TLVal{:x},GG.TLNil}}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLNil}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:call},GG.TLCons{GG.TLVal{:+},GG.TLCons{GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:call},GG.TLCons{GG.TLVal{:*},GG.TLCons{GG.TLVal{2},GG.TLCons{GG.TLVal{:x},GG.TLNil}}}}},GG.TLCons{GG.TLVal{1},GG.TLNil}}}}}}()

julia> apply2 = x -> 2x + 1
#8 (generic function with 1 method)

julia> @btime map(apply1, seq);
  635.250 ns (1 allocation: 7.94 KiB)

julia> @btime map(apply2, seq);
  724.568 ns (4 allocations: 8.03 KiB)

julia> @btime map(apply2, seq);
  704.625 ns (4 allocations: 8.03 KiB)

julia> @btime map(apply1, seq);
  674.888 ns (1 allocation: 7.94 KiB)


EDIT: named functions or type annotated functions are still slower than mk_function. Weird.

1 Like

Interesting. Since you move eval up to type-domain, evaling a function is equivalent to making a closure? This is neat.

BTW, you don’t have to use const to benchmark it. You could use $ to interpolate variables that are in the global scope.

julia> using BenchmarkTools

julia> f = mk_function(:(x, y), :(), :(x + y))
RuntimeFn{GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLCons{GG.TLVal{:x},GG.TLCons{GG.TLVal{:y},GG.TLNil}}}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:tuple},GG.TLNil}},GG.TLSExp{GG.TLVal{Expr},GG.TLCons{GG.TLVal{:call},GG.TLCons{GG.TLVal{:+},GG.TLCons{GG.TLVal{:x},GG.TLCons{GG.TLVal{:y},GG.TLNil}}}}}}()

julia> @btime f(1, 2)
  17.372 ns (0 allocations: 0 bytes)
3

julia> @btime $f(1, 2)
  0.026 ns (0 allocations: 0 bytes)
3

Also, be aware that sub-nano timing is almost certainly a fluke because one CPU cycle is about 0.25 ns already. You could try to use the Ref trick to correctly benchmark it.

julia> @btime $f(Ref(1)[], 2)
  1.277 ns (0 allocations: 0 bytes)
3

julia> @btime Ref(1)[] + 2
  1.278 ns (0 allocations: 0 bytes)
3

Right. This has been confusing to me, determining the best way to measure things in a way that will be representative of a given use case. I was concerned about speed when calling from global scope, in case the function is used in that way.

I don’t know this trick, where can I read more?

I am not sure if it is documented anywhere, but you could do

julia> @btime $f(x, y) setup=(x=rand(Int); y=rand(Int))
  1.278 ns (0 allocations: 0 bytes)
5287131514655690321

, too.

The manual of BenchmarkTools.jl is here.

1 Like