Skip to content

Commit

Permalink
effects: improve :effect_free-ness analysis for local mutability (J…
Browse files Browse the repository at this point in the history
…uliaLang#46200)

This commit improves the accuracy of the `:effect-free`-ness analysis,
that currently doesn't handle `setfield!` call on local mutable object
pretty well.

The existing analysis taints `:effect_free`-ness upon any `setfield!`
call on mutable object because we really don't have a knowledge about
the object lifetime and so we need to conservatively take into account a
possibility of the mutable object being a global variable.

However we can "recover" `:effect_free`-cness tainted by `setfield!` on
mutable object when the newly added `:noglobal` helper effect has been
proven because in that case we can conclude that all mutable objects
accessed within the method are purely local and `setfield!` on them
are `:effect_free` (more precisely we also need to confirm that all the
call arguments are known not to be mutable global objects to derive this
conclusion).

For example now we can prove `:effect_free`-ness of the function below
and it will be DCE-eligible (and it will even be concrete-evaluated
after JuliaLang#46184):
```julia
julia> makeref() = Ref{Any}()
makeref (generic function with 1 method)

julia> setref!(ref, @nospecialize v) = ref[] = v
setref! (generic function with 1 method)

julia> @noinline function mutable_effect_free(v)
           x = makeref()
           setref!(x, v)
           x
       end
mutable_effect_free (generic function with 1 method)

julia> Base.infer_effects(mutable_effect_free, (String,))
(!c,+e,+n,+t,+s,+g)

julia> code_typed() do
           mutable_effect_free("foo") # will be DCE-ed
           nothing
       end
1-element Vector{Any}:
 CodeInfo(
1 ─     return Main.nothing
) => Nothing
```
  • Loading branch information
aviatesk committed Aug 4, 2022
1 parent 83f8bed commit 480df09
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 27 deletions.
4 changes: 2 additions & 2 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2081,7 +2081,7 @@ 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.effect_free ? ALWAYS_TRUE : effects.effect_free,
override.nothrow ? true : effects.nothrow,
override.terminates_globally ? true : effects.terminates,
override.notaskstate ? true : effects.notaskstate,
Expand Down Expand Up @@ -2185,7 +2185,7 @@ function abstract_eval_global(M::Module, s::Symbol, frame::InferenceState)
end

function handle_global_assignment!(interp::AbstractInterpreter, frame::InferenceState, lhs::GlobalRef, @nospecialize(newty))
effect_free = false
effect_free = ALWAYS_FALSE
nothrow = global_assignment_nothrow(lhs.mod, lhs.name, newty)
inaccessiblememonly = ALWAYS_FALSE
merge_effects!(frame, Effects(EFFECTS_TOTAL; effect_free, nothrow, inaccessiblememonly))
Expand Down
51 changes: 31 additions & 20 deletions base/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ following meanings:
* `CONSISTENT_IF_NOTRETURNED`: the `:consistent`-cy of this method can later be refined to
`ALWAYS_TRUE` in a case when the return value of this method never involves newly
allocated mutable objects.
* `CONSISTENT_IF_INACCESSIBLEMEMONLY`: the `:consistent`-cy of this method can later be refined to
`ALWAYS_TRUE` in a case when `:inaccessiblememonly` is proven.
- `effect_free::Bool`: this method is free from externally semantically visible side effects.
* `CONSISTENT_IF_INACCESSIBLEMEMONLY`: the `:consistent`-cy of this method can later be
refined to `ALWAYS_TRUE` in a case when `:inaccessiblememonly` is proven.
- `effect_free::UInt8`:
* `ALWAYS_TRUE`: this method is free from externally semantically visible side effects.
* `ALWAYS_FALSE`: this method may not be free from externally semantically visible side effects, and there is
no need for further analysis with respect to this effect property as this conclusion
will not be refined anyway.
* `EFFECT_FREE_IF_INACCESSIBLEMEMONLY`: the `:effect-free`-ness of this method can later be
refined to `ALWAYS_TRUE` in a case when `:inaccessiblememonly` is proven.
- `nothrow::Bool`: this method is guaranteed to not throw an exception.
- `terminates::Bool`: this method is guaranteed to terminate.
- `notaskstate::Bool`: this method does not access any state bound to the current
Expand Down Expand Up @@ -51,7 +57,7 @@ analysis on each statement usually taint the global conclusion conservatively.
"""
struct Effects
consistent::UInt8
effect_free::Bool
effect_free::UInt8
nothrow::Bool
terminates::Bool
notaskstate::Bool
Expand All @@ -60,7 +66,7 @@ struct Effects
noinbounds::Bool
function Effects(
consistent::UInt8,
effect_free::Bool,
effect_free::UInt8,
nothrow::Bool,
terminates::Bool,
notaskstate::Bool,
Expand All @@ -86,17 +92,20 @@ const ALWAYS_FALSE = 0x01
const CONSISTENT_IF_NOTRETURNED = 0x01 << 1
const CONSISTENT_IF_INACCESSIBLEMEMONLY = 0x01 << 2

# :effect_free-ness bits
const EFFECT_FREE_IF_INACCESSIBLEMEMONLY = 0x01 << 1

# :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
const EFFECTS_TOTAL = Effects(ALWAYS_TRUE, ALWAYS_TRUE, true, true, true, ALWAYS_TRUE, true)
const EFFECTS_THROWS = Effects(ALWAYS_TRUE, ALWAYS_TRUE, false, true, true, ALWAYS_TRUE, true)
const EFFECTS_UNKNOWN = Effects(ALWAYS_FALSE, ALWAYS_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, ALWAYS_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,
effect_free::UInt8 = e.effect_free,
nothrow::Bool = e.nothrow,
terminates::Bool = e.terminates,
notaskstate::Bool = e.notaskstate,
Expand Down Expand Up @@ -135,7 +144,7 @@ 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_effect_free(effects::Effects) = effects.effect_free === ALWAYS_TRUE
is_nothrow(effects::Effects) = effects.nothrow
is_terminates(effects::Effects) = effects.terminates
is_notaskstate(effects::Effects) = effects.notaskstate
Expand All @@ -160,27 +169,29 @@ is_removable_if_unused(effects::Effects) =
is_consistent_if_notreturned(effects::Effects) = !iszero(effects.consistent & CONSISTENT_IF_NOTRETURNED)
is_consistent_if_inaccessiblememonly(effects::Effects) = !iszero(effects.consistent & CONSISTENT_IF_INACCESSIBLEMEMONLY)

is_effect_free_if_inaccessiblememonly(effects::Effects) = !iszero(effects.effect_free & EFFECT_FREE_IF_INACCESSIBLEMEMONLY)

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) << 3) |
((e.nothrow % UInt32) << 4) |
((e.terminates % UInt32) << 5) |
((e.notaskstate % UInt32) << 6) |
((e.inaccessiblememonly % UInt32) << 7) |
((e.nonoverlayed % UInt32) << 9)
((e.nothrow % UInt32) << 5) |
((e.terminates % UInt32) << 6) |
((e.notaskstate % UInt32) << 7) |
((e.inaccessiblememonly % UInt32) << 8) |
((e.nonoverlayed % UInt32) << 10)
end

function decode_effects(e::UInt32)
return Effects(
UInt8((e >> 0) & 0x07),
_Bool((e >> 3) & 0x01),
_Bool((e >> 4) & 0x01),
UInt8((e >> 3) & 0x03),
_Bool((e >> 5) & 0x01),
_Bool((e >> 6) & 0x01),
UInt8((e >> 7) & 0x03),
_Bool((e >> 9) & 0x01))
_Bool((e >> 7) & 0x01),
UInt8((e >> 8) & 0x03),
_Bool((e >> 10) & 0x01))
end

struct EffectsOverride
Expand Down
4 changes: 2 additions & 2 deletions base/compiler/ssair/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ 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 || name === :inaccessiblememonly
if name === :consistent || name === :effect_free || name === :inaccessiblememonly
prefix = getfield(effects, name) === ALWAYS_TRUE ? '+' :
getfield(effects, name) === ALWAYS_FALSE ? '!' : '?'
else
Expand All @@ -801,7 +801,7 @@ function effectbits_letter(effects::Effects, name::Symbol, suffix::Char)
end

function effectbits_color(effects::Effects, name::Symbol)
if name === :consistent || name === :inaccessiblememonly
if name === :consistent || name === :effect_free || name === :inaccessiblememonly
color = getfield(effects, name) === ALWAYS_TRUE ? :green :
getfield(effects, name) === ALWAYS_FALSE ? :red : :yellow
else
Expand Down
10 changes: 8 additions & 2 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1960,7 +1960,13 @@ function builtin_effects(f::Builtin, argtypes::Vector{Any}, @nospecialize(rt))
return getglobal_effects(argtypes, rt)
else
consistent = contains_is(_CONSISTENT_BUILTINS, f) ? ALWAYS_TRUE : ALWAYS_FALSE
effect_free = (contains_is(_EFFECT_FREE_BUILTINS, f) || contains_is(_PURE_BUILTINS, f))
if f === setfield! || f === arrayset
effect_free = EFFECT_FREE_IF_INACCESSIBLEMEMONLY
elseif contains_is(_EFFECT_FREE_BUILTINS, f) || contains_is(_PURE_BUILTINS, f)
effect_free = ALWAYS_TRUE
else
effect_free = ALWAYS_FALSE
end
nothrow = (!(!isempty(argtypes) && isvarargtype(argtypes[end])) && builtin_nothrow(f, argtypes, rt))
if contains_is(_INACCESSIBLEMEM_BUILTINS, f)
inaccessiblememonly = ALWAYS_TRUE
Expand Down Expand Up @@ -2143,7 +2149,7 @@ function intrinsic_effects(f::IntrinsicFunction, argtypes::Vector{Any})
f === Intrinsics.have_fma || # this one depends on the runtime environment
f === Intrinsics.cglobal # cglobal lookup answer changes at runtime
) ? ALWAYS_TRUE : ALWAYS_FALSE
effect_free = !(f === Intrinsics.pointerset)
effect_free = !(f === Intrinsics.pointerset) ? ALWAYS_TRUE : ALWAYS_FALSE
nothrow = (!(!isempty(argtypes) && isvarargtype(argtypes[end])) && intrinsic_nothrow(f, argtypes))

return Effects(EFFECTS_TOTAL; consistent, effect_free, nothrow)
Expand Down
11 changes: 10 additions & 1 deletion base/compiler/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,15 @@ function adjust_effects(sv::InferenceState)
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_FALSE)
end
end
if is_effect_free_if_inaccessiblememonly(ipo_effects)
if is_inaccessiblememonly(ipo_effects)
effect_free = ipo_effects.effect_free & ~EFFECT_FREE_IF_INACCESSIBLEMEMONLY
ipo_effects = Effects(ipo_effects; effect_free)
elseif is_inaccessiblemem_or_argmemonly(ipo_effects)
else # `:inaccessiblememonly` is already tainted, there will be no chance to refine this
ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_FALSE)
end
end

# override the analyzed effects using manually annotated effect settings
def = sv.linfo.def
Expand All @@ -460,7 +469,7 @@ function adjust_effects(sv::InferenceState)
ipo_effects = Effects(ipo_effects; consistent=ALWAYS_TRUE)
end
if is_effect_overridden(override, :effect_free)
ipo_effects = Effects(ipo_effects; effect_free=true)
ipo_effects = Effects(ipo_effects; effect_free=ALWAYS_TRUE)
end
if is_effect_overridden(override, :nothrow)
ipo_effects = Effects(ipo_effects; nothrow=true)
Expand Down
59 changes: 59 additions & 0 deletions test/compiler/effects.jl
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,62 @@ global inconsistent_condition_ref = Ref{Bool}(false)
return 1
end
end |> !Core.Compiler.is_consistent

# the `:inaccessiblememonly` helper effect allows us to prove `:effect_free`-ness of frames
# including `setfield!` modifying local mutable object

const global_ref = Ref{Any}()
global const global_bit::Int = 42
makeref() = Ref{Any}()
setref!(ref, @nospecialize v) = ref[] = v

@noinline function removable_if_unused1()
x = makeref()
setref!(x, 42)
x
end
@noinline function removable_if_unused2()
x = makeref()
setref!(x, global_bit)
x
end
for f = Any[removable_if_unused1, removable_if_unused2]
effects = Base.infer_effects(f)
@test Core.Compiler.is_inaccessiblememonly(effects)
@test Core.Compiler.is_effect_free(effects)
@test Core.Compiler.is_removable_if_unused(effects)
@test @eval fully_eliminated() do
$f()
nothing
end
end
@noinline function removable_if_unused3(v)
x = makeref()
setref!(x, v)
x
end
let effects = Base.infer_effects(removable_if_unused3, (Int,))
@test Core.Compiler.is_inaccessiblememonly(effects)
@test Core.Compiler.is_effect_free(effects)
@test Core.Compiler.is_removable_if_unused(effects)
end
@test fully_eliminated((Int,)) do v
removable_if_unused3(v)
nothing
end

@noinline function unremovable_if_unused1!(x)
setref!(x, 42)
end
@test !Core.Compiler.is_removable_if_unused(Base.infer_effects(unremovable_if_unused1!, (typeof(global_ref),)))
@test !Core.Compiler.is_removable_if_unused(Base.infer_effects(unremovable_if_unused1!, (Any,)))

@noinline function unremovable_if_unused2!()
setref!(global_ref, 42)
end
@test !Core.Compiler.is_removable_if_unused(Base.infer_effects(unremovable_if_unused2!))

@noinline function unremovable_if_unused3!()
getfield(@__MODULE__, :global_ref)[] = nothing
end
@test !Core.Compiler.is_removable_if_unused(Base.infer_effects(unremovable_if_unused3!))

0 comments on commit 480df09

Please sign in to comment.