/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
Compiler/src/ssair/passes.jl
2 762 строки
113 KB
Shuhei Kadowaki
inference: Simplify `PartialTask` and restore per-builtin `CallInfo` types (#62548)
31 июл 2026, 03:22
Не верифицирован
31 июл 2026, 03:22
0d72726
Код
Авторство
О чём код?
# This file is a part of Julia. License is MIT: https://julialang.org/license function is_known_call(@nospecialize(x), @nospecialize(func), ir::Union{IRCode,IncrementalCompact}) isexpr(x, :call) || return false arg = x.args[1] isa(arg, GlobalRef) && return globalref_singleton(arg, ir) === func return singleton_type(argextype(arg, ir)) === func end function is_known_invoke_or_call(@nospecialize(x), @nospecialize(func), ir::Union{IRCode,IncrementalCompact}) isinvoke = isexpr(x, :invoke) (isinvoke || isexpr(x, :call)) || return false narg = isinvoke ? 2 : 1 length(x.args) < narg && return false arg = x.args[narg] isa(arg, GlobalRef) && return globalref_singleton(arg, ir) === func return singleton_type(argextype(arg, ir)) === func end struct SSAUse kind::Symbol idx::Int end GetfieldUse(idx::Int) = SSAUse(:getfield, idx) PreserveUse(idx::Int) = SSAUse(:preserve, idx) NoPreserve() = SSAUse(:nopreserve, 0) IsdefinedUse(idx::Int) = SSAUse(:isdefined, idx) FinalizerUse(idx::Int) = SSAUse(:finalizer, idx) """ du::SSADefUse This struct keeps track of all uses of some mutable struct allocated in the current function: - `du.uses::Vector{SSAUse}` are some "usages" (like `getfield`) of the struct - `du.defs::Vector{Int}` are all instances of `setfield!` on the struct The terminology refers to the uses/defs of the "slot bundle" that the mutable struct represents. `du.uses` tracks all instances of `getfield` and `isdefined` calls on the struct. Additionally it also tracks all instances of a `:foreigncall` that preserves of this mutable struct. Somewhat counterintuitively, we don't actually need to make sure that the struct itself is live (or even allocated) at a `ccall` site. If there are no other places where the struct escapes (and thus e.g. where its address is taken), it need not be allocated. We do however, need to make sure to preserve any elements of this struct. """ struct SSADefUse uses::Vector{SSAUse} defs::Vector{Int} end SSADefUse() = SSADefUse(SSAUse[], Int[]) function compute_live_ins(cfg::CFG, du::SSADefUse) uses = Int[] for use in du.uses use.kind === :isdefined && continue # filter out `isdefined` usages push!(uses, use.idx) end compute_live_ins(cfg, sort!(du.defs), uses) end function try_compute_field(ir::Union{IncrementalCompact,IRCode}, @nospecialize(field)) # fields are usually literals, handle them manually if isa(field, QuoteNode) field = field.value elseif isa(field, Int) || isa(field, Symbol) else # try to resolve other constants, e.g. global reference field = argextype(field, ir) if isa(field, Const) field = field.val else return nothing end end return isa(field, Union{Int, Symbol}) ? field : nothing end # assume `stmt` is a call of `getfield`/`setfield!`/`isdefined` function try_compute_fieldidx_stmt(ir::Union{IncrementalCompact,IRCode}, stmt::Expr, @nospecialize(typ)) field = try_compute_field(ir, stmt.args[3]) return try_compute_fieldidx(typ, field) end function find_curblock(domtree::DomTree, allblocks::BitSet, curblock::Int) # TODO: This can be much faster by looking at current level and only # searching for those blocks in a sorted order while curblock ∉ allblocks && curblock ≠ 0 curblock = domtree.idoms_bb[curblock] end return curblock end function val_for_def_expr(ir::IRCode, def::Int, fidx::Int) ex = ir[SSAValue(def)][:stmt] if isexpr(ex, :new) return ex.args[1+fidx] else @assert is_known_call(ex, setfield!, ir) return ex.args[4] end end function compute_value_for_block(ir::IRCode, domtree::DomTree, allblocks::BitSet, du::SSADefUse, phinodes::IdDict{Int, SSAValue}, fidx::Int, curblock::Int) curblock = find_curblock(domtree, allblocks, curblock) def = 0 for stmt in du.defs if block_for_inst(ir.cfg, stmt) == curblock def = max(def, stmt) end end def == 0 ? phinodes[curblock] : val_for_def_expr(ir, def, fidx) end function compute_value_for_use(ir::IRCode, domtree::DomTree, allblocks::BitSet, du::SSADefUse, phinodes::IdDict{Int, SSAValue}, fidx::Int, use::Int) def, useblock, curblock = find_def_for_use(ir, domtree, allblocks, du, use) if def == 0 if !haskey(phinodes, curblock) # If this happens, we need to search the predecessors for defs. Which # one doesn't matter - if it did, we'd have had a phinode return compute_value_for_block(ir, domtree, allblocks, du, phinodes, fidx, first(ir.cfg.blocks[useblock].preds)) end # The use is the phinode return phinodes[curblock] else return val_for_def_expr(ir, def, fidx) end end # even when the allocation contains an uninitialized field, we try an extra effort to check # if this load at `idx` have any "safe" `setfield!` calls that define the field function has_safe_def( ir::IRCode, domtree::DomTree, allblocks::BitSet, du::SSADefUse, newidx::Int, idx::Int) def, _, _ = find_def_for_use(ir, domtree, allblocks, du, idx) # will throw since we already checked this `:new` site doesn't define this field def == newidx && return false # found a "safe" definition def ≠ 0 && return true # we may still be able to replace this load with `PhiNode` # examine if all predecessors of `block` have any "safe" definition block = block_for_inst(ir, idx) seen = BitSet(block) worklist = BitSet(ir.cfg.blocks[block].preds) isempty(worklist) && return false while !isempty(worklist) pred = pop!(worklist) # if this block has already been examined, bail out to avoid infinite cycles pred in seen && return false idx = last(ir.cfg.blocks[pred].stmts) # NOTE `idx` isn't a load, thus we can use inclusive condition within the `find_def_for_use` def, _, _ = find_def_for_use(ir, domtree, allblocks, du, idx, true) # will throw since we already checked this `:new` site doesn't define this field def == newidx && return false push!(seen, pred) # found a "safe" definition for this predecessor def ≠ 0 && continue # check for the predecessors of this predecessor for newpred in ir.cfg.blocks[pred].preds push!(worklist, newpred) end end return true end # find the first dominating def for the given use function find_def_for_use( ir::IRCode, domtree::DomTree, allblocks::BitSet, du::SSADefUse, use::Int, inclusive::Bool=false) useblock = block_for_inst(ir.cfg, use) curblock = find_curblock(domtree, allblocks, useblock) local def = 0 for idx in du.defs if block_for_inst(ir.cfg, idx) == curblock if curblock != useblock # Find the last def in this block def = max(def, idx) else # Find the last def before our use if inclusive def = max(def, idx ≤ use ? idx : 0) else def = max(def, idx < use ? idx : 0) end end end end return def, useblock, curblock end function collect_leaves(compact::IncrementalCompact, @nospecialize(val), @nospecialize(typeconstraint), 𝕃ₒ::AbstractLattice, predecessors::Pre = ((@nospecialize(def), _compact::IncrementalCompact) -> isa(def, PhiNode) ? def.values : nothing)) where {Pre} if isa(val, Union{OldSSAValue, SSAValue}) val, typeconstraint = simple_walk_constraint(compact, val, typeconstraint) end return walk_to_defs(compact, val, typeconstraint, predecessors, 𝕃ₒ) end abstract type WalkerCallback end struct TrivialWalker <: WalkerCallback end (::TrivialWalker)(@nospecialize(_def), @nospecialize(_defssa::AnySSAValue)) = nothing struct PiWalker <: WalkerCallback end function (::PiWalker)(@nospecialize(def), @nospecialize(_defssa::AnySSAValue)) if isa(def, PiNode) return LiftedValue(def.val) end return nothing end function simple_walk(compact::IncrementalCompact, @nospecialize(defssa::AnySSAValue), walker_callback::WalkerCallback=TrivialWalker()) while true if isa(defssa, OldSSAValue) if already_inserted(compact, defssa) rename = compact.ssa_rename[defssa.id] if isa(rename, Refined) rename = rename.val end if isa(rename, AnySSAValue) defssa = rename continue end return rename end end def = compact[defssa][:stmt] if isa(def, AnySSAValue) walker_callback(def, defssa) if isa(def, SSAValue) is_old(compact, defssa) && (def = OldSSAValue(def.id)) end defssa = def elseif isa(def, Union{PhiNode, PhiCNode, GlobalRef}) return defssa else new_def = walker_callback(def, defssa) if new_def === nothing return defssa end new_def = new_def.val if !isa(new_def, AnySSAValue) return new_def elseif isa(new_def, SSAValue) is_old(compact, defssa) && (new_def = OldSSAValue(new_def.id)) end defssa = new_def end end end mutable struct TypeConstrainingWalker <: WalkerCallback typeconstraint::Any TypeConstrainingWalker(@nospecialize(typeconstraint::Any)) = new(typeconstraint) end function (walker_callback::TypeConstrainingWalker)(@nospecialize(def), @nospecialize(_defssa::AnySSAValue)) if isa(def, PiNode) walker_callback.typeconstraint = typeintersect(walker_callback.typeconstraint, widenconst(def.typ)) return LiftedValue(def.val) end return nothing end function simple_walk_constraint(compact::IncrementalCompact, @nospecialize(val::AnySSAValue), @nospecialize(typeconstraint)) def = simple_walk(compact, val, TypeConstrainingWalker(typeconstraint)) return Pair{Any, Any}(def, typeconstraint) end """ walk_to_defs(compact, val, typeconstraint, predecessors) Starting at `val` walk use-def chains to get all the leaves feeding into this `val` (pruning those leaves ruled out by path conditions). `predecessors(def, compact)` is a callback which should return the set of possible predecessors for a "phi-like" node (PhiNode or Core.ifelse) or `nothing` otherwise. """ function walk_to_defs(compact::IncrementalCompact, @nospecialize(defssa), @nospecialize(typeconstraint), predecessors::Pre, 𝕃ₒ::AbstractLattice) where {Pre} visited_philikes = AnySSAValue[] isa(defssa, AnySSAValue) || return Any[defssa], visited_philikes def = compact[defssa][:stmt] if predecessors(def, compact) === nothing return Any[defssa], visited_philikes end visited_constraints = IdDict{AnySSAValue, Any}() worklist_defs = AnySSAValue[] worklist_constraints = Any[] leaves = Any[] push!(worklist_defs, defssa) push!(worklist_constraints, typeconstraint) while !isempty(worklist_defs) defssa = pop!(worklist_defs) typeconstraint = pop!(worklist_constraints) visited_constraints[defssa] = typeconstraint def = compact[defssa][:stmt] values = predecessors(def, compact) if values !== nothing if isa(def, PhiNode) || length(values) > 1 push!(visited_philikes, defssa) end possible_predecessors = Int[] for n in 1:length(values) isassigned(values, n) || continue val = values[n] if is_old(compact, defssa) && isa(val, SSAValue) val = OldSSAValue(val.id) end edge_typ = argextype_widened(val, compact) hasintersect(edge_typ, typeconstraint) || continue push!(possible_predecessors, n) end for n in possible_predecessors val = values[n] if is_old(compact, defssa) && isa(val, SSAValue) val = OldSSAValue(val.id) end if isa(val, AnySSAValue) new_def, new_constraint = simple_walk_constraint(compact, val, typeconstraint) if isa(new_def, AnySSAValue) if !haskey(visited_constraints, new_def) push!(worklist_defs, new_def) push!(worklist_constraints, new_constraint) elseif !(new_constraint <: visited_constraints[new_def]) # We have reached the same definition via a different # path, with a different type constraint. We may have # to redo some work here with the wider typeconstraint push!(worklist_defs, new_def) push!(worklist_constraints, tmerge(𝕃ₒ, new_constraint, visited_constraints[new_def])) end continue end val = new_def end if def === val # This shouldn't really ever happen, but # patterns like this can occur in dead code, # so bail out. break else push!(leaves, val) end continue end else push!(leaves, defssa) end end return leaves, visited_philikes end function record_immutable_preserve!(new_preserves::Vector{Any}, def::Expr, compact::IncrementalCompact) args = isexpr(def, :new) ? def.args : def.args[2:end] for i = 1:length(args) arg = args[i] if !isbitstype(argextype_widened(arg, compact)) push!(new_preserves, arg) end end end function already_inserted(compact::IncrementalCompact, old::OldSSAValue) already_inserted_ssa(compact, compact.idx-1)(0, old) end function _already_inserted_ssa(compact::IncrementalCompact, processed_idx::Int, phi_arg::Int, old::OldSSAValue) id = old.id if id <= length(compact.ir.stmts) return id <= processed_idx end id -= length(compact.ir.stmts) if id <= length(compact.ir.new_nodes) return _already_inserted_ssa(compact, processed_idx, phi_arg, OldSSAValue(compact.ir.new_nodes.info[id].pos)) end id -= length(compact.ir.new_nodes) @assert id <= length(compact.pending_nodes) return !(id in compact.pending_perm) end function already_inserted_ssa(compact::IncrementalCompact, processed_idx::Int) return (phi_arg::Int, old::OldSSAValue) -> _already_inserted_ssa(compact, processed_idx, phi_arg, old) end function is_pending(compact::IncrementalCompact, old::OldSSAValue) return old.id > length(compact.ir.stmts) + length(compact.ir.new_nodes) end function is_getfield_captures(@nospecialize(def), compact::IncrementalCompact, 𝕃ₒ::AbstractLattice) isa(def, Expr) || return false length(def.args) >= 3 || return false is_known_call(def, getfield, compact) || return false which = argextype(def.args[3], compact) isa(which, Const) || return false which.val === :captures || return false oc = argextype(def.args[2], compact) return ⊑(𝕃ₒ, oc, Core.OpaqueClosure) end struct LiftedValue val LiftedValue(@nospecialize val) = new(val) end const LiftedLeaves = IdDict{Any, Union{Nothing,LiftedValue}} const LiftedDefs = IdDict{Any, Bool} # try to compute lifted values that can replace `getfield(x, field)` call # where `x` is an immutable struct that are defined at any of `leaves` function lift_leaves(compact::IncrementalCompact, field::Int, leaves::Vector{Any}, 𝕃ₒ::AbstractLattice) # For every leaf, the lifted value lifted_leaves = LiftedLeaves() maybe_undef = false for i = 1:length(leaves) leaf = leaves[i] cache_key = leaf if isa(leaf, AnySSAValue) (def, leaf) = walk_to_def(compact, leaf) if is_known_call(def, tuple, compact) && 1 ≤ field < length(def.args) lift_arg!(compact, leaf, cache_key, def, 1+field, lifted_leaves) continue elseif isexpr(def, :new) typ = unwrap_unionall(widenconst(types(compact)[leaf])) (isa(typ, DataType) && !isabstracttype(typ)) || return nothing if ismutabletype(typ) isconst(typ, field) || return nothing end if length(def.args) < 1+field if field > fieldcount(typ) return nothing end ftyp = fieldtype(typ, field) if !isbitstype(ftyp) # On this branch, this will be a guaranteed UndefRefError. # We use the regular undef mechanic to lift this to a boolean slot maybe_undef = true lifted_leaves[cache_key] = nothing continue end return nothing end lift_arg!(compact, leaf, cache_key, def, 1+field, lifted_leaves) continue # NOTE we can enable this, but most `:splatnew` expressions are transformed into # `:new` expressions by the inliner # elseif isexpr(def, :splatnew) && length(def.args) == 2 && isa(def.args[2], AnySSAValue) # tplssa = def.args[2]::AnySSAValue # tplexpr = compact[tplssa][:stmt] # if is_known_call(tplexpr, tuple, compact) && 1 ≤ field < length(tplexpr.args) # lift_arg!(compact, tplssa, cache_key, tplexpr, 1+field, lifted_leaves) # continue # end # return nothing elseif is_getfield_captures(def, compact, 𝕃ₒ) # Walk to new_opaque_closure ocleaf = def.args[2] if isa(ocleaf, AnySSAValue) ocleaf = simple_walk(compact, ocleaf) end ocdef, _ = walk_to_def(compact, ocleaf) if isexpr(ocdef, :new_opaque_closure) && isa(field, Int) && 1 ≤ field ≤ length(ocdef.args)-5 lift_arg!(compact, leaf, cache_key, ocdef, 5+field, lifted_leaves) continue end return nothing else typ = argextype(leaf, compact) if !isa(typ, Const) # TODO: (disabled since #27126) # If the leaf is an old ssa value, insert a getfield here # We will revisit this getfield later when compaction gets # to the appropriate point. # N.B.: This can be a bit dangerous because it can lead to # infinite loops if we accidentally insert a node just ahead # of where we are return nothing end leaf = typ.val # Fall through to below end elseif isa(leaf, QuoteNode) leaf = leaf.value elseif isa(leaf, GlobalRef) typ = argextype(leaf, compact) if isa(typ, Const) leaf = typ.val else return nothing end elseif isa(leaf, Argument) || isa(leaf, Expr) return nothing end ismutable(leaf) && return nothing isdefined(leaf, field) || return nothing val = getfield(leaf, field) is_inlineable_constant(val) || return nothing lifted_leaves[cache_key] = LiftedValue(quoted(val)) end return lifted_leaves, maybe_undef end function lift_arg!( compact::IncrementalCompact, @nospecialize(leaf), @nospecialize(cache_key), stmt::Expr, argidx::Int, lifted_leaves::LiftedLeaves) lifted = stmt.args[argidx] if is_old(compact, leaf) && isa(lifted, SSAValue) lifted = OldSSAValue(lifted.id) if already_inserted(compact, lifted) new_lifted = compact.ssa_rename[lifted.id] if isa(new_lifted, Refined) new_lifted = new_lifted.val end # Special case: If lifted happens to be the statement we're currently processing, # leave it as old SSAValue in case we decide to handle this in the renamer if !isa(new_lifted, SSAValue) || new_lifted != SSAValue(compact.result_idx-1) lifted = new_lifted end end end lifted_leaves[cache_key] = LiftedValue(lifted) return nothing end function walk_to_def(compact::IncrementalCompact, @nospecialize(leaf)) if isa(leaf, OldSSAValue) && already_inserted(compact, leaf) leaf = compact.ssa_rename[leaf.id] if isa(leaf, Refined) leaf = leaf.val end if isa(leaf, AnySSAValue) leaf = simple_walk(compact, leaf) end if isa(leaf, AnySSAValue) def = compact[leaf][:stmt] else def = leaf end elseif isa(leaf, AnySSAValue) def = compact[leaf][:stmt] else def = leaf end return Pair{Any, Any}(def, leaf) end """ lift_comparison!(cmp, compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice) Replaces `cmp(φ(x, y)::Union{X,Y}, constant)` by `φ(cmp(x, constant), cmp(y, constant))`, where `cmp(x, constant)` and `cmp(y, constant)` can be replaced with constant `Bool`eans. It helps codegen avoid generating expensive code for `cmp` with `Union` types. In particular, this is supposed to improve the performance of the iteration protocol: ```julia while x !== nothing x = iterate(...)::Union{Nothing,Tuple{Any,Any}} end ``` """ function lift_comparison! end function lift_comparison!(::typeof(===), compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice) args = stmt.args length(args) == 3 || return lhs, rhs = args[2], args[3] vl = argextype(lhs, compact) vr = argextype(rhs, compact) if isa(vl, Const) isa(vr, Const) && return val = rhs cmp = vl elseif isa(vr, Const) val = lhs cmp = vr else return end lift_comparison_leaves!(egal_tfunc, compact, val, cmp, idx, 𝕃ₒ) end function lift_comparison!(::typeof(isa), compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice) args = stmt.args length(args) == 3 || return cmp = argextype(args[3], compact) val = args[2] lift_comparison_leaves!(isa_tfunc, compact, val, cmp, idx, 𝕃ₒ) end function lift_comparison!(::typeof(isdefined), compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice) args = stmt.args length(args) == 3 || return cmp = argextype(args[3], compact) isa(cmp, Const) || return # `isdefined_tfunc` won't return Const val = args[2] lift_comparison_leaves!(isdefined_tfunc, compact, val, cmp, idx, 𝕃ₒ) end function phi_or_ifelse_predecessors(@nospecialize(def), compact::IncrementalCompact) isa(def, PhiNode) && return def.values is_known_call(def, Core.ifelse, compact) && return def.args[3:4] return nothing end function lift_comparison_leaves!(@specialize(tfunc), compact::IncrementalCompact, @nospecialize(val), @nospecialize(cmp), idx::Int, 𝕃ₒ::AbstractLattice) typeconstraint = argextype_widened(val, compact) if isa(val, Union{OldSSAValue, SSAValue}) val, typeconstraint = simple_walk_constraint(compact, val, typeconstraint) end isa(typeconstraint, Union) || return # bail out if there won't be a good chance for lifting leaves, visited_philikes = collect_leaves(compact, val, typeconstraint, 𝕃ₒ, phi_or_ifelse_predecessors) length(leaves) ≤ 1 && return # bail out if we don't have multiple leaves # check if we can evaluate the comparison for each one of the leaves lifted_leaves = nothing for i = 1:length(leaves) leaf = leaves[i] result = tfunc(𝕃ₒ, argextype(leaf, compact), cmp) if isa(result, Const) if lifted_leaves === nothing lifted_leaves = LiftedLeaves() end lifted_leaves[leaf] = LiftedValue(result.val) else return # TODO In some cases it might be profitable to hoist the comparison here end end (lifted_val, nest) = perform_lifting!(compact, visited_philikes, cmp, nothing, Bool, lifted_leaves::LiftedLeaves, val, nothing) compact[idx] = (lifted_val::LiftedValue).val finish_phi_nest!(compact, nest) end struct IfElseCall call::Expr end # An intermediate data structure used for lifting expressions through a # "phi-like" instruction (either a PhiNode or a call to Core.ifelse) struct LiftedPhilike ssa::AnySSAValue node::Union{PhiNode,IfElseCall} need_argupdate::Bool end struct SkipToken end; const SKIP_TOKEN = SkipToken() function lifted_value(compact::IncrementalCompact, @nospecialize(old_node_ssa::AnySSAValue), @nospecialize(old_value), lifted_philikes::Vector{LiftedPhilike}, lifted_leaves::Union{LiftedLeaves, LiftedDefs}, reverse_mapping::IdDict{AnySSAValue, Int}, walker_callback::WalkerCallback) val = old_value if is_old(compact, old_node_ssa) && isa(val, SSAValue) val = OldSSAValue(val.id) end if isa(val, AnySSAValue) val = simple_walk(compact, val, LiftedLeaveWalker(lifted_leaves, reverse_mapping, walker_callback)) end if val in keys(lifted_leaves) lifted_val = lifted_leaves[val] if isa(lifted_leaves, LiftedDefs) return lifted_val end lifted_val === nothing && return UNDEF_TOKEN val = lifted_val.val if isa(val, AnySSAValue) val = simple_walk(compact, val, PiWalker()) end return val elseif isa(val, AnySSAValue) && val in keys(reverse_mapping) return lifted_philikes[reverse_mapping[val]].ssa else return SKIP_TOKEN # Probably ignored by path condition, skip this end end function is_old(compact, @nospecialize(old_node_ssa)) isa(old_node_ssa, OldSSAValue) || return false is_pending(compact, old_node_ssa) && return false already_inserted(compact, old_node_ssa) && return false return true end struct PhiNest{C<:WalkerCallback} visited_philikes::Vector{AnySSAValue} lifted_philikes::Vector{LiftedPhilike} lifted_leaves::Union{LiftedLeaves, LiftedDefs} reverse_mapping::IdDict{AnySSAValue, Int} walker_callback::C end function finish_phi_nest!(compact::IncrementalCompact, nest::PhiNest) (;visited_philikes, lifted_philikes, lifted_leaves, reverse_mapping, walker_callback) = nest nphilikes = length(lifted_philikes) # Fix up arguments for i = 1:nphilikes (old_node_ssa, lf) = visited_philikes[i], lifted_philikes[i] lf.need_argupdate || continue should_count = !isa(lf.ssa, OldSSAValue) || already_inserted(compact, lf.ssa) lfnode = lf.node if isa(lfnode, PhiNode) old_node = compact[old_node_ssa][:stmt]::PhiNode new_node = lfnode for i = 1:length(old_node.values) isassigned(old_node.values, i) || continue val = lifted_value(compact, old_node_ssa, old_node.values[i], lifted_philikes, lifted_leaves, reverse_mapping, walker_callback) val !== SKIP_TOKEN && push!(new_node.edges, old_node.edges[i]) if val === UNDEF_TOKEN resize!(new_node.values, length(new_node.values)+1) elseif val !== SKIP_TOKEN should_count && _count_added_node!(compact, val) push!(new_node.values, val) end end elseif isa(lfnode, IfElseCall) old_node = compact[old_node_ssa][:stmt]::Expr then_result, else_result = old_node.args[3], old_node.args[4] then_result = lifted_value(compact, old_node_ssa, then_result, lifted_philikes, lifted_leaves, reverse_mapping, walker_callback) else_result = lifted_value(compact, old_node_ssa, else_result, lifted_philikes, lifted_leaves, reverse_mapping, walker_callback) # In cases where the Core.ifelse condition is statically-known, e.g., thanks # to a PiNode from a guarding conditional, replace with the remaining branch. if then_result === SKIP_TOKEN || else_result === SKIP_TOKEN only_result = (then_result === SKIP_TOKEN) ? else_result : then_result # Replace Core.ifelse(%cond, %a, %b) with %a compact[lf.ssa] = only_result # Note: Core.ifelse(%cond, %a, %b) has observable effects (!nothrow), but since # we have not deleted the preceding statement that this was derived from, this # replacement is safe, i.e. it will not affect the effects observed. continue end @assert then_result !== SKIP_TOKEN && then_result !== UNDEF_TOKEN @assert else_result !== SKIP_TOKEN && else_result !== UNDEF_TOKEN if should_count _count_added_node!(compact, then_result) _count_added_node!(compact, else_result) end push!(lfnode.call.args, then_result) push!(lfnode.call.args, else_result) end end end struct LiftedLeaveWalker{C<:WalkerCallback} <: WalkerCallback lifted_leaves::Union{LiftedLeaves, LiftedDefs} reverse_mapping::IdDict{AnySSAValue, Int} inner_walker_callback::C function LiftedLeaveWalker(@nospecialize(lifted_leaves::Union{LiftedLeaves, LiftedDefs}), @nospecialize(reverse_mapping::IdDict{AnySSAValue, Int}), inner_walker_callback::C) where C<:WalkerCallback return new{C}(lifted_leaves, reverse_mapping, inner_walker_callback) end end function (walker_callback::LiftedLeaveWalker)(@nospecialize(def), @nospecialize(defssa::AnySSAValue)) (; lifted_leaves, reverse_mapping, inner_walker_callback) = walker_callback if defssa in keys(lifted_leaves) || defssa in keys(reverse_mapping) return nothing end isa(def, PiNode) && return LiftedValue(def.val) return inner_walker_callback(def, defssa) end function perform_lifting!(compact::IncrementalCompact, visited_philikes::Vector{AnySSAValue}, @nospecialize(cache_key), lifting_cache::Union{IdDict{Pair{AnySSAValue,Pair{Any,Any}}, AnySSAValue}, Nothing}, @nospecialize(result_t), lifted_leaves::Union{LiftedLeaves, LiftedDefs}, @nospecialize(stmt_val), lazydomtree::Union{LazyDomtree,Nothing}, walker_callback::WalkerCallback = TrivialWalker()) reverse_mapping = IdDict{AnySSAValue, Int}() for id in 1:length(visited_philikes) reverse_mapping[visited_philikes[id]] = id end @assert !isempty(lifted_leaves) "perform_lifting! requires at least one lifted leaf" # Check if all the lifted leaves are the same local the_leaf all_same = true for (_, val) in lifted_leaves if !@isdefined(the_leaf) the_leaf = val continue end if val !== the_leaf all_same = false end end @assert @isdefined(the_leaf) if all_same && isa(the_leaf, LiftedValue) dominates_all = true the_leaf_val = the_leaf.val if isa(the_leaf_val, AnySSAValue) if lazydomtree === nothing # Must conservatively assume this dominates_all = false else # This code guards against the possibility of accidentally forwarding a value from a # previous iteration. Consider for example: # # %p = phi(%arg, %t) # %b = <...> # %c = getfield(%p, 1) # %t = tuple(%b) # # It would be incorrect to replace `%c` by `%b`, because that would read the value of # `%b` in the *current* iteration, while the value of `%b` that comes in via `%p` is # that of the previous iteration. domtree = get!(lazydomtree) for item in visited_philikes if !dominates_ssa(compact, domtree, the_leaf_val, item) dominates_all = false break end end end end if dominates_all if isa(the_leaf_val, OldSSAValue) the_leaf = LiftedValue(simple_walk(compact, the_leaf_val)) end return Pair{Union{LiftedValue,Nothing}, PhiNest}(the_leaf, PhiNest(visited_philikes, Vector{LiftedPhilike}(undef, 0), lifted_leaves, reverse_mapping, walker_callback)) end end # Insert PhiNodes nphilikes = length(visited_philikes) lifted_philikes = Vector{LiftedPhilike}(undef, nphilikes) # The cache key includes stmt_val to distinguish liftings through the same phi # node with different type constraints (e.g. direct access vs through a PiNode). # stmt_val has not been walked yet at this point, so it reflects the original # getfield argument and thus the type constraint used by collect_leaves. local cache_key_with_val = nothing if lifting_cache !== nothing cache_key_with_val = Pair{Any,Any}(cache_key, stmt_val) end for i = 1:nphilikes old_ssa = visited_philikes[i] old_inst = compact[old_ssa] old_node = old_inst[:stmt]::Union{PhiNode,Expr} if lifting_cache !== nothing && cache_key_with_val !== nothing ckey = Pair{AnySSAValue,Pair{Any,Any}}(old_ssa, cache_key_with_val) if ckey in keys(lifting_cache) ssa = lifting_cache[ckey] if isa(old_node, PhiNode) lifted_philikes[i] = LiftedPhilike(ssa, old_node, false) else lifted_philikes[i] = LiftedPhilike(ssa, IfElseCall(old_node), false) end continue end end if isa(old_node, PhiNode) new_node = PhiNode() ssa = insert_node!(compact, old_ssa, removable_if_unused(NewInstruction(new_node, result_t))) lifted_philikes[i] = LiftedPhilike(ssa, new_node, true) else @assert is_known_call(old_node, Core.ifelse, compact) ifelse_func, condition = old_node.args if is_old(compact, old_ssa) && isa(condition, SSAValue) condition = OldSSAValue(condition.id) end new_node = Expr(:call, ifelse_func, condition) # Renamed then_result, else_result added below new_inst = NewInstruction(new_node, result_t, NoCallInfo(), old_inst[:line], old_inst[:flag]) ssa = insert_node!(compact, old_ssa, new_inst, #= attach_after =# true) lifted_philikes[i] = LiftedPhilike(ssa, IfElseCall(new_node), true) end if lifting_cache !== nothing && cache_key_with_val !== nothing ckey = Pair{AnySSAValue,Pair{Any,Any}}(old_ssa, cache_key_with_val) lifting_cache[ckey] = ssa end end # Fixup the stmt itself if isa(stmt_val, Union{SSAValue, OldSSAValue}) stmt_val = simple_walk(compact, stmt_val, LiftedLeaveWalker(lifted_leaves, reverse_mapping, walker_callback)) end stmt_val = resolve_lifted_stmt_val(stmt_val, lifted_leaves, reverse_mapping, lifted_philikes) return Pair{Union{LiftedValue,Nothing}, PhiNest}(stmt_val, PhiNest(visited_philikes, lifted_philikes, lifted_leaves, reverse_mapping, walker_callback)) end function resolve_lifted_stmt_val(@nospecialize(stmt_val), lifted_leaves::LiftedLeaves, reverse_mapping::IdDict{AnySSAValue, Int}, lifted_philikes::Vector{LiftedPhilike}) if haskey(lifted_leaves, stmt_val) return lifted_leaves[stmt_val] elseif isa(stmt_val, AnySSAValue) && haskey(reverse_mapping, stmt_val) return LiftedValue(lifted_philikes[reverse_mapping[stmt_val]].ssa) end error("failed to resolve lifted statement value") end function resolve_lifted_stmt_val(@nospecialize(stmt_val), lifted_defs::LiftedDefs, reverse_mapping::IdDict{AnySSAValue, Int}, lifted_philikes::Vector{LiftedPhilike}) @assert !haskey(lifted_defs, stmt_val) "definition lifting should only resolve through a lifted phi-like value" if isa(stmt_val, AnySSAValue) && haskey(reverse_mapping, stmt_val) return LiftedValue(lifted_philikes[reverse_mapping[stmt_val]].ssa) end error("failed to resolve lifted definition value") end # Handle _apply_iterate calls: convert arguments to use `Core.svec`. # The behavior of `Core.svec` (with boxing) better matches the ABI of codegen. function lift_apply_args!(compact::IncrementalCompact, idx::Int, stmt::Expr) compact[idx] = nothing for i in 4:length(stmt.args) # Skip `_apply_iterate`, `iterate`, and the function arg = stmt.args[i] arg_type = argextype_widened(arg, compact) if isa(arg_type, DataType) && arg_type.name === Tuple.name svec_args = nothing if isa(arg, SSAValue) arg_stmt = compact[arg][:stmt] if is_known_call(arg_stmt, Core.tuple, compact) svec_args = copy(arg_stmt.args) end end if svec_args === nothing # Fallback path: generate getfield calls for tuple elements tuple_length = length(arg_type.parameters) if tuple_length > 0 && !isvarargtype(arg_type.parameters[tuple_length]) svec_args = Vector{Any}(undef, tuple_length + 1) for j in 1:tuple_length getfield_call = Expr(:call, GlobalRef(Core, :getfield), arg, j) getfield_type = arg_type.parameters[j] inst = compact[SSAValue(idx)] getfield_ssa = insert_node!(compact, SSAValue(idx), NewInstruction(getfield_call, getfield_type, NoCallInfo(), inst[:line], inst[:flag])) svec_args[j + 1] = getfield_ssa end end end if svec_args !== nothing svec_args[1] = GlobalRef(Core, :svec) new_svec_call = Expr(:call) new_svec_call.args = svec_args inst = compact[SSAValue(idx)] new_svec_ssa = insert_node!(compact, SSAValue(idx), NewInstruction(new_svec_call, SimpleVector, NoCallInfo(), inst[:line], inst[:flag])) stmt.args[i] = new_svec_ssa end end end compact[idx] = stmt nothing end function lift_svec_ref!(compact::IncrementalCompact, idx::Int, stmt::Expr) length(stmt.args) != 3 && return vec = stmt.args[2] val = stmt.args[3] valT = argextype(val, compact) (isa(valT, Const) && isa(valT.val, Int)) || return valI = valT.val::Int valI >= 1 || return if isa(vec, SimpleVector) valI <= length(vec) || return compact[idx] = quoted(vec[valI]) elseif isa(vec, SSAValue) def = compact[vec][:stmt] if is_known_call(def, Core.svec, compact) valI <= length(def.args) - 1 || return compact[idx] = def.args[valI+1] elseif is_known_call(def, Core._compute_sparams, compact) valI != 1 && return # TODO generalize this for more values of valI res = _lift_svec_ref(def, compact) res === nothing && return compact[idx] = res.val end end return end function lift_leaves_keyvalue(compact::IncrementalCompact, @nospecialize(key), leaves::Vector{Any}, 𝕃ₒ::AbstractLattice) # For every leaf, the lifted value lifted_leaves = LiftedLeaves() for i = 1:length(leaves) leaf = leaves[i] cache_key = leaf if isa(leaf, AnySSAValue) (def, leaf) = walk_to_def(compact, leaf) if is_known_invoke_or_call(def, Core.OptimizedGenerics.KeyValue.set, compact) @assert isexpr(def, :invoke) if length(def.args) in (5, 6) set_key = def.args[end-1] set_val_idx = length(def.args) elseif length(def.args) == 4 # Key is deleted # TODO: Model this return nothing elseif length(def.args) == 3 # The whole collection is deleted # TODO: Model this return nothing else return nothing end if set_key === key || (egal_tfunc(𝕃ₒ, argextype(key, compact), argextype(set_key, compact)) == Const(true)) lift_arg!(compact, leaf, cache_key, def, set_val_idx, lifted_leaves) continue end end end return nothing end return lifted_leaves end function keyvalue_predecessors(@nospecialize(key), 𝕃ₒ::AbstractLattice) function(@nospecialize(def), compact::IncrementalCompact) if is_known_invoke_or_call(def, Core.OptimizedGenerics.KeyValue.set, compact) @assert isexpr(def, :invoke) if length(def.args) in (5, 6) collection = def.args[end-2] set_key = def.args[end-1] elseif length(def.args) == 4 # Key is deleted # collection = def.args[end-1] # TODO: Model this return nothing elseif length(def.args) == 3 # The whole collection is deleted # collection = def.args[end] # TODO: Model this return nothing else return nothing end if set_key === key || (egal_tfunc(𝕃ₒ, argextype(key, compact), argextype(set_key, compact)) == Const(true)) # This is an actual def return nothing end return Any[collection] end return phi_or_ifelse_predecessors(def, compact) end end struct KeyValueWalker <: WalkerCallback compact::IncrementalCompact end function (walker_callback::KeyValueWalker)(@nospecialize(def), @nospecialize(_defssa::AnySSAValue)) if is_known_invoke_or_call(def, Core.OptimizedGenerics.KeyValue.set, walker_callback.compact) @assert length(def.args) in (5, 6) return LiftedValue(def.args[end-2]) end return nothing end function lift_keyvalue_get!(compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice) collection = stmt.args[end-1] key = stmt.args[end] leaves, visited_philikes = collect_leaves(compact, collection, Any, 𝕃ₒ, keyvalue_predecessors(key, 𝕃ₒ)) isempty(leaves) && return lifted_leaves = lift_leaves_keyvalue(compact, key, leaves, 𝕃ₒ) lifted_leaves === nothing && return result_t = Union{} for v in values(lifted_leaves) v === nothing && return result_t = tmerge(𝕃ₒ, result_t, argextype(v.val, compact)) end # Extract the wrapper type (e.g. Some{V}) from the inferred return type # Union{Nothing, Some{V}} by subtracting Nothing. Bail out if the result # is not a valid single-field concrete wrapper type. get_rtype = widenconst(compact[SSAValue(idx)][:type]) wrapper_typ = typesubtract(get_rtype, Nothing, 0) isconcretetype(wrapper_typ) || return fieldcount(wrapper_typ) == 1 || return ⊑(𝕃ₒ, result_t, fieldtype(wrapper_typ, 1)) || return (lifted_val, nest) = perform_lifting!(compact, visited_philikes, key, nothing, result_t, lifted_leaves, collection, nothing, KeyValueWalker(compact)) if lifted_val !== nothing compact[idx] = Expr(:new, wrapper_typ, lifted_val.val) compact[SSAValue(idx)][:type] = wrapper_typ add_flag!(compact[SSAValue(idx)], IR_FLAG_REFINED) else compact[idx] = nothing end finish_phi_nest!(compact, nest) return end # TODO: We could do the whole lifting machinery here, but really all # we want to do is clean this up when it got inserted by inlining, # which always targets simple `svec` call or `_compute_sparams`, # so this specialized lifting would be enough @inline function _lift_svec_ref(def::Expr, compact::IncrementalCompact) length(def.args) >= 3 || return nothing m = argextype(def.args[2], compact) isa(m, Const) || return nothing m = m.val isa(m, Method) || return nothing # TODO: More general structural analysis of the intersection sig = m.sig isa(sig, UnionAll) || return nothing tvar = sig.var sig = sig.body isa(sig, DataType) || return nothing sig.name === Tuple.name || return nothing sig_parameters = sig.parameters::SimpleVector length_sig_parameters = length(sig_parameters) length_sig_parameters >= 1 || return nothing function has_typevar_closure(j::Int) has_typevar(sig_parameters[j], tvar) end i = findfirst(has_typevar_closure, 1:length_sig_parameters) i === nothing && return nothing any(has_typevar_closure, i+1:length_sig_parameters) && return nothing arg = sig_parameters[i] rarg = def.args[2 + i] isa(rarg, SSAValue) || return nothing argdef = compact[rarg][:stmt] if isexpr(argdef, :new) rarg = argdef.args[1] isa(rarg, SSAValue) || return nothing argdef = compact[rarg][:stmt] else isType(arg) || return nothing arg = type_parameter(arg) end is_known_call(argdef, Core.apply_type, compact) || return nothing length(argdef.args) == 3 || return nothing applyT = argextype(argdef.args[2], compact) isa(applyT, Const) || return nothing applyT = applyT.val isa(applyT, UnionAll) || return nothing # N.B.: At the moment we only lift the valI == 1 case, so we # only need to look at the outermost tvar. applyTvar = applyT.var applyTbody = applyT.body arg = unwrap_unionall(arg) applyTbody = unwrap_unionall(applyTbody) (isa(arg, DataType) && isa(applyTbody, DataType)) || return nothing applyTbody.name === arg.name || return nothing length(applyTbody.parameters) == length(arg.parameters) || return nothing for i = 1:length(applyTbody.parameters) if applyTbody.parameters[i] === applyTvar && arg.parameters[i] === tvar return LiftedValue(argdef.args[3]) end end return nothing end struct IsEgal <: Function x::Any IsEgal(@nospecialize(x)) = new(x) end (x::IsEgal)(@nospecialize(y)) = x.x === y # This tries to match patterns of the form # %ft = typeof(%farg) # %Targ = apply_type(Foo, ft) # %x = new(%Targ, %farg) # # and if possible refines the nothrowness of the new expr based on it. function pattern_match_typeof(compact::IncrementalCompact, typ::DataType, fidx::Int, @nospecialize(Targ), @nospecialize(farg)) isa(Targ, SSAValue) || return false Tdef = compact[Targ][:stmt] is_known_call(Tdef, Core.apply_type, compact) || return false length(Tdef.args) ≥ 2 || return false applyT = argextype(Tdef.args[2], compact) isa(applyT, Const) || return false applyT = applyT.val tvars = Any[] while isa(applyT, UnionAll) applyTvar = applyT.var applyT = applyT.body push!(tvars, applyTvar) end @assert applyT.name === typ.name fT = fieldtype(applyT, fidx) idx = findfirst(IsEgal(fT), tvars) idx === nothing && return false checkbounds(Bool, Tdef.args, 2+idx) || return false valarg = Tdef.args[2+idx] isa(valarg, SSAValue) || return false valdef = compact[valarg][:stmt] is_known_call(valdef, typeof, compact) || return false return valdef.args[2] === farg end function refine_new_effects!(𝕃ₒ::AbstractLattice, compact::IncrementalCompact, idx::Int, stmt::Expr) inst = compact[SSAValue(idx)] if has_flag(inst, IR_FLAGS_REMOVABLE) return # already accurate end (consistent, removable, nothrow) = new_expr_effect_flags(𝕃ₒ, stmt.args, compact, pattern_match_typeof) if consistent add_flag!(inst, IR_FLAG_CONSISTENT) end if removable add_flag!(inst, IR_FLAGS_REMOVABLE) elseif nothrow add_flag!(inst, IR_FLAG_NOTHROW) end return nothing end function fold_ifelse!(compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice) length(stmt.args) == 4 || return false condarg = stmt.args[2] condtyp = argextype(condarg, compact) if isa(condtyp, Const) if condtyp.val === true compact[idx] = stmt.args[3] return true elseif condtyp.val === false compact[idx] = stmt.args[4] return true end elseif ⊑(𝕃ₒ, condtyp, Bool) && stmt.args[3] === stmt.args[4] compact[idx] = stmt.args[3] return true end return false end # NOTE we use `IdSet{Int}` instead of `BitSet` for in these passes since they work on IR after inlining, # which can be very large sometimes, and program counters in question are often very sparse const SPCSet = IdSet{Int} struct IntermediaryCollector <: WalkerCallback intermediaries::SPCSet end function (walker_callback::IntermediaryCollector)(@nospecialize(def), @nospecialize(defssa::AnySSAValue)) if !(def isa Expr) push!(walker_callback.intermediaries, defssa.id) if def isa PiNode return LiftedValue(def.val) end end return nothing end function update_scope_mapping!(scope_mapping, bb, val) current_mapping = scope_mapping[bb] if current_mapping != SSAValue(0) if val == SSAValue(0) # Unreachable bbs will have SSAValue(0), but can branch into # try/catch regions. We could validate with the domtree, but that's # quite expensive for a debug check, so simply allow this without # making any changes to mapping. return end @assert current_mapping == val return end scope_mapping[bb] = val end """ sroa_pass!(ir::IRCode) -> newir::IRCode `getfield` elimination pass, a.k.a. Scalar Replacements of Aggregates optimization. This pass is based on a local field analysis by def-use chain walking. It looks for struct allocation sites ("definitions"), and `getfield` calls as well as `:foreigncall`s that preserve the structs ("usages"). If "definitions" have enough information, then this pass will replace corresponding usages with forwarded values. `mutable struct`s require additional cares and need to be handled separately from immutables. For `mutable struct`s, `setfield!` calls account for "definitions" also, and the pass should give up the lifting conservatively when there are any "intermediate usages" that may escape the mutable struct (e.g. non-inlined generic function call that takes the mutable struct as its argument). In a case when all usages are fully eliminated, `struct` allocation may also be erased as a result of succeeding dead code elimination. """ function sroa_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) 𝕃ₒ = inlining === nothing ? SimpleInferenceLattice.instance : optimizer_lattice(inlining.interp) compact = IncrementalCompact(ir) defuses = nothing # will be initialized once we encounter mutability in order to reduce dynamic allocations # initialization of domtree is delayed to avoid the expensive computation in many cases lazydomtree = LazyDomtree(ir) lifting_cache = IdDict{Pair{AnySSAValue,Pair{Any,Any}}, AnySSAValue}() def_lifting_cache = IdDict{Pair{AnySSAValue,Pair{Any,Any}}, AnySSAValue}() scope_mapping::Union{Vector{SSAValue}, Nothing} = nothing for ((old_idx, idx), stmt) in compact # If we encounter any EnterNode with set :scope, propagate the current scope for all basic blocks, so # we have easy access for current_scope folding below. if !isa(stmt, Expr) bb = compact.active_result_bb - 1 if scope_mapping !== nothing && did_just_finish_bb(compact) this_scope = scope_mapping[bb] if isa(stmt, GotoIfNot) update_scope_mapping!(scope_mapping, stmt.dest, this_scope) update_scope_mapping!(scope_mapping, bb+1, this_scope) elseif isa(stmt, GotoNode) update_scope_mapping!(scope_mapping, stmt.label, this_scope) elseif isa(stmt, EnterNode) if stmt.catch_dest != 0 update_scope_mapping!(scope_mapping, stmt.catch_dest, this_scope) end isdefined(stmt, :scope) || update_scope_mapping!(scope_mapping, bb+1, this_scope) elseif !isa(stmt, ReturnNode) update_scope_mapping!(scope_mapping, bb+1, this_scope) end end if isa(stmt, EnterNode) if isdefined(stmt, :scope) if scope_mapping === nothing scope_mapping = SSAValue[SSAValue(0) for _ = 1:length(compact.cfg_transform.result_bbs)] end update_scope_mapping!(scope_mapping, bb+1, SSAValue(idx)) end end continue end if scope_mapping !== nothing && did_just_finish_bb(compact) bb = compact.active_result_bb - 1 bbs = scope_mapping[bb] if isexpr(stmt, :leave) && bbs != SSAValue(0) # Here we want to count the number of scopes that we're leaving, # which is the same as the number of EnterNodes being referenced # by `stmt.args`. Which have :scope set. In practice, the frontend # does emit these in order, so we could simply go to the last one, # but we want to avoid making that semantic assumption. for i = 1:length(stmt.args) scope = stmt.args[i] scope === nothing && continue enter = compact[scope][:inst] @assert isa(enter, EnterNode) isdefined(enter, :scope) || continue bbs = scope_mapping[block_for_inst(compact, bbs)] end end update_scope_mapping!(scope_mapping, bb+1, bbs) end # check whether this statement is `getfield` / `setfield!` (or other "interesting" statement) is_setfield = is_isdefined = is_finalizer = false field_ordering = :unspecified if is_known_call(stmt, setfield!, compact) 4 <= length(stmt.args) <= 5 || continue is_setfield = true if length(stmt.args) == 5 field_ordering = argextype(stmt.args[5], compact) end elseif is_known_call(stmt, getfield, compact) 3 <= length(stmt.args) <= 5 || continue if length(stmt.args) == 5 field_ordering = argextype(stmt.args[5], compact) elseif length(stmt.args) == 4 field_ordering = argextype(stmt.args[4], compact) widenconst(field_ordering) === Bool && (field_ordering = :unspecified) end elseif is_known_call(stmt, isdefined, compact) 3 <= length(stmt.args) <= 4 || continue is_isdefined = true if length(stmt.args) == 4 field_ordering = argextype(stmt.args[4], compact) widenconst(field_ordering) === Bool && (field_ordering = :unspecified) end elseif is_known_call(stmt, Core.finalizer, compact) 3 <= length(stmt.args) <= 5 || continue info = compact[SSAValue(idx)][:info] if isa(info, FinalizerInfo) is_finalizer_inlineable(info.effects) || continue else # Inlining performs legality checks on the finalizer to determine # whether or not we may inline it. If so, it appends extra arguments # at the end of the intrinsic. Detect that here. if length(stmt.args) == 4 && stmt.args[4] === nothing # constant case elseif length(stmt.args) == 5 && stmt.args[4] isa Bool && stmt.args[5] isa Core.CodeInstance # inlining case else continue end end is_finalizer = true elseif isexpr(stmt, :foreigncall) nccallargs = length(stmt.args[3]::SimpleVector) preserved = Int[] new_preserves = Any[] for pidx in (6+nccallargs):length(stmt.args) preserved_arg = stmt.args[pidx] isa(preserved_arg, SSAValue) || continue let intermediaries = SPCSet() def = simple_walk(compact, preserved_arg, IntermediaryCollector(intermediaries)) isa(def, SSAValue) || continue defidx = def.id def = compact[def][:stmt] if is_known_call(def, tuple, compact) record_immutable_preserve!(new_preserves, def, compact) push!(preserved, preserved_arg.id) continue elseif isexpr(def, :new) typ = unwrap_unionall(argextype_widened(SSAValue(defidx), compact)) if typ isa DataType && !ismutabletype(typ) record_immutable_preserve!(new_preserves, def, compact) push!(preserved, preserved_arg.id) continue end else continue end if defuses === nothing defuses = IdDict{Int, Tuple{SPCSet, SSADefUse}}() end mid, defuse = get!(()->(SPCSet(),SSADefUse()), defuses, defidx) push!(defuse.uses, PreserveUse(idx)) union!(mid, intermediaries) end continue end if !isempty(new_preserves) compact[idx] = nothing compact[idx] = form_new_preserves(stmt, preserved, new_preserves) end continue else # TODO: This isn't the best place to put these if is_known_call(stmt, typeassert, compact) canonicalize_typeassert!(compact, idx, stmt) elseif is_known_call(stmt, Core._svec_ref, compact) lift_svec_ref!(compact, idx, stmt) elseif is_known_call(stmt, (===), compact) lift_comparison!(===, compact, idx, stmt, 𝕃ₒ) elseif is_known_call(stmt, isa, compact) lift_comparison!(isa, compact, idx, stmt, 𝕃ₒ) elseif is_known_call(stmt, Core.ifelse, compact) fold_ifelse!(compact, idx, stmt, 𝕃ₒ) elseif is_known_invoke_or_call(stmt, Core.OptimizedGenerics.KeyValue.get, compact) 2 == (length(stmt.args) - (isexpr(stmt, :invoke) ? 2 : 1)) || continue lift_keyvalue_get!(compact, idx, stmt, 𝕃ₒ) elseif is_known_call(stmt, Core.current_scope, compact) length(stmt.args) == 1 || continue scope_mapping !== nothing || continue bb = compact.active_result_bb did_just_finish_bb(compact) && (bb -= 1) enter_ssa = scope_mapping[bb] enter_ssa == SSAValue(0) && continue compact[SSAValue(idx)] = (compact[enter_ssa][:stmt]::EnterNode).scope elseif isexpr(stmt, :new) refine_new_effects!(𝕃ₒ, compact, idx, stmt) elseif is_known_call(stmt, Core._apply_iterate, compact) length(stmt.args) >= 4 || continue lift_apply_args!(compact, idx, stmt) end continue end if is_finalizer val = stmt.args[3] else # analyze `getfield` / `isdefined` / `setfield!` call val = stmt.args[2] end struct_typ = argextype_widened(val, compact) struct_typ_name = argument_datatypename(struct_typ) if struct_typ_name === nothing if isa(struct_typ, Union) && is_isdefined lift_comparison!(isdefined, compact, idx, stmt, 𝕃ₒ) end continue end struct_typ_name.atomicfields == C_NULL || continue # TODO: handle more if !((field_ordering === :unspecified) || (field_ordering isa Const && field_ordering.val === :not_atomic)) continue end # analyze this mutable struct here for the later pass if ismutabletypename(struct_typ_name) isa(val, SSAValue) || continue let intermediaries = SPCSet() def = simple_walk(compact, val, IntermediaryCollector(intermediaries)) # Mutable stuff here isa(def, SSAValue) || continue if defuses === nothing defuses = IdDict{Int, Tuple{SPCSet, SSADefUse}}() end mid, defuse = get!(()->(SPCSet(),SSADefUse()), defuses, def.id) if is_setfield push!(defuse.defs, idx) elseif is_isdefined push!(defuse.uses, IsdefinedUse(idx)) elseif is_finalizer push!(defuse.uses, FinalizerUse(idx)) else push!(defuse.uses, GetfieldUse(idx)) end union!(mid, intermediaries) end continue elseif is_setfield || is_finalizer continue # invalid `setfield!` or `Core.finalizer` call, but just ignore here elseif is_isdefined continue # TODO? end # perform SROA on immutable structs here on field = try_compute_fieldidx_stmt(compact, stmt, struct_typ) field === nothing && continue leaves, visited_philikes = collect_leaves(compact, val, struct_typ, 𝕃ₒ, phi_or_ifelse_predecessors) isempty(leaves) && continue lifted_result = lift_leaves(compact, field, leaves, 𝕃ₒ) lifted_result === nothing && continue lifted_leaves, any_undef = lifted_result result_t = Union{} for v in values(lifted_leaves) v === nothing && continue result_t = tmerge(𝕃ₒ, result_t, argextype(v.val, compact)) end (lifted_val, nest) = perform_lifting!(compact, visited_philikes, field, lifting_cache, result_t, lifted_leaves, val, lazydomtree) should_delete_node = false line = compact[SSAValue(idx)][:line] if lifted_val !== nothing && !⊑(𝕃ₒ, compact[SSAValue(idx)][:type], result_t) compact[idx] = lifted_val.val add_flag!(compact[SSAValue(idx)], IR_FLAG_REFINED) elseif lifted_val === nothing || isa(lifted_val.val, AnySSAValue) # Save some work in a later compaction, by inserting this into the renamer now, # but only do this if we didn't set the REFINED flag, to save work for irinterp # in revisiting only the renamings that came through *this* idx. compact.ssa_rename[old_idx] = lifted_val === nothing ? nothing : lifted_val.val::AnySSAValue should_delete_node = true else compact[idx] = lifted_val === nothing ? nothing : lifted_val.val end finish_phi_nest!(compact, nest) # Insert the undef check if necessary if any_undef if lifted_val === nothing def_val = false else lifted_leaves_def = LiftedDefs() for (k, v) in pairs(lifted_leaves) lifted_leaves_def[k] = v === nothing ? false : true end (def_val, nest) = perform_lifting!(compact, visited_philikes, field, def_lifting_cache, Bool, lifted_leaves_def, val, lazydomtree) def_val = (def_val::LiftedValue).val finish_phi_nest!(compact, nest) end throw_expr = Expr(:throw_undef_if_not, Symbol("##getfield##"), def_val) if should_delete_node # Replace the node we already have rather than deleting/re-inserting. # This way it is easier to handle BB boundary corner cases. compact[SSAValue(idx)] = throw_expr compact[SSAValue(idx)][:type] = Nothing compact[SSAValue(idx)][:flag] = IR_FLAG_EFFECT_FREE | IR_FLAG_CONSISTENT | IR_FLAG_NOUB should_delete_node = false else ni = NewInstruction(throw_expr, Nothing, line) insert_node!(compact, SSAValue(idx), ni) end else # val must be defined @assert lifted_val !== nothing end should_delete_node && delete_inst_here!(compact) end non_dce_finish!(compact) if defuses !== nothing # now go through analyzed mutable structs and see which ones we can eliminate # NOTE copy the use count here, because `simple_dce!` may modify it and we need it # consistent with the state of the IR here (after tracking `PhiNode` arguments, # but before the DCE) for our predicate within `sroa_mutables!`, but we also # try an extra effort using a callback so that reference counts are updated used_ssas = copy(compact.used_ssas) simple_dce!(compact) do x::SSAValue used_ssas[x.id] -= 1 end ir = complete(compact) # remove any use that has been optimized away by the DCE for (_, defuse) in values(defuses) filter!(x -> ir[SSAValue(x.idx)][:stmt] !== nothing, defuse.uses) end sroa_mutables!(ir, defuses, used_ssas, lazydomtree, inlining) return ir else simple_dce!(compact) return complete(compact) end end # NOTE we resolve the inlining source here as we don't want to serialize `Core.Compiler` # data structure into the global cache (see the comment in `handle_finalizer_call!`) function try_inline_finalizer!(ir::IRCode, argexprs::Vector{Any}, idx::Int, code::CodeInstance, @nospecialize(info::CallInfo), inlining::InliningState, attach_after::Bool) mi = get_ci_mi(code) et = InliningEdgeTracker(inlining) if code isa CodeInstance if use_const_api(code) # No code in the function - Nothing to do add_inlining_edge!(et, code) return true end # COMBAK: this has awkward and unreliable global cache effects, but # this doesn't respect the bottom-up inliner order so we do not have # CallInfo anymore. See `handle_finalizer_call!` too. src = ci_get_source(inlining.interp, code) else return false end src_inlining_policy(inlining.interp, mi, src, info, IR_FLAG_NULL) || return false src, spec_info, di = retrieve_ir_for_inlining(code, src) # For now: Require finalizer to only have one basic block length(src.cfg.blocks) == 1 || return false # Ok, we're committed to inlining the finalizer add_inlining_edge!(et, code) # TODO: Should there be a special line number node for inlined finalizers? inline_at = ir[SSAValue(idx)][:line] ssa_substitute = ir_prepare_inlining!(InsertBefore(ir, SSAValue(idx)), ir, src, spec_info, di, mi, inline_at, argexprs) # TODO: Use the actual inliner here rather than open coding this special purpose inliner. ssa_rename = Vector{Any}(undef, length(src.stmts)) for idx′ = 1:length(src.stmts) inst = src[SSAValue(idx′)] stmt′ = inst[:stmt] isa(stmt′, ReturnNode) && continue stmt′ = ssamap(stmt′) do ssa::SSAValue ssa_rename[ssa.id] end stmt′ = ssa_substitute_op!(InsertBefore(ir, SSAValue(idx)), inst, stmt′, ssa_substitute) ssa_rename[idx′] = insert_node!(ir, idx, NewInstruction(inst; stmt=stmt′, line=(ssa_substitute.inlined_at[1], ssa_substitute.inlined_at[2], Int32(idx′))), attach_after) end return true end is_nothrow(ir::IRCode, ssa::SSAValue) = has_flag(ir[ssa], IR_FLAG_NOTHROW) function reachable_blocks(cfg::CFG, from_bb::Int, to_bb::Int) worklist = Int[from_bb] visited = BitSet(from_bb) if to_bb == from_bb return visited else push!(visited, to_bb) end function visit!(bb::Int) if bb ∉ visited push!(visited, bb) push!(worklist, bb) end end while !isempty(worklist) foreach(visit!, cfg.blocks[pop!(worklist)].succs) end return visited end function _update_finalizer_insert!(ir::IRCode, lazypostdomtree::LazyPostDomtree, finalizer_idx::Int, insert_bb::Int, insert_idx::Union{Int,Nothing}, x::Union{Int,SSAUse}) defuse_idx = x isa SSAUse ? x.idx : x defuse_idx == finalizer_idx && return insert_bb, insert_idx defuse_bb = block_for_inst(ir, defuse_idx) new_insert_bb = nearest_common_dominator(get!(lazypostdomtree), insert_bb, defuse_bb) if new_insert_bb == insert_bb && insert_idx !== nothing insert_idx = max(insert_idx::Int, defuse_idx) elseif new_insert_bb == defuse_bb insert_idx = defuse_idx else insert_idx = nothing end insert_bb = new_insert_bb return insert_bb, insert_idx end function try_resolve_finalizer!(ir::IRCode, alloc_idx::Int, finalizer_idx::Int, defuse::SSADefUse, inlining::InliningState, lazydomtree::LazyDomtree, lazypostdomtree::LazyPostDomtree, @nospecialize(info::CallInfo)) # For now, require that: # 1. The allocation dominates the finalizer registration # 2. The insertion block for the finalizer is the post-dominator of all # uses (including the finalizer registration). # 3. The path from the finalizer registration to the finalizer inlining # location is nothrow # # TODO: We could relax the check 2, by inlining the finalizer multiple times. # Check #1: The allocation dominates the finalizer registration domtree = get!(lazydomtree) finalizer_bb = block_for_inst(ir, finalizer_idx) alloc_bb = block_for_inst(ir, alloc_idx) dominates(domtree, alloc_bb, finalizer_bb) || return nothing # Check #2: The insertion block for the finalizer is the post-dominator of all uses insert_bb::Int = finalizer_bb insert_idx::Union{Int,Nothing} = finalizer_idx for x in defuse.uses insert_bb, insert_idx = _update_finalizer_insert!(ir, lazypostdomtree, finalizer_idx, insert_bb, insert_idx, x) end for x in defuse.defs insert_bb, insert_idx = _update_finalizer_insert!(ir, lazypostdomtree, finalizer_idx, insert_bb, insert_idx, x) end insert_bb != 0 || return nothing # verify post-dominator of all uses exists if !OptimizationParams(inlining.interp).assume_fatal_throw # Collect all reachable blocks between the finalizer registration and the # insertion point blocks = reachable_blocks(ir.cfg, finalizer_bb, insert_bb) # Check #3 function check_range_nothrow(s::Int, e::Int) return all(s:e) do sidx::Int sidx == finalizer_idx && return true sidx == alloc_idx && return true return is_nothrow(ir, SSAValue(sidx)) end end for bb in blocks range = ir.cfg.blocks[bb].stmts s, e = first(range), last(range) if bb == insert_bb insert_idx === nothing && continue e = insert_idx end if bb == finalizer_bb s = finalizer_idx end check_range_nothrow(s, e) || return nothing end end # Ok, legality check complete. Figure out the exact statement where we're # going to inline the finalizer. loc = insert_idx === nothing ? first(ir.cfg.blocks[insert_bb].stmts) : insert_idx::Int attach_after = insert_idx !== nothing finalizer_stmt = ir[SSAValue(finalizer_idx)][:stmt] argexprs = Any[finalizer_stmt.args[2], finalizer_stmt.args[3]] flag = isa(info, FinalizerInfo) ? flags_for_effects(info.effects) : IR_FLAG_NULL if length(finalizer_stmt.args) >= 4 inline = finalizer_stmt.args[4] if inline === nothing # No code in the function - Nothing to do else ci = finalizer_stmt.args[5]::CodeInstance if inline::Bool && try_inline_finalizer!(ir, argexprs, loc, ci, info, inlining, attach_after) # the finalizer body has been inlined else newinst = add_flag(NewInstruction(Expr(:invoke, ci, argexprs...), Any), flag) insert_node!(ir, loc, newinst, attach_after) end end else newinst = add_flag(NewInstruction(Expr(:call, argexprs...), Nothing), flag) insert_node!(ir, loc, newinst, attach_after) end # Erase the call to `finalizer` ir[SSAValue(finalizer_idx)][:stmt] = nothing return nothing end function sroa_mutables!(ir::IRCode, defuses::IdDict{Int,Tuple{SPCSet,SSADefUse}}, used_ssas::Vector{Int}, lazydomtree::LazyDomtree, inlining::Union{Nothing,InliningState}) 𝕃ₒ = inlining === nothing ? SimpleInferenceLattice.instance : optimizer_lattice(inlining.interp) lazypostdomtree = LazyPostDomtree(ir) function find_finalizer_useidx(defuse::SSADefUse) finalizer_useidx = nothing for (useidx, use) in enumerate(defuse.uses) if use.kind === :finalizer # For now: Only allow one finalizer per allocation finalizer_useidx !== nothing && return false finalizer_useidx = useidx end end if finalizer_useidx === nothing || inlining === nothing return true end return finalizer_useidx end for (defidx, (intermediaries, defuse)) in defuses # Find the type for this allocation defexpr = ir[SSAValue(defidx)][:stmt] isexpr(defexpr, :new) || continue # Could still end up here if we tried to setfield! on an immutable, which would # error at runtime, but is not illegal to have in the IR. typ = unwrap_unionall(widenconst(ir.stmts[defidx][:type])) ismutabletype(typ) || continue typ = typ::DataType # Check if there are any uses we did not account for. If so, the variable # escapes and we cannot eliminate the allocation. This works, because we're guaranteed # not to include any intermediaries that have dead uses. As a result, missing uses will only ever # show up in the nuses_total count. nleaves = length(defuse.uses) + length(defuse.defs) nuses = 0 for iidx in intermediaries nuses += used_ssas[iidx] end nuses_total = used_ssas[defidx] + nuses - length(intermediaries) all_eliminated = all_forwarded = true if nleaves ≠ nuses_total finalizer_useidx = find_finalizer_useidx(defuse) if finalizer_useidx isa Int nargs = length(ir.argtypes) # COMBAK this might need to be `Int(opt.src.nargs)` estate = EscapeAnalysis.analyze_escapes(ir, nargs, 𝕃ₒ, get_escape_cache(inlining.interp)) # disable finalizer inlining when this allocation is aliased to somewhere, # mostly likely to edges of `PhiNode` hasaliases = EscapeAnalysis.getaliases(SSAValue(defidx), estate) !== nothing einfo = estate[SSAValue(defidx)] if !hasaliases && EscapeAnalysis.has_no_escape(einfo) already = BitSet(use.idx for use in defuse.uses) for idx = einfo.Liveness if idx ∉ already push!(defuse.uses, SSAUse(:EALiveness, idx)) end end finalizer_idx = defuse.uses[finalizer_useidx].idx try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining::InliningState, lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info]) end end continue else finalizer_useidx = find_finalizer_useidx(defuse) if finalizer_useidx isa Int finalizer_idx = defuse.uses[finalizer_useidx].idx try_resolve_finalizer!(ir, defidx, finalizer_idx, defuse, inlining::InliningState, lazydomtree, lazypostdomtree, ir[SSAValue(finalizer_idx)][:info]) deleteat!(defuse.uses, finalizer_useidx) all_eliminated = all_forwarded = false # can't eliminate `setfield!` calls safely elseif !finalizer_useidx continue end end # Partition defuses by field fielddefuse = SSADefUse[SSADefUse() for _ = 1:fieldcount(typ)] for use in defuse.uses if use.kind === :preserve for du in fielddefuse push!(du.uses, use) end continue end stmt = ir[SSAValue(use.idx)][:stmt] # == `getfield`/`isdefined` call # We may have discovered above that this use is dead # after the getfield elim of immutables. In that case, # it would have been deleted. That's fine, just ignore # the use in that case. if stmt === nothing all_forwarded = false continue end field = try_compute_fieldidx_stmt(ir, stmt::Expr, typ) field === nothing && @goto skip push!(fielddefuse[field].uses, use) end for def in defuse.defs stmt = ir[SSAValue(def)][:stmt]::Expr # == `setfield!` call field = try_compute_fieldidx_stmt(ir, stmt, typ) field === nothing && @goto skip isconst(typ, field) && @goto skip # we discovered an attempt to mutate a const field, which must error push!(fielddefuse[field].defs, def) end # Check that the defexpr has defined values for all the fields # we're accessing. In the future, we may want to relax this, # but we should come up with semantics for well defined semantics # for uninitialized fields first. ndefuse = length(fielddefuse) blocks = Vector{Tuple{#=phiblocks=#Vector{Int},#=allblocks=#BitSet}}(undef, ndefuse) for fidx in 1:ndefuse du = fielddefuse[fidx] isempty(du.uses) && continue push!(du.defs, defidx) ldu = compute_live_ins(ir.cfg, du) if isempty(ldu.live_in_bbs) phiblocks = Int[] else phiblocks = iterated_dominance_frontier(ir.cfg, ldu, get!(lazydomtree)) end allblocks = union!(BitSet(phiblocks), ldu.def_bbs) blocks[fidx] = phiblocks, allblocks if fidx + 1 > length(defexpr.args) for i = 1:length(du.uses) use = du.uses[i] if use.kind === :isdefined if has_safe_def(ir, get!(lazydomtree), allblocks, du, defidx, use.idx) ir[SSAValue(use.idx)][:stmt] = true else all_eliminated = false end continue elseif use.kind === :preserve if length(du.defs) == 1 # allocation with this field uninitialized # there is nothing to preserve, just ignore this use du.uses[i] = NoPreserve() continue end end has_safe_def(ir, get!(lazydomtree), allblocks, du, defidx, use.idx) || @goto skip end else # always have some definition at the allocation site for i = 1:length(du.uses) use = du.uses[i] if use.kind === :isdefined ir[SSAValue(use.idx)][:stmt] = true end end end end # Everything accounted for. Go field by field and perform idf: # Compute domtree now, needed below, now that we have finished compacting the IR. # This needs to be after we iterate through the IR with `IncrementalCompact` # because removing dead blocks can invalidate the domtree. domtree = get!(lazydomtree) local preserve_uses = nothing for fidx in 1:ndefuse du = fielddefuse[fidx] ftyp = fieldtype(typ, fidx) if !isempty(du.uses) phiblocks, allblocks = blocks[fidx] phinodes = IdDict{Int, SSAValue}() for b in phiblocks phinodes[b] = insert_node!(ir, first(ir.cfg.blocks[b].stmts), NewInstruction(PhiNode(), ftyp)) end # Now go through all uses and rewrite them for use in du.uses if use.kind === :getfield inst = ir[SSAValue(use.idx)] inst[:stmt] = compute_value_for_use(ir, domtree, allblocks, du, phinodes, fidx, use.idx) add_flag!(inst, IR_FLAG_REFINED) elseif use.kind === :isdefined continue # already rewritten if possible elseif use.kind === :nopreserve continue # nothing to preserve (may happen when there are uninitialized fields) elseif use.kind === :preserve newval = compute_value_for_use(ir, domtree, allblocks, du, phinodes, fidx, use.idx) if !isbitstype(argextype_widened(newval, ir)) if preserve_uses === nothing preserve_uses = IdDict{Int, Vector{Any}}() end push!(get!(Vector{Any}, preserve_uses, use.idx), newval) end else @assert false "sroa_mutables!: unexpected use" end end for b in phiblocks n = ir[phinodes[b]][:stmt]::PhiNode result_t = Bottom for p in ir.cfg.blocks[b].preds push!(n.edges, p) v = compute_value_for_block(ir, domtree, allblocks, du, phinodes, fidx, p) push!(n.values, v) result_t = tmerge(𝕃ₒ, result_t, argextype(v, ir)) end ir[phinodes[b]][:type] = result_t end end all_eliminated || continue # all "usages" (i.e. `getfield` and `isdefined` calls) are eliminated, # now eliminate "definitions" (i.e. `setfield!`) calls # (NOTE the allocation itself will be eliminated by DCE pass later) for didx in du.defs didx == defidx && continue # this is allocation # verify this statement won't throw, otherwise it can't be eliminated safely setfield_ssa = SSAValue(didx) if is_nothrow(ir, setfield_ssa) ir[setfield_ssa][:stmt] = nothing else # We can't eliminate this statement, because it might still # throw an error, but we can mark it as effect-free since we # know we have removed all uses of the mutable allocation. # As a result, if we ever do prove nothrow, we can delete # this statement then. add_flag!(ir[setfield_ssa], IR_FLAG_EFFECT_FREE) end end end preserve_uses === nothing && continue if all_forwarded # this means all ccall preserves have been replaced with forwarded loads # so we can potentially eliminate the allocation, otherwise we must preserve # the whole allocation. push!(intermediaries, defidx) end # Insert the new preserves for (useidx, new_preserves) in preserve_uses ir[SSAValue(useidx)][:stmt] = form_new_preserves(ir[SSAValue(useidx)][:stmt]::Expr, intermediaries, new_preserves) end @label skip end end function form_new_preserves(origex::Expr, intermediaries::Union{Vector{Int},SPCSet}, new_preserves::Vector{Any}) newex = Expr(:foreigncall) nccallargs = length(origex.args[3]::SimpleVector) for i in 1:(6+nccallargs-1) push!(newex.args, origex.args[i]) end for i in (6+nccallargs):length(origex.args) x = origex.args[i] # don't need to preserve intermediaries if isa(x, SSAValue) && x.id in intermediaries continue end push!(newex.args, x) end for i in 1:length(new_preserves) push!(newex.args, new_preserves[i]) end return newex end """ canonicalize_typeassert!(compact::IncrementalCompact, idx::Int, stmt::Expr) Canonicalizes `X = typeassert(Y, T)::S` into `typeassert(Y, T); X = π(Y, S)` so that subsequent analysis only has to deal with the latter form. N.B. Inference may have a more precise type for `S`, than just `T`, but from here on out, there's no problem with just using that. We should probably have a version of `typeassert` that's defined not to return its value to make life easier for the backend. """ function canonicalize_typeassert!(compact::IncrementalCompact, idx::Int, stmt::Expr) length(stmt.args) == 3 || return pi = insert_node_here!(compact, NewInstruction( PiNode(stmt.args[2], compact.result[idx][:type]), compact.result[idx][:type], compact.result[idx][:line]), #=reverse_affinity=#true) compact.ssa_rename[compact.idx-1] = pi end function adce_erase!(phi_uses::Vector{Int}, extra_worklist::Vector{Int}, compact::IncrementalCompact, idx::Int, in_worklist::Bool) # return whether this made a change if isa(compact.result[idx][:stmt], PhiNode) return maybe_erase_unused!(compact, idx, in_worklist, extra_worklist) do val::SSAValue phi_uses[val.id] -= 1 end else return maybe_erase_unused!(compact, idx, in_worklist, extra_worklist) end end function mark_phi_cycles!(compact::IncrementalCompact, safe_phis::SPCSet, phi::Int) worklist = Int[] push!(worklist, phi) while !isempty(worklist) phi = pop!(worklist) push!(safe_phis, phi) for ur in userefs(compact.result[phi][:stmt]) val = ur[] isa(val, SSAValue) || continue isa(compact[val][:stmt], PhiNode) || continue (val.id in safe_phis) && continue push!(worklist, val.id) end end end function is_some_union(@nospecialize(t)) return isa(t, Union) end function is_union_phi(compact::IncrementalCompact, idx::Int) inst = compact.result[idx] isa(inst[:stmt], PhiNode) || return false return is_some_union(inst[:type]) end function kill_phi!(compact::IncrementalCompact, phi_uses::Vector{Int}, to_drop::Union{Vector{Int}, UnitRange{Int}}, ssa::SSAValue, phi::PhiNode, delete_inst::Bool = false) for d in to_drop if isassigned(phi.values, d) val = phi.values[d] if !delete_inst # Deleting the inst will update compact's use count, so # don't do it here. kill_current_use!(compact, val) end if isa(val, SSAValue) phi_uses[val.id] -= 1 end end end if delete_inst compact[ssa] = nothing elseif !isempty(to_drop) deleteat!(phi.values, to_drop) deleteat!(phi.edges, to_drop) end end """ adce_pass!(ir::IRCode) -> newir::IRCode Aggressive Dead Code Elimination pass. In addition to a simple DCE for unused values and allocations, this pass also nullifies `typeassert` calls that can be proved to be no-op, in order to allow LLVM to emit simpler code down the road. Note that this pass is more effective after SROA optimization (i.e. `sroa_pass!`), since SROA often allows this pass to: - eliminate allocation of object whose field references are all replaced with scalar values, and - nullify `typeassert` call whose first operand has been replaced with a scalar value (, which may have introduced new type information that inference did not understand) Also note that currently this pass _needs_ to run after `sroa_pass!`, because the `typeassert` elimination depends on the transformation by `canonicalize_typeassert!` done within `sroa_pass!` which redirects references of `typeassert`ed value to the corresponding `PiNode`. """ function adce_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing) 𝕃ₒ = inlining === nothing ? SimpleInferenceLattice.instance : optimizer_lattice(inlining.interp) phi_uses = fill(0, length(ir.stmts) + length(ir.new_nodes)) all_phis = Int[] unionphis = Pair{Int,Any}[] # sorted compact = IncrementalCompact(ir, true) made_changes = false for ((old_idx, idx), stmt) in compact if isa(stmt, PhiNode) if reprocess_phi_node!(𝕃ₒ, compact, stmt, old_idx) # Phi node has a single predecessor and was deleted made_changes = true continue end push!(all_phis, idx) if is_some_union(compact.result[idx][:type]) push!(unionphis, Pair{Int,Any}(idx, Union{})) end elseif isa(stmt, PiNode) val = stmt.val if isa(val, SSAValue) && is_union_phi(compact, val.id) r = searchsorted(unionphis, val.id; by = first) if !isempty(r) unionphi = unionphis[first(r)] t = tmerge(𝕃ₒ, unionphi[2], stmt.typ) unionphis[first(r)] = Pair{Int,Any}(unionphi[1], t) end end else if is_known_call(stmt, typeassert, compact) && length(stmt.args) == 3 # nullify safe `typeassert` calls ty, isexact = instanceof_tfunc(argextype(stmt.args[3], compact), true) if isexact && ⊑(𝕃ₒ, argextype(stmt.args[2], compact), ty) delete_inst_here!(compact) continue end end for ur in userefs(stmt) use = ur[] if isa(use, SSAValue) && is_union_phi(compact, use.id) r = searchsorted(unionphis, use.id; by = first) if !isempty(r) deleteat!(unionphis, first(r)) end end end end end non_dce_finish!(compact) for phi in all_phis inst = compact.result[phi] for ur in userefs(inst[:stmt]::PhiNode) use = ur[] if isa(use, SSAValue) phi_uses[use.id] += 1 stmt = compact.result[use.id][:stmt] if isa(stmt, PhiNode) r = searchsorted(unionphis, use.id; by=first) if !isempty(r) unionphi = unionphis[first(r)] unionphis[first(r)] = Pair{Int,Any}(unionphi[1], tmerge(𝕃ₒ, unionphi[2], inst[:type])) end end end end end # Narrow any union phi nodes that have unused branches for i = 1:length(unionphis) unionphi = unionphis[i] phi = unionphi[1] t = unionphi[2] inst = compact.result[phi] if t === Union{} stmt = inst[:stmt]::PhiNode kill_phi!(compact, phi_uses, 1:length(stmt.values), SSAValue(phi), stmt, true) made_changes = true continue elseif t === Any continue end ⊏ = strictpartialorder(𝕃ₒ) t ⊏ inst[:type] || continue to_drop = Int[] stmt = inst[:stmt] stmt === nothing && continue stmt = stmt::PhiNode for i = 1:length(stmt.values) if !isassigned(stmt.values, i) # Should be impossible to have something used only by PiNodes that's undef push!(to_drop, i) elseif !hasintersect(argextype_widened(stmt.values[i], compact), widenconst(t)) push!(to_drop, i) end end inst[:type] = t add_flag!(inst, IR_FLAG_REFINED) # t ⊏ inst[:type] kill_phi!(compact, phi_uses, to_drop, SSAValue(phi), stmt, false) made_changes = true end # Perform simple DCE for unused values extra_worklist = Int[] for (idx, nused) in Iterators.enumerate(compact.used_ssas) idx >= compact.result_idx && break nused == 0 || continue made_changes |= adce_erase!(phi_uses, extra_worklist, compact, idx, false) end while !isempty(extra_worklist) made_changes |= adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist), true) end # Go back and erase any phi cycles changed = true while changed changed = false safe_phis = SPCSet() for phi in all_phis # Save any phi cycles that have non-phi uses if compact.used_ssas[phi] - phi_uses[phi] != 0 mark_phi_cycles!(compact, safe_phis, phi) end end for phi in all_phis if !(phi in safe_phis) push!(extra_worklist, phi) end end while !isempty(extra_worklist) if adce_erase!(phi_uses, extra_worklist, compact, pop!(extra_worklist), true) changed = true made_changes = true end end end return Pair{IRCode, Bool}(complete(compact), made_changes) end function is_bb_empty(ir::IRCode, bb::BasicBlock) isempty(bb.stmts) && return true if length(bb.stmts) == 1 stmt = ir[SSAValue(first(bb.stmts))][:stmt] return stmt === nothing || isa(stmt, GotoNode) end return false end # TODO: This is terrible, we should change the IR for GotoIfNot to gain an else case function is_legal_bb_drop(_ir::IRCode, bbidx::Int, _bb::BasicBlock) # For the time being, don't drop the first bb, because it has special predecessor semantics. bbidx == 1 && return false return true end function legalize_bb_drop_pred!(ir::IRCode, bb::BasicBlock, bbidx::Int, bbs::Vector{BasicBlock}, dropped_bbs::Vector{Int}) (bbidx-1) in bb.preds || return true last_fallthrough = bbidx-1 dbi = length(dropped_bbs) while dbi != 0 && dropped_bbs[dbi] == last_fallthrough && (last_fallthrough-1 in bbs[last_fallthrough].preds) last_fallthrough -= 1 dbi -= 1 end last_fallthrough_term_ssa = SSAValue(last(bbs[last_fallthrough].stmts)) terminator = ir[last_fallthrough_term_ssa][:stmt] if isa(terminator, GotoIfNot) if terminator.dest != bbidx # The previous terminator's destination matches our fallthrough. # If we're also a fallthrough terminator, then we just have # to delete the GotoIfNot. our_terminator = ir[SSAValue(last(bb.stmts))][:stmt] if terminator.dest != (isa(our_terminator, GotoNode) ? our_terminator.label : bbidx + 1) return false end end ir[last_fallthrough_term_ssa] = nothing kill_edge!(bbs, last_fallthrough, terminator.dest) elseif isa(terminator, GotoNode) return true elseif isterminator(terminator) return false end # Hack, but effective. If we have a predecessor with a fall-through terminator, change the # instruction numbering to merge the blocks now such that below processing will properly # update it. bbs[last_fallthrough] = BasicBlock(first(bbs[last_fallthrough].stmts):last(bb.stmts), bbs[last_fallthrough].preds, bbs[last_fallthrough].succs) return true end function follow_map(map::Vector{Int}, idx::Int) while map[idx] ≠ 0 idx = map[idx] end return idx end function ascend_eliminated_preds(bbs::Vector{BasicBlock}, pred::Int) pred == 0 && return pred while pred != 1 && length(bbs[pred].preds) == 1 && length(bbs[pred].succs) == 1 pred = bbs[pred].preds[1] end return pred end # Compute (renamed) successors and predecessors given (renamed) block function compute_succs(merged_succ::Vector{Int}, bbs::Vector{BasicBlock}, result_bbs::Vector{Int}, bb_rename_succ::Vector{Int}, i::Int) orig_bb = follow_map(merged_succ, result_bbs[i]) return Int[bb_rename_succ[i] for i in bbs[orig_bb].succs] end function compute_preds(bbs::Vector{BasicBlock}, result_bbs::Vector{Int}, bb_rename_pred::Vector{Int}, i::Int) orig_bb = result_bbs[i] preds = copy(bbs[orig_bb].preds) res = Int[] while !isempty(preds) pred = popfirst!(preds) if pred == 0 push!(res, 0) continue end r = bb_rename_pred[pred] (r == -2 || r == -1) && continue if r == -3 prepend!(preds, bbs[pred].preds) else push!(res, r) end end return res end function add_preds!(all_new_preds::Vector{Int32}, bbs::Vector{BasicBlock}, bb_rename_pred::Vector{Int}, old_edge::Int32) preds = copy(bbs[old_edge].preds) while !isempty(preds) old_edge′ = popfirst!(preds) if old_edge′ == 0 push!(all_new_preds, old_edge′) continue end new_edge = bb_rename_pred[old_edge′] if new_edge > 0 && new_edge ∉ all_new_preds push!(all_new_preds, Int32(new_edge)) elseif new_edge == -3 prepend!(preds, bbs[old_edge′].preds) end end end function cfg_simplify!(ir::IRCode) bbs = ir.cfg.blocks merge_into = zeros(Int, length(bbs)) merged_succ = zeros(Int, length(bbs)) dropped_bbs = Vector{Int}() # sorted # Walk the CFG from the entry block and aggressively combine blocks for (idx, bb) in enumerate(bbs) if length(bb.succs) == 1 succ = bb.succs[1] if length(bbs[succ].preds) == 1 && succ != 1 # Can't merge blocks with a non-GotoNode terminator, even if they # only have one successor, because it would not be legal to have that # terminator in the middle of a basic block. terminator = ir[SSAValue(last(bb.stmts))][:stmt] if !isa(terminator, GotoNode) && isterminator(terminator) continue end # Prevent cycles by making sure we don't end up back at `idx` # by following what is to be merged into `succ` if follow_map(merged_succ, succ) != idx merge_into[succ] = idx merged_succ[idx] = succ end elseif merge_into[idx] == 0 && is_bb_empty(ir, bb) && is_legal_bb_drop(ir, idx, bb) # If this BB is empty, we can still merge it as long as none of our successor's phi nodes # reference our predecessors. # # This is for situations like: # #1 - ... # goto #3 if not ... # #2 - (empty) # #3 - ϕ(#2 => true, #1 => false) # # where we rely on the empty basic block to disambiguate the ϕ-node's value found_interference = false preds = Int[ascend_eliminated_preds(bbs, pred) for pred in bb.preds] for idx in bbs[succ].stmts stmt = ir[SSAValue(idx)][:stmt] stmt === nothing && continue isa(stmt, PhiNode) || break for edge in stmt.edges edge = ascend_eliminated_preds(bbs, Int(edge)) for pred in preds if pred == edge found_interference = true @goto done end end end end @label done found_interference && continue legalize_bb_drop_pred!(ir, bb, idx, bbs, dropped_bbs) || continue push!(dropped_bbs, idx) end end end # Assign new BB numbers in DFS order, dropping unreachable blocks max_bb_num = 1 bb_rename_succ = zeros(Int, length(bbs)) worklist = BitSetBoundedMinPrioritySet(length(bbs)) push!(worklist, 1) while !isempty(worklist) i = popfirst!(worklist) # Drop blocks that will be merged away if merge_into[i] != 0 bb_rename_succ[i] = typemin(Int) end # Mark dropped blocks for fixup if !isempty(searchsorted(dropped_bbs, i)) succ = bbs[i].succs[1] push!(worklist, succ) bb_rename_succ[i] = -succ end if bb_rename_succ[i] == 0 curr = i while true @assert bb_rename_succ[curr] == 0 bb_rename_succ[curr] = max_bb_num max_bb_num += 1 # Now walk the chain of blocks we merged. # If we end in something that may fall through, # we have to schedule that block next while merged_succ[curr] != 0 if bb_rename_succ[curr] == 0 bb_rename_succ[curr] = typemin(Int) end curr = merged_succ[curr] end terminator = ir[SSAValue(bbs[curr].stmts[end])][:stmt] if isa(terminator, GotoIfNot) if bb_rename_succ[terminator.dest] == 0 push!(worklist, terminator.dest) end elseif isa(terminator, EnterNode) catchbb = terminator.catch_dest if catchbb ≠ 0 if bb_rename_succ[catchbb] == 0 push!(worklist, catchbb) end end elseif isa(terminator, GotoNode) || isa(terminator, ReturnNode) # No implicit fall through. Schedule from work list. break else is_bottom = ir[SSAValue(bbs[curr].stmts[end])][:type] === Union{} if is_bottom && !isa(terminator, PhiNode) && terminator !== nothing # If this is a regular statement (not PhiNode/GotoNode/GotoIfNot # or the `nothing` special case deletion marker), # and the type is Union{}, then this may be a terminator. # Ordinarily we normalize with ReturnNode(), but this is not # required. In any case, we do not fall through, so we # do not need to schedule the fall-through block. break end end ncurr = curr + 1 while !isempty(searchsorted(dropped_bbs, ncurr)) bb_rename_succ[ncurr] = -bbs[ncurr].succs[1] ncurr += 1 end curr = ncurr end for succ in bbs[curr].succs if bb_rename_succ[succ] == 0 push!(worklist, succ) end end end end # Fixup dropped BBs resolved_all = false while !resolved_all # TODO: There are faster ways to do this resolved_all = true for bb in dropped_bbs obb = bb_rename_succ[bb] if obb < 0 && obb != typemin(Int) nsucc = bb_rename_succ[-obb] if nsucc == typemin(Int) nsucc = -merge_into[-obb] end bb_rename_succ[bb] = nsucc resolved_all = false end end end # Drop remaining unvisited bbs bb_rename_pred = zeros(Int, length(bbs)) for i = 1:length(bbs) if bb_rename_succ[i] == 0 bb_rename_succ[i] = -2 bb_rename_pred[i] = -2 elseif bb_rename_succ[i] == typemin(Int) bb_rename_succ[i] = -2 end end # Compute map from new to old blocks result_bbs = zeros(Int, max_bb_num-1) for (o, bb) in enumerate(bb_rename_succ) bb > 0 || continue isempty(searchsorted(dropped_bbs, o)) || continue result_bbs[bb] = o end # Figure out how predecessors should be renamed for i = 1:length(bbs) if merged_succ[i] != 0 # Block `i` should no longer be a predecessor (before renaming) # because it is being merged with its sole successor bb_rename_pred[i] = -1 continue end pred = i is_unreachable = false is_multi = false while pred !== 1 && !isempty(searchsorted(dropped_bbs, pred)) preds = bbs[pred].preds if length(preds) == 0 is_unreachable = true break elseif length(preds) > 1 # This block has multiple predecessors - the only way this is # legal is if we proved above that our successors don't have # any phi nodes that would interfere with the renaming. Mark # this specially. is_multi = true break end @assert length(preds) == 1 pred = preds[1] end if is_unreachable @assert bb_rename_pred[i] == -2 elseif is_multi bb_rename_pred[i] = -3 else bbnum = follow_map(merge_into, pred) bb_rename_pred[i] = bb_rename_succ[bbnum] end end # Compute new block lengths result_bbs_lengths = zeros(Int, max_bb_num-1) for (idx, orig_bb) in enumerate(result_bbs) ms = orig_bb while ms != 0 result_bbs_lengths[idx] += length(bbs[ms].stmts) ms = merged_succ[ms] end end # Compute statement indices the new blocks start at bb_starts = Vector{Int}(undef, 1+length(result_bbs_lengths)) bb_starts[1] = 1 for i = 1:length(result_bbs_lengths) bb_starts[i+1] = bb_starts[i] + result_bbs_lengths[i] end cresult_bbs = BasicBlock[ BasicBlock(StmtRange(bb_starts[i], bb_starts[i+1]-1), compute_preds(bbs, result_bbs, bb_rename_pred, i), compute_succs(merged_succ, bbs, result_bbs, bb_rename_succ, i)) for i = 1:length(result_bbs)] # Fixup terminators for any blocks that would have caused double edges for (bbidx, new_bb) in enumerate(cresult_bbs) @assert length(new_bb.succs) <= 2 length(new_bb.succs) <= 1 && continue if new_bb.succs[1] == new_bb.succs[2] old_bb2 = findfirst(x::Int->x==bbidx, bb_rename_pred)::Int terminator = ir[SSAValue(last(bbs[old_bb2].stmts))] @assert terminator[:stmt] isa GotoIfNot # N.B.: The dest will be renamed in process_node! below terminator[:stmt] = GotoNode(terminator[:stmt].dest::Int) pop!(new_bb.succs) new_succ = cresult_bbs[new_bb.succs[1]] for (i, nsp) in enumerate(new_succ.preds) if nsp == bbidx deleteat!(new_succ.preds, i) break end end end end # Run instruction compaction to produce the result, # but we're messing with the CFG # so we don't want compaction to do so independently compact = IncrementalCompact(ir, CFGTransformState(true, false, cresult_bbs, bb_rename_pred, bb_rename_succ, nothing, nothing)) for (idx, orig_bb) in enumerate(result_bbs) ms = orig_bb bb_start = true while ms != 0 old_bb_stmts = bbs[ms].stmts for i in old_bb_stmts node = ir.stmts[i] compact.result[compact.result_idx] = node stmt = node[:stmt] if isa(stmt, GotoNode) && merged_succ[ms] != 0 # If we merged a basic block, we need remove the trailing GotoNode (if any) compact.result[compact.result_idx][:stmt] = nothing elseif isa(stmt, PhiNode) phi = stmt (; ssa_rename, late_fixup, used_ssas, new_new_used_ssas) = compact ssa_rename[i] = SSAValue(compact.result_idx) already_inserted = function (branch::Int, val::OldSSAValue) if val.id in old_bb_stmts return val.id <= i end return 0 < bb_rename_pred[phi.edges[branch]] < idx end edges = Int32[] values = Any[] sizehint!(edges, length(phi.edges)); sizehint!(values, length(phi.values)) for old_index in 1:length(phi.edges) old_edge = phi.edges[old_index] new_edge = bb_rename_pred[old_edge] if new_edge > 0 push!(edges, new_edge) if isassigned(phi.values, old_index) val = process_phinode_value(phi.values, old_index, late_fixup, already_inserted, compact.result_idx, ssa_rename, used_ssas, new_new_used_ssas, true, nothing) push!(values, val) else resize!(values, length(values)+1) end elseif new_edge == -1 @assert length(phi.edges) == 1 if isassigned(phi.values, old_index) val = process_phinode_value(phi.values, old_index, late_fixup, already_inserted, compact.result_idx, ssa_rename, used_ssas, new_new_used_ssas, true, nothing) push!(edges, -1) push!(values, val) end elseif new_edge == -3 # Multiple predecessors, we need to expand out this phi all_new_preds = Int32[] add_preds!(all_new_preds, bbs, bb_rename_pred, old_edge) append!(edges, all_new_preds) np = length(all_new_preds) if np > 0 if isassigned(phi.values, old_index) val = process_phinode_value(phi.values, old_index, late_fixup, already_inserted, compact.result_idx, ssa_rename, used_ssas, new_new_used_ssas, true, nothing) for p in 1:np push!(values, val) p > 2 && count_added_node!(compact, val) end else resize!(values, length(values)+np) end end end end if length(edges) == 0 || (length(edges) == 1 && !isassigned(values, 1)) compact.result[compact.result_idx][:stmt] = nothing elseif length(edges) == 1 && !bb_start compact.result[compact.result_idx][:stmt] = values[1] else @assert bb_start compact.result[compact.result_idx][:stmt] = PhiNode(edges, values) end else ri = process_node!(compact, compact.result_idx, node, i, i, ms, true) if ri == compact.result_idx # process_node! wanted this statement dropped. We don't do this, # but we still need to erase the node compact.result[compact.result_idx][:stmt] = nothing end end # We always increase the result index to ensure a predictable # placement of the resulting nodes. compact.result_idx += 1 end ms = merged_succ[ms] bb_start = false end end compact.idx = length(ir.stmts) compact.active_result_bb = length(bb_starts) return finish(compact) end