Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take purity modeling seriously #43852

Merged
merged 20 commits into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Compiler/Runtime improvements
* Abstract callsite can now be inlined or statically resolved as far as the callsite has a single
matching method ([#43113]).
* Builtin function are now a bit more like generic functions, and can be enumerated with `methods` ([#43865]).
* Inference now tracks various effects such as sideeffectful-ness and nothrow-ness on a per-specialization basis. Code heavily dependent on constant propagation should see significant compile-time performance improvements and certain cases (e.g. calls to uninlinable functions that are nevertheless effect free) should see runtime performance improvements. Effects may be overwritten manually with the `@Base.assume_effects` macro. (#43852).

Command-line option changes
---------------------------
Expand Down
14 changes: 10 additions & 4 deletions base/abstractarray.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1238,10 +1238,16 @@ function unsafe_getindex(A::AbstractArray, I...)
r
end

struct CanonicalIndexError
func::String
type::Any
CanonicalIndexError(func::String, @nospecialize(type)) = new(func, type)
end

error_if_canonical_getindex(::IndexLinear, A::AbstractArray, ::Int) =
error("getindex not defined for ", typeof(A))
throw(CanonicalIndexError("getindex", typeof(A)))
error_if_canonical_getindex(::IndexCartesian, A::AbstractArray{T,N}, ::Vararg{Int,N}) where {T,N} =
error("getindex not defined for ", typeof(A))
throw(CanonicalIndexError("getindex", typeof(A)))
error_if_canonical_getindex(::IndexStyle, ::AbstractArray, ::Any...) = nothing

## Internal definitions
Expand Down Expand Up @@ -1333,9 +1339,9 @@ function unsafe_setindex!(A::AbstractArray, v, I...)
end

error_if_canonical_setindex(::IndexLinear, A::AbstractArray, ::Int) =
error("setindex! not defined for ", typeof(A))
throw(CanonicalIndexError("setindex!", typeof(A)))
error_if_canonical_setindex(::IndexCartesian, A::AbstractArray{T,N}, ::Vararg{Int,N}) where {T,N} =
error("setindex! not defined for ", typeof(A))
throw(CanonicalIndexError("setindex!", typeof(A)))
error_if_canonical_setindex(::IndexStyle, ::AbstractArray, ::Any...) = nothing

## Internal definitions
Expand Down
2 changes: 1 addition & 1 deletion base/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ function bitsunionsize(u::Union)
end

length(a::Array) = arraylen(a)
elsize(::Type{<:Array{T}}) where {T} = aligned_sizeof(T)
elsize(@nospecialize _::Type{A}) where {T,A<:Array{T}} = aligned_sizeof(T)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nospecialize is probably not allowed here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean because of where it is in bootstrap or syntax wise? It worked and fixed the inference time issue, so that wasn't a problem.

sizeof(a::Array) = Core.sizeof(a)

function isassigned(a::Array, i::Int...)
Expand Down
7 changes: 4 additions & 3 deletions base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,10 @@ eval(Core, :(LineInfoNode(mod::Module, @nospecialize(method), file::Symbol, line
$(Expr(:new, :LineInfoNode, :mod, :method, :file, :line, :inlined_at))))
eval(Core, :(CodeInstance(mi::MethodInstance, @nospecialize(rettype), @nospecialize(inferred_const),
@nospecialize(inferred), const_flags::Int32,
min_world::UInt, max_world::UInt, relocatability::UInt8) =
ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt, UInt8),
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, relocatability)))
min_world::UInt, max_world::UInt, ipo_effects::UInt8, effects::UInt8,
relocatability::UInt8) =
ccall(:jl_new_codeinst, Ref{CodeInstance}, (Any, Any, Any, Any, Int32, UInt, UInt, UInt8, UInt8, UInt8),
mi, rettype, inferred_const, inferred, const_flags, min_world, max_world, ipo_effects, effects, relocatability)))
eval(Core, :(Const(@nospecialize(v)) = $(Expr(:new, :Const, :v))))
eval(Core, :(PartialStruct(@nospecialize(typ), fields::Array{Any, 1}) = $(Expr(:new, :PartialStruct, :typ, :fields))))
eval(Core, :(PartialOpaque(@nospecialize(typ), @nospecialize(env), isva::Bool, parent::MethodInstance, source::Method) = $(Expr(:new, :PartialOpaque, :typ, :env, :isva, :parent, :source))))
Expand Down
4 changes: 4 additions & 0 deletions base/c.jl
Original file line number Diff line number Diff line change
Expand Up @@ -733,3 +733,7 @@ name, if desired `"libglib-2.0".g_uri_escape_string(...`
macro ccall(expr)
return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...)
end

macro ccall_effects(effects, expr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment that this is used internally by assume_effects?

return ccall_macro_lower((:ccall, effects), ccall_macro_parse(expr)...)
end
258 changes: 220 additions & 38 deletions base/compiler/abstractinterpretation.jl

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions base/compiler/inferencestate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ mutable struct InferenceState
inferred::Bool
dont_work_on_me::Bool

# Inferred purity flags
ipo_effects::Effects

# The place to look up methods while working on this function.
# In particular, we cache method lookup results for the same function to
# fast path repeated queries.
Expand Down Expand Up @@ -113,6 +116,16 @@ mutable struct InferenceState
valid_worlds = WorldRange(src.min_world,
src.max_world == typemax(UInt) ? get_world_counter() : src.max_world)

# TODO: Currently, any :inbounds declaration taints consistency,
# because we cannot be guaranteed whether or not boundschecks
# will be eliminated and if they are, we cannot be guaranteed
# that no undefined behavior will occur (the effects assumptions
# are stronger than the inbounds assumptions, since the latter
# requires dynamic reachability, while the former is global).
inbounds = inbounds_option()
inbounds_taints_consistency = !(inbounds === :on || (inbounds === :default && !any_inbounds(code)))
consistent = inbounds_taints_consistency ? TRISTATE_UNKNOWN : ALWAYS_TRUE

@assert cache === :no || cache === :local || cache === :global
frame = new(
params, result, linfo,
Expand All @@ -126,13 +139,26 @@ mutable struct InferenceState
Vector{InferenceState}(), # callers_in_cycle
#=parent=#nothing,
cache === :global, false, false,
Effects(consistent, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE,
inbounds_taints_consistency),
CachedMethodTable(method_table(interp)),
interp)
result.result = frame
cache !== :no && push!(get_inference_cache(interp), result)
return frame
end
end
Effects(state::InferenceState) = state.ipo_effects

function any_inbounds(code::Vector{Any})
for i=1:length(code)
stmt = code[i]
if isa(stmt, Expr) && stmt.head === :inbounds
return true
end
end
return false
end

function compute_trycatch(code::Vector{Any}, ip::BitSet)
# The goal initially is to record the frame like this for the state at exit:
Expand Down
41 changes: 9 additions & 32 deletions base/compiler/optimize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -149,16 +149,6 @@ const IR_FLAG_THROW_BLOCK = 0x01 << 3
# thus be both pure and effect free.
const IR_FLAG_EFFECT_FREE = 0x01 << 4

# known to be always effect-free (in particular nothrow)
const _PURE_BUILTINS = Any[tuple, svec, ===, typeof, nfields]

# known to be effect-free if the are nothrow
const _PURE_OR_ERROR_BUILTINS = [
fieldtype, apply_type, isa, UnionAll,
getfield, arrayref, const_arrayref, arraysize, isdefined, Core.sizeof,
Core.kwfunc, Core.ifelse, Core._typevar, (<:),
]

const TOP_TUPLE = GlobalRef(Core, :tuple)

#########
Expand Down Expand Up @@ -225,7 +215,7 @@ function stmt_effect_free(@nospecialize(stmt), @nospecialize(rt), src::Union{IRC
M, s = argextype(args[2], src), argextype(args[3], src)
return get_binding_type_effect_free(M, s)
end
contains_is(_PURE_OR_ERROR_BUILTINS, f) || return false
contains_is(_EFFECT_FREE_BUILTINS, f) || return false
rt === Bottom && return false
return _builtin_nothrow(f, Any[argextype(args[i], src) for i = 2:length(args)], rt)
elseif head === :new
Expand Down Expand Up @@ -297,12 +287,14 @@ function alloc_array_ndims(name::Symbol)
return nothing
end

const FOREIGNCALL_ARG_START = 6

function alloc_array_no_throw(args::Vector{Any}, ndims::Int, src::Union{IRCode,IncrementalCompact})
length(args) ≥ ndims+6 || return false
atype = instanceof_tfunc(argextype(args[6], src))[1]
length(args) ≥ ndims+FOREIGNCALL_ARG_START || return false
atype = instanceof_tfunc(argextype(args[FOREIGNCALL_ARG_START], src))[1]
dims = Csize_t[]
for i in 1:ndims
dim = argextype(args[i+6], src)
dim = argextype(args[i+FOREIGNCALL_ARG_START], src)
isa(dim, Const) || return false
dimval = dim.val
isa(dimval, Int) || return false
Expand All @@ -312,9 +304,9 @@ function alloc_array_no_throw(args::Vector{Any}, ndims::Int, src::Union{IRCode,I
end

function new_array_no_throw(args::Vector{Any}, src::Union{IRCode,IncrementalCompact})
length(args) ≥ 7 || return false
atype = instanceof_tfunc(argextype(args[6], src))[1]
dims = argextype(args[7], src)
length(args) ≥ FOREIGNCALL_ARG_START+1 || return false
atype = instanceof_tfunc(argextype(args[FOREIGNCALL_ARG_START], src))[1]
dims = argextype(args[FOREIGNCALL_ARG_START+1], src)
isa(dims, Const) || return dims === Tuple{}
dimsval = dims.val
isa(dimsval, Tuple{Vararg{Int}}) || return false
Expand Down Expand Up @@ -621,21 +613,6 @@ function slot2reg(ir::IRCode, ci::CodeInfo, sv::OptimizationState)
return ir
end

# whether `f` is pure for inference
function is_pure_intrinsic_infer(f::IntrinsicFunction)
return !(f === Intrinsics.pointerref || # this one is volatile
f === Intrinsics.pointerset || # this one is never effect-free
f === Intrinsics.llvmcall || # this one is never effect-free
f === Intrinsics.arraylen || # this one is volatile
f === Intrinsics.sqrt_llvm_fast || # this one may differ at runtime (by a few ulps)
f === Intrinsics.have_fma || # this one depends on the runtime environment
f === Intrinsics.cglobal) # cglobal lookup answer changes at runtime
end

# whether `f` is effect free if nothrow
intrinsic_effect_free_if_nothrow(f) = f === Intrinsics.pointerref ||
f === Intrinsics.have_fma || is_pure_intrinsic_infer(f)

## Computing the cost of a function body

# saturating sum (inputs are nonnegative), prevents overflow with typemax(Int) below
Expand Down
Loading