Skip to content

Commit 10cf29d

Browse files
committed
optimizer: eliminate isdefined in sroa_mutables!
This commit implements a simple optimization within `sroa_mutables!` to eliminate `isdefined` call by checking load-forwardability of the field. This optimization may be especially useful to eliminate extra allocation of `Core.Box` involved with a capturing closure, e.g.: ```julia julia> callit(f, args...) = f(args...); julia> function isdefined_elim() local arr::Vector{Any} callit() do arr = Any[] end return arr end; julia> code_typed(isdefined_elim) ``` ```diff diff --git a/_master.jl b/_pr.jl index 3aa40ba20e5..11eccf65f32 100644 --- a/_master.jl +++ b/_pr.jl @@ -1,15 +1,8 @@ 1-element Vector{Any}: CodeInfo( -1 ─ %1 = Core.Box::Type{Core.Box} -│ %2 = %new(%1)::Core.Box -│ %3 = $(Expr(:foreigncall, :(:jl_alloc_array_1d), Vector{Any}, svec(Any, Int64), 0, :(:ccall), Vector{Any}, 0, 0))::Vector{Any} -│ Core.setfield!(%2, :contents, %3)::Vector{Any} -│ %5 = Core.isdefined(%2, :contents)::Bool -└── goto #3 if not %5 -2 ─ goto #4 -3 ─ $(Expr(:throw_undef_if_not, :arr, false))::Any -4 ┄ %9 = Core.getfield(%2, :contents)::Any -│ Core.typeassert(%9, Vector{Any})::Vector{Any} -│ %11 = π (%9, Vector{Any}) -└── return %11 +1 ─ %1 = $(Expr(:foreigncall, :(:jl_alloc_array_1d), Vector{Any}, svec(Any, Int64), 0, :(:ccall), Vector{Any}, 0, 0))::Vector{Any} +└── goto #3 if not true +2 ─ goto #4 +3 ─ $(Expr(:throw_undef_if_not, :arr, false))::Any +4 ┄ return %1 ) => Vector{Any} ```
1 parent c503611 commit 10cf29d

File tree

2 files changed

+83
-12
lines changed

2 files changed

+83
-12
lines changed

base/compiler/ssair/passes.jl

+45-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ end
1010
du::SSADefUse
1111
1212
This struct keeps track of all uses of some mutable struct allocated in the current function:
13-
- `du.uses::Vector{Int}` are all instances of `getfield` on the struct
13+
- `du.uses::Vector{Int}` are all instances of `getfield` / `isdefined` on the struct
1414
- `du.defs::Vector{Int}` are all instances of `setfield!` on the struct
1515
The terminology refers to the uses/defs of the "slot bundle" that the mutable struct represents.
1616
@@ -27,7 +27,10 @@ struct SSADefUse
2727
end
2828
SSADefUse() = SSADefUse(Int[], Int[], Int[])
2929

30-
compute_live_ins(cfg::CFG, du::SSADefUse) = compute_live_ins(cfg, du.defs, du.uses)
30+
function compute_live_ins(cfg::CFG, du::SSADefUse)
31+
# filter out `isdefined` usages
32+
return compute_live_ins(cfg, du.defs, filter(>(0), du.uses))
33+
end
3134

3235
# assume `stmt == getfield(obj, field, ...)` or `stmt == setfield!(obj, field, val, ...)`
3336
try_compute_field_stmt(ir::Union{IncrementalCompact,IRCode}, stmt::Expr) =
@@ -725,7 +728,7 @@ function sroa_pass!(ir::IRCode)
725728
for ((_, idx), stmt) in compact
726729
# check whether this statement is `getfield` / `setfield!` (or other "interesting" statement)
727730
isa(stmt, Expr) || continue
728-
is_setfield = false
731+
is_setfield = is_isdefined = false
729732
field_ordering = :unspecified
730733
if is_known_call(stmt, setfield!, compact)
731734
4 <= length(stmt.args) <= 5 || continue
@@ -741,6 +744,13 @@ function sroa_pass!(ir::IRCode)
741744
field_ordering = argextype(stmt.args[4], compact)
742745
widenconst(field_ordering) === Bool && (field_ordering = :unspecified)
743746
end
747+
elseif is_known_call(stmt, isdefined, compact)
748+
3 <= length(stmt.args) <= 4 || continue
749+
is_isdefined = true
750+
if length(stmt.args) == 4
751+
field_ordering = argextype(stmt.args[4], compact)
752+
widenconst(field_ordering) === Bool && (field_ordering = :unspecified)
753+
end
744754
elseif isexpr(stmt, :foreigncall)
745755
nccallargs = length(stmt.args[3]::SimpleVector)
746756
preserved = Int[]
@@ -795,13 +805,11 @@ function sroa_pass!(ir::IRCode)
795805
lift_comparison!(===, compact, idx, stmt, lifting_cache)
796806
elseif is_known_call(stmt, isa, compact)
797807
lift_comparison!(isa, compact, idx, stmt, lifting_cache)
798-
elseif is_known_call(stmt, isdefined, compact)
799-
lift_comparison!(isdefined, compact, idx, stmt, lifting_cache)
800808
end
801809
continue
802810
end
803811

804-
# analyze this `getfield` / `setfield!` call
812+
# analyze this `getfield` / `isdefined` / `setfield!` call
805813

806814
field = try_compute_field_stmt(compact, stmt)
807815
field === nothing && continue
@@ -812,10 +820,15 @@ function sroa_pass!(ir::IRCode)
812820
if isa(struct_typ, Union) && struct_typ <: Tuple
813821
struct_typ = unswitchtupleunion(struct_typ)
814822
end
823+
if isa(struct_typ, Union) && is_isdefined
824+
lift_comparison!(isdefined, compact, idx, stmt, lifting_cache)
825+
continue
826+
end
815827
isa(struct_typ, DataType) || continue
816828

817829
struct_typ.name.atomicfields == C_NULL || continue # TODO: handle more
818-
if !(field_ordering === :unspecified || (field_ordering isa Const && field_ordering.val === :not_atomic))
830+
if !((field_ordering === :unspecified) ||
831+
(field_ordering isa Const && field_ordering.val === :not_atomic))
819832
continue
820833
end
821834

@@ -836,6 +849,8 @@ function sroa_pass!(ir::IRCode)
836849
mid, defuse = get!(defuses, def.id, (SPCSet(), SSADefUse()))
837850
if is_setfield
838851
push!(defuse.defs, idx)
852+
elseif is_isdefined
853+
push!(defuse.uses, -idx)
839854
else
840855
push!(defuse.uses, idx)
841856
end
@@ -844,6 +859,8 @@ function sroa_pass!(ir::IRCode)
844859
continue
845860
elseif is_setfield
846861
continue # invalid `setfield!` call, but just ignore here
862+
elseif is_isdefined
863+
continue # TODO?
847864
end
848865

849866
# perform SROA on immutable structs here on
@@ -927,9 +944,9 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse
927944
typ = typ::DataType
928945
# Partition defuses by field
929946
fielddefuse = SSADefUse[SSADefUse() for _ = 1:fieldcount(typ)]
930-
all_forwarded = true
947+
all_eliminated = all_forwarded = true
931948
for use in defuse.uses
932-
stmt = ir[SSAValue(use)][:inst] # == `getfield` call
949+
stmt = ir[SSAValue(abs(use))][:inst] # == `getfield`/`isdefined` call
933950
# We may have discovered above that this use is dead
934951
# after the getfield elim of immutables. In that case,
935952
# it would have been deleted. That's fine, just ignore
@@ -969,7 +986,15 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse
969986
blocks[fidx] = phiblocks, allblocks
970987
if fidx + 1 > length(defexpr.args)
971988
for use in du.uses
972-
has_safe_def(ir, get_domtree(), allblocks, du, newidx, use) || @goto skip
989+
if use > 0 # == `getfield` use
990+
has_safe_def(ir, get_domtree(), allblocks, du, newidx, use) || @goto skip
991+
else # == `isdefined` use
992+
if has_safe_def(ir, get_domtree(), allblocks, du, newidx, -use)
993+
ir[SSAValue(-use)][:inst] = true
994+
else
995+
all_eliminated = false
996+
end
997+
end
973998
end
974999
end
9751000
end
@@ -991,8 +1016,12 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse
9911016
NewInstruction(PhiNode(), ftyp))
9921017
end
9931018
# Now go through all uses and rewrite them
994-
for stmt in du.uses
995-
ir[SSAValue(stmt)][:inst] = compute_value_for_use(ir, domtree, allblocks, du, phinodes, fidx, stmt)
1019+
for use in du.uses
1020+
if use > 0 # == `getfield` use
1021+
ir[SSAValue(use)][:inst] = compute_value_for_use(ir, domtree, allblocks, du, phinodes, fidx, use)
1022+
else # == `isdefined` use
1023+
continue # already rewritten if possible
1024+
end
9961025
end
9971026
if !isbitstype(ftyp)
9981027
if preserve_uses !== nothing
@@ -1010,6 +1039,10 @@ function sroa_mutables!(ir::IRCode, defuses::IdDict{Int, Tuple{SPCSet, SSADefUse
10101039
end
10111040
end
10121041
end
1042+
all_eliminated || continue
1043+
# all "usages" (i.e. `getfield` and `isdefined` calls) are eliminated,
1044+
# now eliminate "definitions" (`setfield!`) calls
1045+
# (NOTE the allocation itself will be eliminated by DCE pass later)
10131046
for stmt in du.defs
10141047
stmt == newidx && continue
10151048
ir[SSAValue(stmt)][:inst] = nothing

test/compiler/irpasses.jl

+38
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,44 @@ let src = code_typed1() do
425425
@test count(isnew, src.code) == 1
426426
end
427427

428+
# isdefined elimination
429+
# ---------------------
430+
431+
let src = code_typed1((Any,)) do a
432+
r = Ref{Any}()
433+
r[] = a
434+
if isassigned(r)
435+
return r[]
436+
end
437+
return nothing
438+
end
439+
@test is_scalar_replaced(src)
440+
end
441+
442+
let src = code_typed1((Bool, Any,)) do cnd, a
443+
r = Ref{Any}()
444+
if cnd
445+
r[] = a # this `setfield!` shouldn't be eliminated
446+
end
447+
return isassigned(r)
448+
end
449+
@test count(isnew, src.code) == 1
450+
@test count(iscall((src, setfield!)), src.code) == 1
451+
end
452+
453+
callit(f, args...) = f(args...)
454+
function isdefined_elim()
455+
local arr::Vector{Any}
456+
callit() do
457+
arr = Any[]
458+
end
459+
return arr
460+
end
461+
let src = code_typed1(isdefined_elim)
462+
@test is_scalar_replaced(src)
463+
end
464+
@test isdefined_elim() == Any[]
465+
428466
# comparison lifting
429467
# ==================
430468

0 commit comments

Comments
 (0)