Skip to content

Commit

Permalink
effects: add :inaccessiblememonly effect (JuliaLang#46198)
Browse files Browse the repository at this point in the history
This is a preparatory PR for future improvements on the effects analysis.
This commit adds the `:inaccessiblememonly` helper effect, that tracks
if a method involves any access or modification on any mutable state.
This effect property is basically same as LLVM's `inaccessiblememonly`
function attribute, except that it only reasons about mutable memory.
This effect property can be considered as a very limited and coarse
version of escape analysis and allow us prove `:consistent`-cy or
`:effect_free`-ness even in a presence of accesses or modifications
on local mutable allocations in cases when we can prove they are local
and don't escape. Separate PRs that actually improve the effect analysis
accuracy will follow.
  • Loading branch information
aviatesk committed Aug 3, 2022
1 parent eedf3f1 commit c094a89
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 62 deletions.
3 changes: 2 additions & 1 deletion base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,8 @@ macro _foldable_meta()
#=:nothrow=#false,
#=:terminates_globally=#true,
#=:terminates_locally=#false,
#=:notaskstate=#false))
#=:notaskstate=#false,
#=:inaccessiblememonly=#false))
end

const NTuple{N,T} = Tuple{Vararg{T,N}}
Expand Down
23 changes: 15 additions & 8 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2081,10 +2081,11 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e),
override = decode_effects_override(v[2])
effects = Effects(
override.consistent ? ALWAYS_TRUE : effects.consistent,
override.effect_free ? true : effects.effect_free,
override.nothrow ? true : effects.nothrow,
override.terminates_globally ? true : effects.terminates,
override.notaskstate ? true : effects.notaskstate,
override.effect_free ? true : effects.effect_free,
override.nothrow ? true : effects.nothrow,
override.terminates_globally ? true : effects.terminates,
override.notaskstate ? true : effects.notaskstate,
override.inaccessiblememonly ? ALWAYS_TRUE : effects.inaccessiblememonly,
effects.nonoverlayed)
end
merge_effects!(sv, effects)
Expand Down Expand Up @@ -2166,22 +2167,28 @@ end

function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState)
rt = abstract_eval_global(M, s)
consistent = ALWAYS_FALSE
consistent = inaccessiblememonly = ALWAYS_FALSE
nothrow = false
if isa(rt, Const)
consistent = ALWAYS_TRUE
nothrow = true
if is_mutation_free_argtype(rt)
inaccessiblememonly = ALWAYS_TRUE
nothrow = true
else
nothrow = true
end
elseif isdefined(M,s)
nothrow = true
end
merge_effects!(frame, Effects(EFFECTS_TOTAL; consistent, nothrow))
merge_effects!(frame, Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly))
return rt
end

function handle_global_assignment!(interp::AbstractInterpreter, frame::InferenceState, lhs::GlobalRef, @nospecialize(newty))
effect_free = false
nothrow = global_assignment_nothrow(lhs.mod, lhs.name, newty)
merge_effects!(frame, Effects(EFFECTS_TOTAL; effect_free, nothrow))
inaccessiblememonly = ALWAYS_FALSE
merge_effects!(frame, Effects(EFFECTS_TOTAL; effect_free, nothrow, inaccessiblememonly))
return nothing
end

Expand Down
63 changes: 44 additions & 19 deletions base/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ following meanings:
behavior. Note that this currently implies that `noyield` as well, since
yielding modifies the state of the current task, though this may be split
in the future.
- `inaccessiblememonly::UInt8`:
* `ALWAYS_TRUE`: this method does not access or modify externally accessible mutable memory.
This state corresponds to LLVM's `inaccessiblememonly` function attribute.
* `ALWAYS_FALSE`: this method may access or modify externally accessible mutable memory.
* `INACCESSIBLEMEM_OR_ARGMEMONLY`: this method does not access or modify externally accessible mutable memory,
except that it may access or modify mutable memory pointed to by its call arguments.
This may later be refined to `ALWAYS_TRUE` in a case when call arguments are known to be immutable.
This state corresponds to LLVM's `inaccessiblemem_or_argmemonly` function attribute.
- `nonoverlayed::Bool`: indicates that any methods that may be called within this method
are not defined in an [overlayed method table](@ref OverlayMethodTable).
- `noinbounds::Bool`: indicates this method can't be `:consistent` because of bounds checking.
Expand All @@ -45,6 +53,7 @@ struct Effects
nothrow::Bool
terminates::Bool
notaskstate::Bool
inaccessiblememonly::UInt8
nonoverlayed::Bool
noinbounds::Bool
function Effects(
Expand All @@ -53,6 +62,7 @@ struct Effects
nothrow::Bool,
terminates::Bool,
notaskstate::Bool,
inaccessiblememonly::UInt8,
nonoverlayed::Bool,
noinbounds::Bool = true)
return new(
Expand All @@ -61,6 +71,7 @@ struct Effects
nothrow,
terminates,
notaskstate,
inaccessiblememonly,
nonoverlayed,
noinbounds)
end
Expand All @@ -71,17 +82,21 @@ const ALWAYS_FALSE = 0x01

const CONSISTENT_IF_NOTRETURNED = 0x01 << 1

const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, true, true, true, true, true)
const EFFECTS_THROWS = Effects(ALWAYS_TRUE, true, false, true, true, true)
const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, false, false, false, false, true) # unknown mostly, but it's not overlayed at least (e.g. it's not a call)
const EFFECTS_UNKNOWN′ = Effects(ALWAYS_FALSE, false, false, false, false, false) # unknown really
# :inaccessiblememonly bits
const INACCESSIBLEMEM_OR_ARGMEMONLY = 0x01 << 1

const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, true, true, true, true, ALWAYS_TRUE, true)
const EFFECTS_THROWS = Effects(ALWAYS_TRUE, true, false, true, true, ALWAYS_TRUE, true)
const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, false, false, false, false, ALWAYS_FALSE, true) # unknown mostly, but it's not overlayed at least (e.g. it's not a call)
const EFFECTS_UNKNOWN′ = Effects(ALWAYS_FALSE, false, false, false, false, ALWAYS_FALSE, false) # unknown really

function Effects(e::Effects = EFFECTS_UNKNOWN′;
consistent::UInt8 = e.consistent,
effect_free::Bool = e.effect_free,
nothrow::Bool = e.nothrow,
terminates::Bool = e.terminates,
notaskstate::Bool = e.notaskstate,
inaccessiblememonly::UInt8 = e.inaccessiblememonly,
nonoverlayed::Bool = e.nonoverlayed,
noinbounds::Bool = e.noinbounds)
return Effects(
Expand All @@ -90,6 +105,7 @@ function Effects(e::Effects = EFFECTS_UNKNOWN′;
nothrow,
terminates,
notaskstate,
inaccessiblememonly,
nonoverlayed,
noinbounds)
end
Expand All @@ -101,6 +117,7 @@ function merge_effects(old::Effects, new::Effects)
merge_effectbits(old.nothrow, new.nothrow),
merge_effectbits(old.terminates, new.terminates),
merge_effectbits(old.notaskstate, new.notaskstate),
merge_effectbits(old.inaccessiblememonly, new.inaccessiblememonly),
merge_effectbits(old.nonoverlayed, new.nonoverlayed),
merge_effectbits(old.noinbounds, new.noinbounds))
end
Expand All @@ -113,14 +130,15 @@ function merge_effectbits(old::UInt8, new::UInt8)
end
merge_effectbits(old::Bool, new::Bool) = old & new

is_consistent(effects::Effects) = effects.consistent === ALWAYS_TRUE
is_effect_free(effects::Effects) = effects.effect_free
is_nothrow(effects::Effects) = effects.nothrow
is_terminates(effects::Effects) = effects.terminates
is_notaskstate(effects::Effects) = effects.notaskstate
is_nonoverlayed(effects::Effects) = effects.nonoverlayed
is_consistent(effects::Effects) = effects.consistent === ALWAYS_TRUE
is_effect_free(effects::Effects) = effects.effect_free
is_nothrow(effects::Effects) = effects.nothrow
is_terminates(effects::Effects) = effects.terminates
is_notaskstate(effects::Effects) = effects.notaskstate
is_inaccessiblememonly(effects::Effects) = effects.inaccessiblememonly === ALWAYS_TRUE
is_nonoverlayed(effects::Effects) = effects.nonoverlayed

# implies :notaskstate, but not explicitly checked here
# implies `is_notaskstate` & `is_inaccessiblememonly`, but not explicitly checked here
is_foldable(effects::Effects) =
is_consistent(effects) &&
is_effect_free(effects) &&
Expand All @@ -137,13 +155,16 @@ is_removable_if_unused(effects::Effects) =

is_consistent_if_notreturned(effects::Effects) = !iszero(effects.consistent & CONSISTENT_IF_NOTRETURNED)

is_inaccessiblemem_or_argmemonly(effects::Effects) = effects.inaccessiblememonly === INACCESSIBLEMEM_OR_ARGMEMONLY

function encode_effects(e::Effects)
return ((e.consistent % UInt32) << 0) |
((e.effect_free % UInt32) << 2) |
((e.nothrow % UInt32) << 3) |
((e.terminates % UInt32) << 4) |
((e.notaskstate % UInt32) << 5) |
((e.nonoverlayed % UInt32) << 6)
return ((e.consistent % UInt32) << 0) |
((e.effect_free % UInt32) << 2) |
((e.nothrow % UInt32) << 3) |
((e.terminates % UInt32) << 4) |
((e.notaskstate % UInt32) << 5) |
((e.inaccessiblememonly % UInt32) << 6) |
((e.nonoverlayed % UInt32) << 8)
end

function decode_effects(e::UInt32)
Expand All @@ -153,7 +174,8 @@ function decode_effects(e::UInt32)
_Bool((e >> 3) & 0x01),
_Bool((e >> 4) & 0x01),
_Bool((e >> 5) & 0x01),
_Bool((e >> 6) & 0x01))
UInt8((e >> 6) & 0x01),
_Bool((e >> 8) & 0x03))
end

struct EffectsOverride
Expand All @@ -163,6 +185,7 @@ struct EffectsOverride
terminates_globally::Bool
terminates_locally::Bool
notaskstate::Bool
inaccessiblememonly::Bool
end

function encode_effects_override(eo::EffectsOverride)
Expand All @@ -173,6 +196,7 @@ function encode_effects_override(eo::EffectsOverride)
eo.terminates_globally && (e |= (0x01 << 3))
eo.terminates_locally && (e |= (0x01 << 4))
eo.notaskstate && (e |= (0x01 << 5))
eo.inaccessiblememonly && (e |= (0x01 << 6))
return e
end

Expand All @@ -183,5 +207,6 @@ function decode_effects_override(e::UInt8)
(e & (0x01 << 2)) != 0x00,
(e & (0x01 << 3)) != 0x00,
(e & (0x01 << 4)) != 0x00,
(e & (0x01 << 5)) != 0x00)
(e & (0x01 << 5)) != 0x00,
(e & (0x01 << 6)) != 0x00)
end
14 changes: 8 additions & 6 deletions base/compiler/ssair/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -791,19 +791,19 @@ function show_ir(io::IO, code::Union{IRCode, CodeInfo}, config::IRShowConfig=def
end

function effectbits_letter(effects::Effects, name::Symbol, suffix::Char)
if name === :consistent
prefix = effects.consistent === ALWAYS_TRUE ? '+' :
effects.consistent === ALWAYS_FALSE ? '!' : '?'
if name === :consistent || name === :inaccessiblememonly
prefix = getfield(effects, name) === ALWAYS_TRUE ? '+' :
getfield(effects, name) === ALWAYS_FALSE ? '!' : '?'
else
prefix = getfield(effects, name) ? '+' : '!'
end
return string(prefix, suffix)
end

function effectbits_color(effects::Effects, name::Symbol)
if name === :consistent
color = effects.consistent === ALWAYS_TRUE ? :green :
effects.consistent === ALWAYS_FALSE ? :red : :yellow
if name === :consistent || name === :inaccessiblememonly
color = getfield(effects, name) === ALWAYS_TRUE ? :green :
getfield(effects, name) === ALWAYS_FALSE ? :red : :yellow
else
color = getfield(effects, name) ? :green : :red
end
Expand All @@ -821,6 +821,8 @@ function Base.show(io::IO, e::Effects)
printstyled(io, effectbits_letter(e, :terminates, 't'); color=effectbits_color(e, :terminates))
print(io, ',')
printstyled(io, effectbits_letter(e, :notaskstate, 's'); color=effectbits_color(e, :notaskstate))
print(io, ',')
printstyled(io, effectbits_letter(e, :inaccessiblememonly, 'm'); color=effectbits_color(e, :inaccessiblememonly))
print(io, ')')
e.nonoverlayed || printstyled(io, ''; color=:red)
end
Expand Down
56 changes: 50 additions & 6 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1845,11 +1845,38 @@ const _CONSISTENT_BUILTINS = Any[
(<:),
typeassert,
throw,
setfield!
setfield!,
]

const _INACCESSIBLEMEM_BUILTINS = Any[
(<:),
(===),
apply_type,
arraysize,
Core.ifelse,
sizeof,
svec,
fieldtype,
isa,
isdefined,
nfields,
throw,
tuple,
typeassert,
typeof,
]

const _ARGMEM_BUILTINS = Any[
arrayref,
arrayset,
modifyfield!,
replacefield!,
setfield!,
swapfield!,
]

const _SPECIAL_BUILTINS = Any[
Core._apply_iterate
Core._apply_iterate,
]

function isdefined_effects(argtypes::Vector{Any})
Expand Down Expand Up @@ -1891,21 +1918,31 @@ function getfield_effects(argtypes::Vector{Any}, @nospecialize(rt))
else
nothrow = getfield_nothrow(argtypes)
end
return Effects(EFFECTS_TOTAL; consistent, nothrow)
if hasintersect(widenconst(obj), Module)
inaccessiblememonly = getglobal_effects(argtypes, rt).inaccessiblememonly
elseif is_mutation_free_argtype(obj)
inaccessiblememonly = ALWAYS_TRUE
else
inaccessiblememonly = INACCESSIBLEMEM_OR_ARGMEMONLY
end
return Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly)
end

function getglobal_effects(argtypes::Vector{Any}, @nospecialize(rt))
consistent = ALWAYS_FALSE
consistent = inaccessiblememonly = ALWAYS_FALSE
nothrow = false
if getglobal_nothrow(argtypes)
nothrow = true
# typeasserts below are already checked in `getglobal_nothrow`
M, s = (argtypes[1]::Const).val::Module, (argtypes[2]::Const).val::Symbol
if isconst(M, s)
consistent = ALWAYS_TRUE
if is_mutation_free_argtype(rt)
inaccessiblememonly = ALWAYS_TRUE
end
end
end
return Effects(EFFECTS_TOTAL; consistent, nothrow)
return Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly)
end

function builtin_effects(f::Builtin, argtypes::Vector{Any}, @nospecialize(rt))
Expand All @@ -1925,7 +1962,14 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, @nospecialize(rt))
consistent = contains_is(_CONSISTENT_BUILTINS, f) ? ALWAYS_TRUE : ALWAYS_FALSE
effect_free = (contains_is(_EFFECT_FREE_BUILTINS, f) || contains_is(_PURE_BUILTINS, f))
nothrow = (!(!isempty(argtypes) && isvarargtype(argtypes[end])) && builtin_nothrow(f, argtypes, rt))
return Effects(EFFECTS_TOTAL; consistent, effect_free, nothrow)
if contains_is(_INACCESSIBLEMEM_BUILTINS, f)
inaccessiblememonly = ALWAYS_TRUE
elseif contains_is(_ARGMEM_BUILTINS, f)
inaccessiblememonly = INACCESSIBLEMEM_OR_ARGMEMONLY
else
inaccessiblememonly = ALWAYS_FALSE
end
return Effects(EFFECTS_TOTAL; consistent, effect_free, nothrow, inaccessiblememonly)
end
end

Expand Down
8 changes: 8 additions & 0 deletions base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,11 @@ function adjust_effects(sv::InferenceState)
# always throwing an error counts or never returning both count as consistent
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
end
if is_inaccessiblemem_or_argmemonly(ipo_effects) && all(1:narguments(sv)) do i::Int
return is_mutation_free_argtype(sv.slottypes[i])
end
ipo_effects = Effects(ipo_effects; inaccessiblememonly=ALWAYS_TRUE)
end
if is_consistent_if_notreturned(ipo_effects) && is_consistent_argtype(rt)
# in a case when the :consistent-cy here is only tainted by mutable allocations
# (indicated by `CONSISTENT_IF_NOTRETURNED`), we may be able to refine it if the return
Expand Down Expand Up @@ -457,6 +462,9 @@ function adjust_effects(sv::InferenceState)
if is_effect_overridden(override, :notaskstate)
ipo_effects = Effects(ipo_effects; notaskstate=true)
end
if is_effect_overridden(override, :inaccessiblememonly)
ipo_effects = Effects(ipo_effects; inaccessiblememonly=ALWAYS_TRUE)
end
end

return ipo_effects
Expand Down
19 changes: 18 additions & 1 deletion base/compiler/typeutils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,24 @@ is_immutable_argtype(@nospecialize ty) = is_immutable_type(widenconst(ignorelimi
is_immutable_type(@nospecialize ty) = _is_immutable_type(unwrap_unionall(ty))
function _is_immutable_type(@nospecialize ty)
if isa(ty, Union)
return is_immutable_type(ty.a) && is_immutable_type(ty.b)
return _is_immutable_type(ty.a) && _is_immutable_type(ty.b)
end
return !isabstracttype(ty) && !ismutabletype(ty)
end

is_mutation_free_argtype(@nospecialize argtype) =
is_mutation_free_type(widenconst(ignorelimited(argtype)))
is_mutation_free_type(@nospecialize ty) =
_is_mutation_free_type(unwrap_unionall(ty))
function _is_mutation_free_type(@nospecialize ty)
if isa(ty, Union)
return _is_mutation_free_type(ty.a) && _is_mutation_free_type(ty.b)
end
if isType(ty) || ty === DataType || ty === String || ty === Symbol || ty === SimpleVector
return true
end
# this is okay as access and modifcation on global state are tracked separately
ty === Module && return true
# TODO improve this analysis, e.g. allow `Some{Symbol}`
return isbitstype(ty)
end
Loading

0 comments on commit c094a89

Please sign in to comment.