Skip to content

Commit 02c46d7

Browse files
committed
introduce @noinfer macro to tell the compiler to avoid excess inference
This commit introduces new compiler annotation named `@noinfer`, which requests the compiler to avoid excess inference. In order to discuss `@noinfer`, it would help a lot to understand the behavior of `@nospecialize`. Its docstring says simply: > This is only a hint for the compiler to avoid excess code generation. More specifically, it works by _suppressing dispatches_ with complex runtime types of the annotated arguments. This could be understood with the example below: ```julia julia> function invokef(f, itr) local r = 0 r += f(itr[1]) r += f(itr[2]) r += f(itr[3]) r end; julia> _isa = isa; # just for the sake of explanation, global variable to prevent inling julia> f(a) = _isa(a, Function); julia> g(@nospecialize a) = _isa(a, Function); julia> dispatchonly = Any[sin, muladd, nothing]; # untyped container can cause excessive runtime dispatch julia> @code_typed invokef(f, dispatchonly) CodeInfo( 1 ─ %1 = π (0, Int64) │ %2 = Base.arrayref(true, itr, 1)::Any │ %3 = (f)(%2)::Any │ %4 = (%1 + %3)::Any │ %5 = Base.arrayref(true, itr, 2)::Any │ %6 = (f)(%5)::Any │ %7 = (%4 + %6)::Any │ %8 = Base.arrayref(true, itr, 3)::Any │ %9 = (f)(%8)::Any │ %10 = (%7 + %9)::Any └── return %10 ) => Any julia> @code_typed invokef(g, dispatchonly) CodeInfo( 1 ─ %1 = π (0, Int64) │ %2 = Base.arrayref(true, itr, 1)::Any │ %3 = invoke f(%2::Any)::Any │ %4 = (%1 + %3)::Any │ %5 = Base.arrayref(true, itr, 2)::Any │ %6 = invoke f(%5::Any)::Any │ %7 = (%4 + %6)::Any │ %8 = Base.arrayref(true, itr, 3)::Any │ %9 = invoke f(%8::Any)::Any │ %10 = (%7 + %9)::Any └── return %10 ) => Any ``` The calls of `f` remain to be `:call` expression (thus dispatched and compiled at runtime) while the calls of `g` are resolved as `:invoke` expressions. This is because `@nospecialize` requests the compiler to give up compiling `g` with concrete argument types but with precisely declared argument types, and in this way `invokef(g, dispatchonly)` will avoid runtime dispatches and accompanying JIT compilations (i.e. "excess code generation"). The problem here is, it influences dispatch only, does not intervene into inference in anyway. So there is still a possibility of "excess inference" when the compiler sees a considerable complexity of argument types during inference: ```julia julia> withinfernce = tuple(sin, muladd, "foo"); # typed container can cause excessive inference julia> @time @code_typed invokef(f, withinfernce); 0.000812 seconds (3.77 k allocations: 217.938 KiB, 94.34% compilation time) julia> @time @code_typed invokef(g, withinfernce); 0.000753 seconds (3.77 k allocations: 218.047 KiB, 92.42% compilation time) ``` The purpose of this PR is basically to provide a more drastic way to avoid excess compilation. Here are some ideas to implement the functionality: 1. make `@nospecialize` avoid inference also 2. add noinfer effect when `@nospecialize`d method is annotated as `@noinline` also 3. implement as `@pure`-like boolean annotation to request noinfer effect on top of `@nospecialize` 4. implement as annotation that is orthogonal to `@nospecialize` After trying 1 ~ 3., I decided to submit 3. for now, because I think the interface is ready to be experimented. This is almost same as what Jameson has done at <vtjnash@8ab7b6b>. It turned out that this approach performs very badly because some of `@nospecialize`'d arguments still need inference to perform reasonably. For example, it's obvious that the following definition of `getindex(@nospecialize(t::Tuple), i::Int)` would perform very badly if `@nospecialize` blocks inference, because of a lack of useful type information for succeeding optimizations: <https://github.com/JuliaLang/julia/blob/12d364e8249a07097a233ce7ea2886002459cc50/base/tuple.jl#L29-L30> The important observation is that we often use `@nospecialize` even when we expect inference to forward type and constant information. Adversely, we may be able to exploit the fact that we usually don't expect inference to forward information to a callee when we annotate it as `@noinline`. So the idea is to enable the inference suppression when `@nospecialize`'d method is annotated as `@noinline` also. It's a reasonable choice, and could be implemented efficiently after <#41922>. But it sounds a bit weird to me to associate no infer effect with `@noinline`, and I also think there may be some cases we want to inline a method while _partially_ avoiding inference, e.g.: ```julia @noinline function twof(@nospecialize(f), n) # we really want not to inline this method body ? if occursin('+', string(typeof(f).name.name::Symbol)) 2 + n elseif occursin('*', string(typeof(f).name.name::Symbol)) 2n else zero(n) end end ``` So this is what this commit implements. It basically replaces the previous `@noinline` flag with newly-introduced annotation named `@noinfer`. It's still associated with `@nospecialize` and it only has effect when used together with `@nospecialize`, but now it's not associated to `@noinline` at least, and it would help us reason about the behavior of `@noinfer` and experiment its effect more reliably: ```julia Base.@noinfer function twof(@nospecialize(f), n) # the compiler may or not inline this method if occursin('+', string(typeof(f).name.name::Symbol)) 2 + n elseif occursin('*', string(typeof(f).name.name::Symbol)) 2n else zero(n) end end ``` Actually, we can have `@nospecialize` and `@noinfer` separately, and it would allow us to configure compilation strategies in a more fine-grained way. ```julia function noinfspec(Base.@noinfer(f), @nospecialize(g)) ... end ``` I'm fine with this approach, if initial experiments show `@noinfer` is useful.
1 parent 1843201 commit 02c46d7

File tree

15 files changed

+250
-32
lines changed

15 files changed

+250
-32
lines changed

base/compiler/abstractinterpretation.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
347347
add_remark!(interp, sv, "Refusing to infer into `depwarn`")
348348
return MethodCallResult(Any, false, false, nothing)
349349
end
350+
if is_noinfer(method)
351+
sig = get_nospecialize_sig(method, sig, sparams)
352+
end
350353
topmost = nothing
351354
# Limit argument type tuple growth of functions:
352355
# look through the parents list to see if there's a call to the same method
@@ -593,7 +596,11 @@ function maybe_get_const_prop_profitable(interp::AbstractInterpreter, result::Me
593596
end
594597
end
595598
force |= allconst
596-
mi = specialize_method(match; preexisting=!force)
599+
if is_noinfer(method)
600+
mi = specialize_method_noinfer(match; preexisting=!force)
601+
else
602+
mi = specialize_method(match; preexisting=!force)
603+
end
597604
if mi === nothing
598605
add_remark!(interp, sv, "[constprop] Failed to specialize")
599606
return nothing

base/compiler/ssair/inlining.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@ end
803803

804804
function analyze_method!(match::MethodMatch, atypes::Vector{Any},
805805
state::InliningState, @nospecialize(stmttyp), flag::UInt8)
806-
method = match.method
806+
(; method, sparams) = match
807807
methsig = method.sig
808808

809809
# Check that we habe the correct number of arguments
@@ -818,7 +818,7 @@ function analyze_method!(match::MethodMatch, atypes::Vector{Any},
818818
end
819819

820820
# Bail out if any static parameters are left as TypeVar
821-
validate_sparams(match.sparams) || return nothing
821+
validate_sparams(sparams) || return nothing
822822

823823
et = state.et
824824

@@ -827,7 +827,11 @@ function analyze_method!(match::MethodMatch, atypes::Vector{Any},
827827
end
828828

829829
# See if there exists a specialization for this method signature
830-
mi = specialize_method(match; preexisting=true) # Union{Nothing, MethodInstance}
830+
if is_noinfer(method)
831+
mi = specialize_method_noinfer(match; preexisting=true)
832+
else
833+
mi = specialize_method(match; preexisting=true)
834+
end
831835
if !isa(mi, MethodInstance)
832836
return compileable_specialization(et, match)
833837
end

base/compiler/utilities.jl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ function is_inlineable_constant(@nospecialize(x))
9696
return count_const_size(x) <= MAX_INLINE_CONST_SIZE
9797
end
9898

99+
is_nospecialized(method::Method) = method.nospecialize 0
100+
101+
is_noinfer(method::Method) = method.noinfer && is_nospecialized(method)
102+
# is_noinfer(method::Method) = is_nospecialized(method) && is_declared_noinline(method)
103+
99104
###########################
100105
# MethodInstance/CodeInfo #
101106
###########################
@@ -144,6 +149,20 @@ function get_compileable_sig(method::Method, @nospecialize(atypes), sparams::Sim
144149
isa(atypes, DataType) || return nothing
145150
mt = ccall(:jl_method_table_for, Any, (Any,), atypes)
146151
mt === nothing && return nothing
152+
atypes′ = ccall(:jl_normalize_to_compilable_sig, Any, (Any, Any, Any, Any),
153+
mt, atypes, sparams, method)
154+
is_compileable = isdispatchtuple(atypes) ||
155+
ccall(:jl_isa_compileable_sig, Int32, (Any, Any), atypes′, method) 0
156+
return is_compileable ? atypes′ : nothing
157+
end
158+
159+
function get_nospecialize_sig(method::Method, @nospecialize(atypes), sparams::SimpleVector)
160+
if isa(atypes, UnionAll)
161+
atypes, sparams = normalize_typevars(method, atypes, sparams)
162+
end
163+
isa(atypes, DataType) || return method.sig
164+
mt = ccall(:jl_method_table_for, Any, (Any,), atypes)
165+
mt === nothing && return method.sig
147166
return ccall(:jl_normalize_to_compilable_sig, Any, (Any, Any, Any, Any),
148167
mt, atypes, sparams, method)
149168
end
@@ -196,7 +215,7 @@ function specialize_method(method::Method, @nospecialize(atypes), sparams::Simpl
196215
if preexisting
197216
# check cached specializations
198217
# for an existing result stored there
199-
return ccall(:jl_specializations_lookup, Any, (Any, Any), method, atypes)::Union{Nothing,MethodInstance}
218+
return ccall(:jl_specializations_lookup, Ref{MethodInstance}, (Any, Any), method, atypes)
200219
end
201220
return ccall(:jl_specializations_get_linfo, Ref{MethodInstance}, (Any, Any, Any), method, atypes, sparams)
202221
end
@@ -205,6 +224,11 @@ function specialize_method(match::MethodMatch; kwargs...)
205224
return specialize_method(match.method, match.spec_types, match.sparams; kwargs...)
206225
end
207226

227+
function specialize_method_noinfer((; method, spec_types, sparams)::MethodMatch; kwargs...)
228+
atypes = get_nospecialize_sig(method, spec_types, sparams)
229+
return specialize_method(method, atypes, sparams; kwargs...)
230+
end
231+
208232
# This function is used for computing alternate limit heuristics
209233
function method_for_inference_heuristics(method::Method, @nospecialize(sig), sparams::SimpleVector)
210234
if isdefined(method, :generator) && method.generator.expand_early && may_invoke_generator(method, sig, sparams)

base/essentials.jl

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ end
5454
@nospecialize
5555
5656
Applied to a function argument name, hints to the compiler that the method
57-
should not be specialized for different types of that argument,
58-
but instead to use precisely the declared type for each argument.
59-
This is only a hint for avoiding excess code generation.
60-
Can be applied to an argument within a formal argument list,
57+
implementation should not be specialized for different types of that argument,
58+
but instead use the declared type for that argument.
59+
It can be applied to an argument within a formal argument list,
6160
or in the function body.
62-
When applied to an argument, the macro must wrap the entire argument expression.
61+
When applied to an argument, the macro must wrap the entire argument expression, e.g.,
62+
`@nospecialize(x::Real)` or `@nospecialize(i::Integer...)` rather than wrapping just the argument name.
6363
When used in a function body, the macro must occur in statement position and
6464
before any code.
6565
@@ -87,6 +87,39 @@ end
8787
f(y) = [x for x in y]
8888
@specialize
8989
```
90+
91+
!!! note
92+
`@nospecialize` affects code generation but not inference: it limits the diversity
93+
of the resulting native code, but it does not impose any limitations (beyond the
94+
standard ones) on type-inference. Use [`Base.@noinfer`](@ref) together with
95+
`@nospecialize` to additionally suppress inference.
96+
97+
# Example
98+
99+
```julia
100+
julia> f(A::AbstractArray) = g(A)
101+
f (generic function with 1 method)
102+
103+
julia> @noinline g(@nospecialize(A::AbstractArray)) = A[1]
104+
g (generic function with 1 method)
105+
106+
julia> @code_typed f([1.0])
107+
CodeInfo(
108+
1 ─ %1 = invoke Main.g(_2::AbstractArray)::Float64
109+
└── return %1
110+
) => Float64
111+
```
112+
113+
Here, the `@nospecialize` annotation results in the equivalent of
114+
115+
```julia
116+
f(A::AbstractArray) = invoke(g, Tuple{AbstractArray}, A)
117+
```
118+
119+
ensuring that only one version of native code will be generated for `g`,
120+
one that is generic for any `AbstractArray`.
121+
However, the specific return type is still inferred for both `g` and `f`,
122+
and this is still used in optimizing the callers of `f` and `g`.
90123
"""
91124
macro nospecialize(vars...)
92125
if nfields(vars) === 1

base/expr.jl

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,12 @@ macro noinline(x)
336336
end
337337

338338
"""
339-
@pure ex
340-
@pure(ex)
339+
Base.@pure function f(args...)
340+
...
341+
end
342+
Base.@pure f(args...) = ...
341343
342-
`@pure` gives the compiler a hint for the definition of a pure function,
344+
`Base.@pure` gives the compiler a hint for the definition of a pure function,
343345
helping for type inference.
344346
345347
This macro is intended for internal compiler use and may be subject to changes.
@@ -349,16 +351,16 @@ macro pure(ex)
349351
end
350352

351353
"""
352-
@constprop setting ex
353-
@constprop(setting, ex)
354+
Base.@constprop setting ex
355+
Base.@constprop(setting, ex)
354356
355-
`@constprop` controls the mode of interprocedural constant propagation for the
357+
`Base.@constprop` controls the mode of interprocedural constant propagation for the
356358
annotated function. Two `setting`s are supported:
357359
358-
- `@constprop :aggressive ex`: apply constant propagation aggressively.
360+
- `Base.@constprop :aggressive ex`: apply constant propagation aggressively.
359361
For a method where the return type depends on the value of the arguments,
360362
this can yield improved inference results at the cost of additional compile time.
361-
- `@constprop :none ex`: disable constant propagation. This can reduce compile
363+
- `Base.@constprop :none ex`: disable constant propagation. This can reduce compile
362364
times for functions that Julia might otherwise deem worthy of constant-propagation.
363365
Common cases are for functions with `Bool`- or `Symbol`-valued arguments or keyword arguments.
364366
"""
@@ -371,6 +373,39 @@ macro constprop(setting, ex)
371373
throw(ArgumentError("@constprop $setting not supported"))
372374
end
373375

376+
"""
377+
Base.@noinfer function f(args...)
378+
@nospecialize ...
379+
...
380+
end
381+
Base.@noinfer f(@nospecialize args...) = ...
382+
383+
Tells the compiler to infer `f` using the declared types of `@nospecialize`d arguments.
384+
This can be used to limit the number of compiler-generated specializations during inference.
385+
386+
# Example
387+
388+
```julia
389+
julia> f(A::AbstractArray) = g(A)
390+
f (generic function with 1 method)
391+
392+
julia> @noinline Base.@noinfer g(@nospecialize(A::AbstractArray)) = A[1]
393+
g (generic function with 1 method)
394+
395+
julia> @code_typed f([1.0])
396+
CodeInfo(
397+
1 ─ %1 = invoke Main.g(_2::AbstractArray)::Any
398+
└── return %1
399+
) => Any
400+
```
401+
402+
In this example, `f` will be inferred for each specific type of `A`,
403+
but `g` will only be inferred once.
404+
"""
405+
macro noinfer(ex)
406+
esc(isa(ex, Expr) ? pushmeta!(ex, :noinfer) : ex)
407+
end
408+
374409
"""
375410
@propagate_inbounds
376411

doc/src/base/base.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ Base.@inline
273273
Base.@noinline
274274
Base.@nospecialize
275275
Base.@specialize
276+
Base.@noinfer
277+
Base.@constprop
276278
Base.gensym
277279
Base.@gensym
278280
var"name"

src/ast.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,9 @@ jl_sym_t *static_parameter_sym; jl_sym_t *inline_sym;
5959
jl_sym_t *noinline_sym; jl_sym_t *generated_sym;
6060
jl_sym_t *generated_only_sym; jl_sym_t *isdefined_sym;
6161
jl_sym_t *propagate_inbounds_sym; jl_sym_t *specialize_sym;
62+
jl_sym_t *nospecialize_sym; jl_sym_t *noinfer_sym;
6263
jl_sym_t *aggressive_constprop_sym; jl_sym_t *no_constprop_sym;
63-
jl_sym_t *nospecialize_sym; jl_sym_t *macrocall_sym;
64+
jl_sym_t *macrocall_sym;
6465
jl_sym_t *colon_sym; jl_sym_t *hygienicscope_sym;
6566
jl_sym_t *throw_undef_if_not_sym; jl_sym_t *getfield_undefref_sym;
6667
jl_sym_t *gc_preserve_begin_sym; jl_sym_t *gc_preserve_end_sym;
@@ -403,6 +404,7 @@ void jl_init_common_symbols(void)
403404
isdefined_sym = jl_symbol("isdefined");
404405
nospecialize_sym = jl_symbol("nospecialize");
405406
specialize_sym = jl_symbol("specialize");
407+
noinfer_sym = jl_symbol("noinfer");
406408
optlevel_sym = jl_symbol("optlevel");
407409
compile_sym = jl_symbol("compile");
408410
infer_sym = jl_symbol("infer");

src/dump.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,7 @@ static void jl_serialize_value_(jl_serializer_state *s, jl_value_t *v, int as_li
671671
write_int8(s->s, m->isva);
672672
write_int8(s->s, m->pure);
673673
write_int8(s->s, m->is_for_opaque_closure);
674+
write_int8(s->s, m->noinfer);
674675
write_int8(s->s, m->constprop);
675676
jl_serialize_value(s, (jl_value_t*)m->slot_syms);
676677
jl_serialize_value(s, (jl_value_t*)m->roots);
@@ -1526,6 +1527,7 @@ static jl_value_t *jl_deserialize_value_method(jl_serializer_state *s, jl_value_
15261527
m->isva = read_int8(s->s);
15271528
m->pure = read_int8(s->s);
15281529
m->is_for_opaque_closure = read_int8(s->s);
1530+
m->noinfer = read_int8(s->s);
15291531
m->constprop = read_int8(s->s);
15301532
m->slot_syms = jl_deserialize_value(s, (jl_value_t**)&m->slot_syms);
15311533
jl_gc_wb(m, m->slot_syms);

src/gf.c

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,10 +2053,8 @@ JL_DLLEXPORT jl_value_t *jl_normalize_to_compilable_sig(jl_methtable_t *mt, jl_t
20532053
intptr_t nspec = (mt == jl_type_type_mt || mt == jl_nonfunction_mt ? m->nargs + 1 : mt->max_args + 2);
20542054
jl_compilation_sig(ti, env, m, nspec, &newparams);
20552055
tt = (newparams ? jl_apply_tuple_type(newparams) : ti);
2056-
int is_compileable = ((jl_datatype_t*)ti)->isdispatchtuple ||
2057-
jl_isa_compileable_sig(tt, m);
20582056
JL_GC_POP();
2059-
return is_compileable ? (jl_value_t*)tt : jl_nothing;
2057+
return (jl_value_t*)tt;
20602058
}
20612059

20622060
// compile-time method lookup
@@ -2100,9 +2098,7 @@ jl_method_instance_t *jl_get_specialization1(jl_tupletype_t *types JL_PROPAGATES
21002098
}
21012099
else {
21022100
tt = jl_normalize_to_compilable_sig(mt, ti, env, m);
2103-
if (tt != jl_nothing) {
2104-
nf = jl_specializations_get_linfo(m, (jl_value_t*)tt, env);
2105-
}
2101+
nf = jl_specializations_get_linfo(m, (jl_value_t*)tt, env);
21062102
}
21072103
}
21082104
}

src/jltypes.c

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2329,7 +2329,7 @@ void jl_init_types(void) JL_GC_DISABLED
23292329
jl_code_info_type =
23302330
jl_new_datatype(jl_symbol("CodeInfo"), core,
23312331
jl_any_type, jl_emptysvec,
2332-
jl_perm_symsvec(19,
2332+
jl_perm_symsvec(20,
23332333
"code",
23342334
"codelocs",
23352335
"ssavaluetypes",
@@ -2348,8 +2348,9 @@ void jl_init_types(void) JL_GC_DISABLED
23482348
"inlineable",
23492349
"propagate_inbounds",
23502350
"pure",
2351+
"noinfer",
23512352
"constprop"),
2352-
jl_svec(19,
2353+
jl_svec(20,
23532354
jl_array_any_type,
23542355
jl_array_int32_type,
23552356
jl_any_type,
@@ -2368,14 +2369,15 @@ void jl_init_types(void) JL_GC_DISABLED
23682369
jl_bool_type,
23692370
jl_bool_type,
23702371
jl_bool_type,
2372+
jl_bool_type,
23712373
jl_uint8_type),
23722374
jl_emptysvec,
2373-
0, 1, 19);
2375+
0, 1, 20);
23742376

23752377
jl_method_type =
23762378
jl_new_datatype(jl_symbol("Method"), core,
23772379
jl_any_type, jl_emptysvec,
2378-
jl_perm_symsvec(26,
2380+
jl_perm_symsvec(27,
23792381
"name",
23802382
"module",
23812383
"file",
@@ -2401,8 +2403,9 @@ void jl_init_types(void) JL_GC_DISABLED
24012403
"isva",
24022404
"pure",
24032405
"is_for_opaque_closure",
2406+
"noinfer",
24042407
"constprop"),
2405-
jl_svec(26,
2408+
jl_svec(27,
24062409
jl_symbol_type,
24072410
jl_module_type,
24082411
jl_symbol_type,
@@ -2428,6 +2431,7 @@ void jl_init_types(void) JL_GC_DISABLED
24282431
jl_bool_type,
24292432
jl_bool_type,
24302433
jl_bool_type,
2434+
jl_bool_type,
24312435
jl_uint8_type),
24322436
jl_emptysvec,
24332437
0, 1, 10);

0 commit comments

Comments
 (0)