Skip to content

Commit

Permalink
implement compilerbarrier builtin (JuliaLang#46432)
Browse files Browse the repository at this point in the history
This builtin is useful to control compiler behavior.
It could be considered as a more robust and generalized version of `inferencebarrier`.

I scratched the following docstring for `compilerbarrier`, that hopefully
explains its purpose.

    Base.compilerbarrier(setting::Symbol, val)

This function puts a barrier at a specified compilation phase.
It is supposed to only influence the compilation behavior according to `setting`,
and its runtime semantics is just to return the second argument `val` (except that
this function will perform additional checks on `setting` in a case when `setting`
isn't known precisely at compile-time.)

Currently either of the following `setting`s is allowed:
- Barriers on abstract interpretation:
* `:type`: the return type of this function call will be inferred as `Any` always
(the strongest barrier on abstract interpretation)
* `:const`: the return type of this function call will be inferred with widening
constant information on `val`
* `:conditional`: the return type of this function call will be inferred with widening
conditional information on `val` (see the example below)
- Any barriers on optimization aren't implemented yet

!!! note
    This function is supposed to be used _with `setting` known precisely at compile-time_.
    Note that in a case when the `setting` isn't known precisely at compile-time, the compiler
    currently will put the most strongest barrier(s) rather than emitting a compile-time warning.

\# Examples

```julia
julia> Base.return_types((Int,)) do a
       x = compilerbarrier(:type, a) # `x` won't be inferred as `x::Int`
       return x
   end |> only
Any

julia> Base.return_types() do
       x = compilerbarrier(:const, 42)
       if x == 42 # no constant information here, so inference also accounts for the else branch
           return x # but `x` is still inferred as `x::Int` at least here
       else
           return nothing
       end
   end |> only
Union{Nothing, Int64}

julia> Base.return_types((Union{Int,Nothing},)) do a
       if compilerbarrier(:conditional, isa(a, Int))
           # the conditional information `a::Int` isn't available here (leading to less accurate return type inference)
           return a
       else
           return nothing
       end
   end |> only
Union{Nothing, Int64}
```

As a result, `Base.inferencebarrier` is now defined as
```julia
inferencebarrier(@nospecialize(x)) = compilerbarrier(:type, x)
```
  • Loading branch information
aviatesk committed Aug 23, 2022
1 parent 80e50b5 commit df3da05
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 9 deletions.
12 changes: 12 additions & 0 deletions base/compiler/ssair/inlining.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,18 @@ function early_inline_special_case(
end
end
end
if f === compilerbarrier
# check if this `compilerbarrier` has already imposed a barrier on abstract interpretation
# so that it can be eliminated here
length(argtypes) == 3 || return nothing
setting = argtypes[2]
isa(setting, Const) || return nothing
setting = setting.val
isa(setting, Symbol) || return nothing
setting === :const || setting === :conditional || setting === :type || return nothing
# barrierred successfully already, eliminate it
return SomeCase(stmt.args[3])
end
return nothing
end

Expand Down
17 changes: 17 additions & 0 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,23 @@ add_tfunc(atomic_pointerswap, 3, 3, (a, v, order) -> (@nospecialize; pointer_elt
add_tfunc(atomic_pointermodify, 4, 4, atomic_pointermodify_tfunc, 5)
add_tfunc(atomic_pointerreplace, 5, 5, atomic_pointerreplace_tfunc, 5)
add_tfunc(donotdelete, 0, INT_INF, (@nospecialize args...)->Nothing, 0)
function compilerbarrier_tfunc(@nospecialize(setting), @nospecialize(val))
# strongest barrier if a precise information isn't available at compiler time
# XXX we may want to have "compile-time" error instead for such case
isa(setting, Const) || return Any
setting = setting.val
isa(setting, Symbol) || return Any
if setting === :const
return widenconst(val)
elseif setting === :conditional
return widenconditional(val)
elseif setting === :type
return Any
else
return Bottom
end
end
add_tfunc(compilerbarrier, 2, 2, compilerbarrier_tfunc, 5)
add_tfunc(Core.finalizer, 2, 4, (@nospecialize args...)->Nothing, 5)

# more accurate typeof_tfunc for vararg tuples abstract only in length
Expand Down
65 changes: 61 additions & 4 deletions base/docs/basedocs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3061,7 +3061,7 @@ See also [`"`](@ref \")
kw"\"\"\""

"""
donotdelete(args...)
Base.donotdelete(args...)
This function prevents dead-code elimination (DCE) of itself and any arguments
passed to it, but is otherwise the lightest barrier possible. In particular,
Expand All @@ -3078,9 +3078,10 @@ This is intended for use in benchmarks that want to guarantee that `args` are
actually computed. (Otherwise DCE may see that the result of the benchmark is
unused and delete the entire benchmark code).
**Note**: `donotdelete` does not affect constant folding. For example, in
`donotdelete(1+1)`, no add instruction needs to be executed at runtime and
the code is semantically equivalent to `donotdelete(2).`
!!! note
`donotdelete` does not affect constant folding. For example, in
`donotdelete(1+1)`, no add instruction needs to be executed at runtime and
the code is semantically equivalent to `donotdelete(2).`
# Examples
Expand All @@ -3097,6 +3098,62 @@ end
"""
Base.donotdelete

"""
Base.compilerbarrier(setting::Symbol, val)
This function puts a barrier at a specified compilation phase.
It is supposed to only influence the compilation behavior according to `setting`,
and its runtime semantics is just to return the second argument `val` (except that
this function will perform additional checks on `setting` in a case when `setting`
isn't known precisely at compile-time.)
Currently either of the following `setting`s is allowed:
- Barriers on abstract interpretation:
* `:type`: the return type of this function call will be inferred as `Any` always
(the strongest barrier on abstract interpretation)
* `:const`: the return type of this function call will be inferred with widening
constant information on `val`
* `:conditional`: the return type of this function call will be inferred with widening
conditional information on `val` (see the example below)
- Any barriers on optimization aren't implemented yet
!!! note
This function is supposed to be used _with `setting` known precisely at compile-time_.
Note that in a case when the `setting` isn't known precisely at compile-time, the compiler
currently will put the most strongest barrier(s) rather than emitting a compile-time warning.
# Examples
```julia
julia> Base.return_types((Int,)) do a
x = compilerbarrier(:type, a) # `x` won't be inferred as `x::Int`
return x
end |> only
Any
julia> Base.return_types() do
x = compilerbarrier(:const, 42)
if x == 42 # no constant information here, so inference also accounts for the else branch
return x # but `x` is still inferred as `x::Int` at least here
else
return nothing
end
end |> only
Union{Nothing, Int64}
julia> Base.return_types((Union{Int,Nothing},)) do a
if compilerbarrier(:conditional, isa(a, Int))
# the conditional information `a::Int` isn't available here (leading to less accurate return type inference)
return a
else
return nothing
end
end |> only
Union{Nothing, Int64}
```
"""
Base.compilerbarrier

"""
Core.finalizer(f, o)
Expand Down
5 changes: 2 additions & 3 deletions base/essentials.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This file is a part of Julia. License is MIT: https://julialang.org/license

using Core: CodeInfo, SimpleVector, donotdelete, arrayref
import Core: CodeInfo, SimpleVector, donotdelete, compilerbarrier, arrayref

const Callable = Union{Function,Type}

Expand Down Expand Up @@ -846,8 +846,7 @@ function invoke_in_world(world::UInt, @nospecialize(f), @nospecialize args...; k
return Core._call_in_world(world, Core.kwfunc(f), kwargs, f, args...)
end

# TODO: possibly make this an intrinsic
inferencebarrier(@nospecialize(x)) = RefValue{Any}(x).x
inferencebarrier(@nospecialize(x)) = compilerbarrier(:type, x)

"""
isempty(collection) -> Bool
Expand Down
1 change: 1 addition & 0 deletions src/builtin_proto.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ DECLARE_BUILTIN(_typebody);
DECLARE_BUILTIN(typeof);
DECLARE_BUILTIN(_typevar);
DECLARE_BUILTIN(donotdelete);
DECLARE_BUILTIN(compilerbarrier);
DECLARE_BUILTIN(getglobal);
DECLARE_BUILTIN(setglobal);
DECLARE_BUILTIN(finalizer);
Expand Down
14 changes: 14 additions & 0 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,19 @@ JL_CALLABLE(jl_f_donotdelete)
return jl_nothing;
}

JL_CALLABLE(jl_f_compilerbarrier)
{
JL_NARGS(compilerbarrier, 2, 2);
JL_TYPECHK(compilerbarrier, symbol, args[0])
jl_sym_t *setting = (jl_sym_t*)args[0];
if (!(setting == jl_symbol("type") ||
setting == jl_symbol("const") ||
setting == jl_symbol("conditional")))
jl_error("The first argument of `compilerbarrier` must be either of `:type`, `:const` or `:conditional`.");
jl_value_t *val = args[1];
return val;
}

JL_CALLABLE(jl_f_finalizer)
{
// NOTE the compiler may temporarily insert additional argument for the later inlining pass
Expand Down Expand Up @@ -1983,6 +1996,7 @@ void jl_init_primitives(void) JL_GC_DISABLED
jl_builtin__typebody = add_builtin_func("_typebody!", jl_f__typebody);
add_builtin_func("_equiv_typedef", jl_f__equiv_typedef);
jl_builtin_donotdelete = add_builtin_func("donotdelete", jl_f_donotdelete);
jl_builtin_compilerbarrier = add_builtin_func("compilerbarrier", jl_f_compilerbarrier);
add_builtin_func("finalizer", jl_f_finalizer);

// builtin types
Expand Down
1 change: 1 addition & 0 deletions src/codegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,7 @@ static const auto &builtin_func_map() {
{ jl_f_arraysize_addr, new JuliaFunction{XSTR(jl_f_arraysize), get_func_sig, get_func_attrs} },
{ jl_f_apply_type_addr, new JuliaFunction{XSTR(jl_f_apply_type), get_func_sig, get_func_attrs} },
{ jl_f_donotdelete_addr, new JuliaFunction{XSTR(jl_f_donotdelete), get_donotdelete_sig, get_donotdelete_func_attrs} },
{ jl_f_compilerbarrier_addr, new JuliaFunction{XSTR(jl_f_compilerbarrier), get_func_sig, get_func_attrs} },
{ jl_f_finalizer_addr, new JuliaFunction{XSTR(jl_f_finalizer), get_func_sig, get_func_attrs} }
};
return builtins;
Expand Down
5 changes: 3 additions & 2 deletions src/staticdata.c
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ extern "C" {
// TODO: put WeakRefs on the weak_refs list during deserialization
// TODO: handle finalizers

#define NUM_TAGS 155
#define NUM_TAGS 156

// An array of references that need to be restored from the sysimg
// This is a manually constructed dual of the gvars array, which would be produced by codegen for Julia code, for C.
Expand Down Expand Up @@ -253,6 +253,7 @@ jl_value_t **const*const get_tags(void) {
INSERT_TAG(jl_builtin_ifelse);
INSERT_TAG(jl_builtin__typebody);
INSERT_TAG(jl_builtin_donotdelete);
INSERT_TAG(jl_builtin_compilerbarrier);
INSERT_TAG(jl_builtin_getglobal);
INSERT_TAG(jl_builtin_setglobal);
// n.b. must update NUM_TAGS when you add something here
Expand Down Expand Up @@ -313,7 +314,7 @@ static const jl_fptr_args_t id_to_fptrs[] = {
&jl_f_applicable, &jl_f_invoke, &jl_f_sizeof, &jl_f__expr, &jl_f__typevar,
&jl_f_ifelse, &jl_f__structtype, &jl_f__abstracttype, &jl_f__primitivetype,
&jl_f__typebody, &jl_f__setsuper, &jl_f__equiv_typedef, &jl_f_get_binding_type,
&jl_f_set_binding_type, &jl_f_opaque_closure_call, &jl_f_donotdelete,
&jl_f_set_binding_type, &jl_f_opaque_closure_call, &jl_f_donotdelete, &jl_f_compilerbarrier,
&jl_f_getglobal, &jl_f_setglobal, &jl_f_finalizer,
NULL };

Expand Down
61 changes: 61 additions & 0 deletions test/compiler/inference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4123,3 +4123,64 @@ end)[2] == Union{}
@time 1
end
end)[2] == Union{}

# compilerbarrier builtin
import Core: compilerbarrier
# runtime semantics
for setting = (:type, :const, :conditional)
@test compilerbarrier(setting, 42) == 42
@test compilerbarrier(setting, :sym) == :sym
end
@test_throws ErrorException compilerbarrier(:nonexisting, 42)
@test_throws TypeError compilerbarrier("badtype", 42)
@test_throws ArgumentError compilerbarrier(:nonexisting, 42, nothing)
# barrier on abstract interpretation
@test Base.return_types((Int,)) do a
x = compilerbarrier(:type, a) # `x` won't be inferred as `x::Int`
return x
end |> only === Any
@test Base.return_types() do
x = compilerbarrier(:const, 42)
if x == 42 # no constant information here, so inference also accounts for the else branch (leading to less accurate return type inference)
return x # but `x` is still inferred as `x::Int` at least here
else
return nothing
end
end |> only === Union{Int,Nothing}
@test Base.return_types((Union{Int,Nothing},)) do a
if compilerbarrier(:conditional, isa(a, Int))
# the conditional information `a::Int` isn't available here (leading to less accurate return type inference)
return a
else
return nothing
end
end |> only === Union{Int,Nothing}
@test Base.return_types((Symbol,Int)) do setting, val
compilerbarrier(setting, val)
end |> only === Any # XXX we may want to have "compile-time" error for this instead
for setting = (:type, :const, :conditional)
# a successful barrier on abstract interpretation should be eliminated at the optimization
@test @eval fully_eliminated((Int,)) do a
compilerbarrier($(QuoteNode(setting)), 42)
end
end

# https://github.com/JuliaLang/julia/issues/46426
@noinline Base.@assume_effects :nothrow typebarrier() = Base.inferencebarrier(0.0)
@noinline Base.@assume_effects :nothrow constbarrier() = Base.compilerbarrier(:const, 0.0)
let src = code_typed1() do
typebarrier()
end
@test any(isinvoke(:typebarrier), src.code)
@test Base.return_types() do
typebarrier()
end |> only === Any
end
let src = code_typed1() do
constbarrier()
end
@test any(isinvoke(:constbarrier), src.code)
@test Base.return_types() do
constbarrier()
end |> only === Float64
end

0 comments on commit df3da05

Please sign in to comment.