Skip to content

Commit

Permalink
replace @pure annotations in Base with effect settings (JuliaLang#4…
Browse files Browse the repository at this point in the history
…4776)

This commit replaces `@pure`/`@_pure_meta` annotations used in `Base`
with corresponding effect settings (`:total` or `:total_or_throw`).

The concrete evaluation mechanism based on the effect system (JuliaLang#43852)
has the following benefits over the `@pure`-based optimization:
- it can fold cases when consistent exception is thrown
- it can handle constant union-split situation as well
- effects can be propagated inter-procedurally

While revisiting the existing annotations, I removed some unnecessary ones
and also added some more hopefully new annotations (mostly for reflection utilities).
In theory this should give us some performance benefits, e.g. now we
can concrete-evaluate union-spit `typeintersect`:
```julia
@test Base.return_types((Union{Int,Nothing},)) do x
  typeintersect(String, typeof(x))
end |> only === Type{Union{}}
```

Though this commit ends up bigger than I expected -- we should carefully
check a benchmark result so that it doesn't come with any regressions.
  • Loading branch information
aviatesk committed Apr 11, 2022
1 parent 0deb326 commit dfe0e34
Show file tree
Hide file tree
Showing 19 changed files with 125 additions and 109 deletions.
2 changes: 1 addition & 1 deletion base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ include("abstractarraymath.jl")
include("arraymath.jl")

# SIMD loops
@pure sizeof(s::String) = Core.sizeof(s) # needed by gensym as called from simdloop
sizeof(s::String) = Core.sizeof(s) # needed by gensym as called from simdloop
include("simdloop.jl")
using .SimdLoop

Expand Down
2 changes: 1 addition & 1 deletion base/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ size(a::Array{<:Any,N}) where {N} = (@inline; ntuple(M -> size(a, M), Val(N))::D

asize_from(a::Array, n) = n > ndims(a) ? () : (arraysize(a,n), asize_from(a, n+1)...)

allocatedinline(T::Type) = (@_pure_meta; ccall(:jl_stored_inline, Cint, (Any,), T) != Cint(0))
allocatedinline(T::Type) = (@_total_meta; ccall(:jl_stored_inline, Cint, (Any,), T) != Cint(0))

"""
Base.isbitsunion(::Type{T})
Expand Down
4 changes: 2 additions & 2 deletions base/broadcast.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Module containing the broadcasting implementation.
module Broadcast

using .Base.Cartesian
using .Base: Indices, OneTo, tail, to_shape, isoperator, promote_typejoin, promote_typejoin_union, @pure,
using .Base: Indices, OneTo, tail, to_shape, isoperator, promote_typejoin, promote_typejoin_union,
_msk_end, unsafe_bitgetindex, bitcache_chunks, bitcache_size, dumpbitcache, unalias, negate
import .Base: copy, copyto!, axes
export broadcast, broadcast!, BroadcastStyle, broadcast_axes, broadcastable, dotview, @__dot__, BroadcastFunction
Expand Down Expand Up @@ -137,7 +137,7 @@ BroadcastStyle(a::AbstractArrayStyle, ::Style{Tuple}) = a
BroadcastStyle(::A, ::A) where A<:ArrayStyle = A()
BroadcastStyle(::ArrayStyle, ::ArrayStyle) = Unknown()
BroadcastStyle(::A, ::A) where A<:AbstractArrayStyle = A()
Base.@pure function BroadcastStyle(a::A, b::B) where {A<:AbstractArrayStyle{M},B<:AbstractArrayStyle{N}} where {M,N}
function BroadcastStyle(a::A, b::B) where {A<:AbstractArrayStyle{M},B<:AbstractArrayStyle{N}} where {M,N}
if Base.typename(A) === Base.typename(B)
return A(Val(max(M, N)))
end
Expand Down
2 changes: 1 addition & 1 deletion base/c.jl
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,6 @@ macro ccall(expr)
return ccall_macro_lower(:ccall, ccall_macro_parse(expr)...)
end

macro ccall_effects(effects, expr)
macro ccall_effects(effects::UInt8, expr)
return ccall_macro_lower((:ccall, effects), ccall_macro_parse(expr)...)
end
2 changes: 1 addition & 1 deletion base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1950,7 +1950,7 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
end
cconv = e.args[5]
if isa(cconv, QuoteNode) && isa(cconv.value, Tuple{Symbol, UInt8})
effects = cconv.value[2]
effects = cconv.value[2]::UInt8
effects = decode_effects_override(effects)
tristate_merge!(sv, Effects(
effects.consistent ? ALWAYS_TRUE : TRISTATE_UNKNOWN,
Expand Down
53 changes: 27 additions & 26 deletions base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -427,13 +427,34 @@ function cycle_fix_limited(@nospecialize(typ), sv::InferenceState)
return typ
end

function rt_adjust_effects(@nospecialize(rt), ipo_effects::Effects)
function adjust_effects(sv::InferenceState)
ipo_effects = Effects(sv)

# Always throwing an error counts or never returning both count as consistent,
# but we don't currently model idempontency using dataflow, so we don't notice.
# Fix that up here to improve precision.
if !ipo_effects.inbounds_taints_consistency && rt === Union{}
return Effects(ipo_effects; consistent=ALWAYS_TRUE)
if !ipo_effects.inbounds_taints_consistency && sv.bestguess === Union{}
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
end

# override the analyzed effects using manually annotated effect settings
def = sv.linfo.def
if isa(def, Method)
override = decode_effects_override(def.purity)
if is_effect_overridden(override, :consistent)
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
end
if is_effect_overridden(override, :effect_free)
ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_TRUE)
end
if is_effect_overridden(override, :nothrow)
ipo_effects = Effects(ipo_effects; nothrow=ALWAYS_TRUE)
end
if is_effect_overridden(override, :terminates_globally)
ipo_effects = Effects(ipo_effects; terminates=ALWAYS_TRUE)
end
end

return ipo_effects
end

Expand Down Expand Up @@ -495,25 +516,7 @@ function finish(me::InferenceState, interp::AbstractInterpreter)
end
me.result.valid_worlds = me.valid_worlds
me.result.result = me.bestguess
ipo_effects = rt_adjust_effects(me.bestguess, me.ipo_effects)
# override the analyzed effects using manually annotated effect settings
def = me.linfo.def
if isa(def, Method)
override = decode_effects_override(def.purity)
if is_effect_overridden(override, :consistent)
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
end
if is_effect_overridden(override, :effect_free)
ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_TRUE)
end
if is_effect_overridden(override, :nothrow)
ipo_effects = Effects(ipo_effects; nothrow=ALWAYS_TRUE)
end
if is_effect_overridden(override, :terminates_globally)
ipo_effects = Effects(ipo_effects; terminates=ALWAYS_TRUE)
end
end
me.ipo_effects = me.result.ipo_effects = ipo_effects
me.ipo_effects = me.result.ipo_effects = adjust_effects(me)
validate_code_in_debug_mode(me.linfo, me.src, "inferred")
nothing
end
Expand Down Expand Up @@ -887,17 +890,15 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize
typeinf(interp, frame)
update_valid_age!(frame, caller)
edge = frame.inferred ? mi : nothing
edge_effects = rt_adjust_effects(frame.bestguess, Effects(frame))
return EdgeCallResult(frame.bestguess, edge, edge_effects)
return EdgeCallResult(frame.bestguess, edge, Effects(frame)) # effects are adjusted already within `finish`
elseif frame === true
# unresolvable cycle
return EdgeCallResult(Any, nothing, Effects())
end
# return the current knowledge about this cycle
frame = frame::InferenceState
update_valid_age!(frame, caller)
edge_effects = rt_adjust_effects(frame.bestguess, Effects(frame))
return EdgeCallResult(frame.bestguess, nothing, edge_effects)
return EdgeCallResult(frame.bestguess, nothing, adjust_effects(frame))
end

#### entry points for inferring a MethodInstance given a type signature ####
Expand Down
4 changes: 2 additions & 2 deletions base/deprecated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,11 @@ getindex(match::Core.MethodMatch, field::Int) =
tuple_type_head(T::Type) = fieldtype(T, 1)
tuple_type_cons(::Type, ::Type{Union{}}) = Union{}
function tuple_type_cons(::Type{S}, ::Type{T}) where T<:Tuple where S
@_pure_meta
@_total_may_throw_meta
Tuple{S, T.parameters...}
end
function parameter_upper_bound(t::UnionAll, idx)
@_pure_meta
@_total_may_throw_meta
return rewrap_unionall((unwrap_unionall(t)::DataType).parameters[idx], t)
end

Expand Down
30 changes: 29 additions & 1 deletion base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,37 @@ macro isdefined(s::Symbol)
return Expr(:escape, Expr(:isdefined, s))
end

function _is_internal(__module__)
if ccall(:jl_base_relative_to, Any, (Any,), __module__)::Module === Core.Compiler ||
nameof(__module__) === :Base
return true
end
return false
end

# can be used in place of `@pure` (supposed to be used for bootstrapping)
macro _pure_meta()
return Expr(:meta, :pure)
return _is_internal(__module__) && Expr(:meta, :pure)
end
# can be used in place of `@assume_effects :total` (supposed to be used for bootstrapping)
macro _total_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
#=:consistent=#true,
#=:effect_free=#true,
#=:nothrow=#true,
#=:terminates_globally=#true,
#=:terminates_locally=#false))
end
# can be used in place of `@assume_effects :total_may_throw` (supposed to be used for bootstrapping)
macro _total_may_throw_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
#=:consistent=#true,
#=:effect_free=#true,
#=:nothrow=#false,
#=:terminates_globally=#true,
#=:terminates_locally=#false))
end

# another version of inlining that propagates an inbounds context
macro _propagate_inbounds_meta()
return Expr(:meta, :inline, :propagate_inbounds)
Expand Down
8 changes: 3 additions & 5 deletions base/intfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,9 @@ end

# binary GCD (aka Stein's) algorithm
# about 1.7x (2.1x) faster for random Int64s (Int128s)
# Unfortunately, we need to manually annotate this as `@pure` to work around #41694. Since
# this is used in the Rational constructor, constant prop is something we do care about here.
# This does call generic functions, so it might not be completely sound, but since `_gcd` is
# restricted to BitIntegers, it is probably fine in practice.
@pure function _gcd(a::T, b::T) where T<:BitInteger
# Unfortunately, we need to manually annotate this as `@assume_effects :terminates_locally` to work around #41694.
# Since this is used in the Rational constructor, constant folding is something we do care about here.
@assume_effects :terminates_locally function _gcd(a::T, b::T) where T<:BitInteger
za = trailing_zeros(a)
zb = trailing_zeros(b)
k = min(za, zb)
Expand Down
11 changes: 6 additions & 5 deletions base/irrationals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ AbstractFloat(x::AbstractIrrational) = Float64(x)::Float64
Float16(x::AbstractIrrational) = Float16(Float32(x)::Float32)
Complex{T}(x::AbstractIrrational) where {T<:Real} = Complex{T}(T(x))

@pure function Rational{T}(x::AbstractIrrational) where T<:Integer
# XXX this may change `DEFAULT_PRECISION`, thus not effect free
@assume_effects :total function Rational{T}(x::AbstractIrrational) where T<:Integer
o = precision(BigFloat)
p = 256
while true
Expand All @@ -64,7 +65,7 @@ Complex{T}(x::AbstractIrrational) where {T<:Real} = Complex{T}(T(x))
end
Rational{BigInt}(x::AbstractIrrational) = throw(ArgumentError("Cannot convert an AbstractIrrational to a Rational{BigInt}: use rationalize(BigInt, x) instead"))

@pure function (t::Type{T})(x::AbstractIrrational, r::RoundingMode) where T<:Union{Float32,Float64}
@assume_effects :total function (t::Type{T})(x::AbstractIrrational, r::RoundingMode) where T<:Union{Float32,Float64}
setprecision(BigFloat, 256) do
T(BigFloat(x)::BigFloat, r)
end
Expand Down Expand Up @@ -106,11 +107,11 @@ end
<=(x::AbstractFloat, y::AbstractIrrational) = x < y

# Irrational vs Rational
@pure function rationalize(::Type{T}, x::AbstractIrrational; tol::Real=0) where T
@assume_effects :total function rationalize(::Type{T}, x::AbstractIrrational; tol::Real=0) where T
return rationalize(T, big(x), tol=tol)
end
@pure function lessrational(rx::Rational{<:Integer}, x::AbstractIrrational)
# an @pure version of `<` for determining if the rationalization of
@assume_effects :total function lessrational(rx::Rational{<:Integer}, x::AbstractIrrational)
# an @assume_effects :total version of `<` for determining if the rationalization of
# an irrational number required rounding up or down
return rx < big(x)
end
Expand Down
6 changes: 3 additions & 3 deletions base/namedtuple.jl
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ function map(f, nt::NamedTuple{names}, nts::NamedTuple...) where names
NamedTuple{names}(map(f, map(Tuple, (nt, nts...))...))
end

@pure function merge_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}})
@assume_effects :total function merge_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}})
@nospecialize an bn
names = Symbol[an...]
for n in bn
Expand All @@ -229,7 +229,7 @@ end
(names...,)
end

@pure function merge_types(names::Tuple{Vararg{Symbol}}, a::Type{<:NamedTuple}, b::Type{<:NamedTuple})
@assume_effects :total function merge_types(names::Tuple{Vararg{Symbol}}, a::Type{<:NamedTuple}, b::Type{<:NamedTuple})
@nospecialize names a b
bn = _nt_names(b)
return Tuple{Any[ fieldtype(sym_in(names[n], bn) ? b : a, names[n]) for n in 1:length(names) ]...}
Expand Down Expand Up @@ -321,7 +321,7 @@ get(f::Callable, nt::NamedTuple, key::Union{Integer, Symbol}) = isdefined(nt, ke
tail(t::NamedTuple{names}) where names = NamedTuple{tail(names)}(t)
front(t::NamedTuple{names}) where names = NamedTuple{front(names)}(t)

@pure function diff_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}})
@assume_effects :total function diff_names(an::Tuple{Vararg{Symbol}}, bn::Tuple{Vararg{Symbol}})
@nospecialize an bn
names = Symbol[]
for n in an
Expand Down
21 changes: 4 additions & 17 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,8 @@ julia> supertype(Int32)
Signed
```
"""
function supertype(T::DataType)
@_pure_meta
T.super
end

function supertype(T::UnionAll)
@_pure_meta
UnionAll(T.var, supertype(T.body))
end
supertype(T::DataType) = (@_total_meta; T.super)
supertype(T::UnionAll) = (@_total_meta; UnionAll(T.var, supertype(T.body)))

## generic comparison ##

Expand Down Expand Up @@ -247,14 +240,8 @@ isunordered(x) = false
isunordered(x::AbstractFloat) = isnan(x)
isunordered(x::Missing) = true

function ==(T::Type, S::Type)
@_pure_meta
return ccall(:jl_types_equal, Cint, (Any, Any), T, S) != 0
end
function !=(T::Type, S::Type)
@_pure_meta
return !(T == S)
end
==(T::Type, S::Type) = (@_total_meta; ccall(:jl_types_equal, Cint, (Any, Any), T, S) != 0)
!=(T::Type, S::Type) = (@_total_meta; !(T == S))
==(T::TypeVar, S::Type) = false
==(T::Type, S::TypeVar) = false

Expand Down
8 changes: 4 additions & 4 deletions base/promotion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ they both inherit.
"""
typejoin() = Bottom
typejoin(@nospecialize(t)) = t
typejoin(@nospecialize(t), ts...) = (@_pure_meta; typejoin(t, typejoin(ts...)))
typejoin(@nospecialize(t), ts...) = (@_total_meta; typejoin(t, typejoin(ts...)))
function typejoin(@nospecialize(a), @nospecialize(b))
@_pure_meta
@_total_meta
if isa(a, TypeVar)
return typejoin(a.ub, b)
elseif isa(b, TypeVar)
Expand Down Expand Up @@ -128,7 +128,7 @@ end
# WARNING: this is wrong for some objects for which subtyping is broken
# (Core.Compiler.isnotbrokensubtype), use only simple types for `b`
function typesplit(@nospecialize(a), @nospecialize(b))
@_pure_meta
@_total_may_throw_meta
if a <: b
return Bottom
end
Expand Down Expand Up @@ -180,7 +180,7 @@ function promote_typejoin_union(::Type{T}) where T
end

function typejoin_union_tuple(T::DataType)
@_pure_meta
@_total_may_throw_meta
u = Base.unwrap_unionall(T)
p = (u::DataType).parameters
lr = length(p)::Int
Expand Down
Loading

0 comments on commit dfe0e34

Please sign in to comment.