Skip to content

Commit c503611

Browse files
authored
refactor SROA test cases (#44706)
Mostly adapted from #43888. Should be more robust and cover more cases.
1 parent a5fe945 commit c503611

File tree

1 file changed

+139
-28
lines changed

1 file changed

+139
-28
lines changed

test/compiler/irpasses.jl

+139-28
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
using Test
44
using Base.Meta
5-
using Core: PhiNode, SSAValue, GotoNode, PiNode, QuoteNode, ReturnNode, GotoIfNot
5+
import Core:
6+
CodeInfo, Argument, SSAValue, GotoNode, GotoIfNot, PiNode, PhiNode,
7+
QuoteNode, ReturnNode
68

79
include(normpath(@__DIR__, "irutils.jl"))
810

@@ -12,7 +14,7 @@ include(normpath(@__DIR__, "irutils.jl"))
1214
## Test that domsort doesn't mangle single-argument phis (#29262)
1315
let m = Meta.@lower 1 + 1
1416
@assert Meta.isexpr(m, :thunk)
15-
src = m.args[1]::Core.CodeInfo
17+
src = m.args[1]::CodeInfo
1618
src.code = Any[
1719
# block 1
1820
Expr(:call, :opaque),
@@ -47,7 +49,7 @@ end
4749
# test that we don't stack-overflow in SNCA with large functions.
4850
let m = Meta.@lower 1 + 1
4951
@assert Meta.isexpr(m, :thunk)
50-
src = m.args[1]::Core.CodeInfo
52+
src = m.args[1]::CodeInfo
5153
code = Any[]
5254
N = 2^15
5355
for i in 1:2:N
@@ -73,30 +75,87 @@ end
7375
# SROA
7476
# ====
7577

78+
import Core.Compiler: widenconst
79+
80+
is_load_forwarded(src::CodeInfo) = !any(iscall((src, getfield)), src.code)
81+
is_scalar_replaced(src::CodeInfo) =
82+
is_load_forwarded(src) && !any(iscall((src, setfield!)), src.code) && !any(isnew, src.code)
83+
84+
function is_load_forwarded(@nospecialize(T), src::CodeInfo)
85+
for i in 1:length(src.code)
86+
x = src.code[i]
87+
if iscall((src, getfield), x)
88+
widenconst(argextype(x.args[1], src)) <: T && return false
89+
end
90+
end
91+
return true
92+
end
93+
function is_scalar_replaced(@nospecialize(T), src::CodeInfo)
94+
is_load_forwarded(T, src) || return false
95+
for i in 1:length(src.code)
96+
x = src.code[i]
97+
if iscall((src, setfield!), x)
98+
widenconst(argextype(x.args[1], src)) <: T && return false
99+
elseif isnew(x)
100+
widenconst(argextype(SSAValue(i), src)) <: T && return false
101+
end
102+
end
103+
return true
104+
end
105+
76106
struct ImmutableXYZ; x; y; z; end
77107
mutable struct MutableXYZ; x; y; z; end
108+
struct ImmutableOuter{T}; x::T; y::T; z::T; end
109+
mutable struct MutableOuter{T}; x::T; y::T; z::T; end
110+
struct ImmutableRef{T}; x::T; end
111+
Base.getindex(r::ImmutableRef) = r.x
112+
mutable struct SafeRef{T}; x::T; end
113+
Base.getindex(s::SafeRef) = getfield(s, 1)
114+
Base.setindex!(s::SafeRef, x) = setfield!(s, 1, x)
115+
116+
# simple immutability
117+
# -------------------
78118

79-
# should optimize away very basic cases
80119
let src = code_typed1((Any,Any,Any)) do x, y, z
81120
xyz = ImmutableXYZ(x, y, z)
82121
xyz.x, xyz.y, xyz.z
83122
end
84-
@test !any(isnew, src.code)
123+
@test is_scalar_replaced(src)
124+
@test any(src.code) do @nospecialize x
125+
iscall((src, tuple), x) &&
126+
x.args[2:end] == Any[#=x=# Core.Argument(2), #=y=# Core.Argument(3), #=z=# Core.Argument(4)]
127+
end
85128
end
129+
let src = code_typed1((Any,Any,Any)) do x, y, z
130+
xyz = (x, y, z)
131+
xyz[1], xyz[2], xyz[3]
132+
end
133+
@test is_scalar_replaced(src)
134+
@test any(src.code) do @nospecialize x
135+
iscall((src, tuple), x) &&
136+
x.args[2:end] == Any[#=x=# Core.Argument(2), #=y=# Core.Argument(3), #=z=# Core.Argument(4)]
137+
end
138+
end
139+
140+
# simple mutability
141+
# -----------------
142+
86143
let src = code_typed1((Any,Any,Any)) do x, y, z
87144
xyz = MutableXYZ(x, y, z)
88145
xyz.x, xyz.y, xyz.z
89146
end
90-
@test !any(isnew, src.code)
147+
@test is_scalar_replaced(src)
148+
@test any(src.code) do @nospecialize x
149+
iscall((src, tuple), x) &&
150+
x.args[2:end] == Any[#=x=# Core.Argument(2), #=y=# Core.Argument(3), #=z=# Core.Argument(4)]
151+
end
91152
end
92-
93-
# should handle simple mutabilities
94153
let src = code_typed1((Any,Any,Any)) do x, y, z
95154
xyz = MutableXYZ(x, y, z)
96155
xyz.y = 42
97156
xyz.x, xyz.y, xyz.z
98157
end
99-
@test !any(isnew, src.code)
158+
@test is_scalar_replaced(src)
100159
@test any(src.code) do @nospecialize x
101160
iscall((src, tuple), x) &&
102161
x.args[2:end] == Any[#=x=# Core.Argument(2), 42, #=x=# Core.Argument(4)]
@@ -107,19 +166,23 @@ let src = code_typed1((Any,Any,Any)) do x, y, z
107166
xyz.x, xyz.z = xyz.z, xyz.x
108167
xyz.x, xyz.y, xyz.z
109168
end
110-
@test !any(isnew, src.code)
169+
@test is_scalar_replaced(src)
111170
@test any(src.code) do @nospecialize x
112171
iscall((src, tuple), x) &&
113172
x.args[2:end] == Any[#=z=# Core.Argument(4), #=y=# Core.Argument(3), #=x=# Core.Argument(2)]
114173
end
115174
end
116-
# circumvent uninitialized fields as far as there is a solid `setfield!` definition
175+
176+
# uninitialized fields
177+
# --------------------
178+
179+
# safe cases
117180
let src = code_typed1() do
118181
r = Ref{Any}()
119182
r[] = 42
120183
return r[]
121184
end
122-
@test !any(isnew, src.code)
185+
@test is_scalar_replaced(src)
123186
end
124187
let src = code_typed1((Bool,)) do cond
125188
r = Ref{Any}()
@@ -131,7 +194,7 @@ let src = code_typed1((Bool,)) do cond
131194
return r[]
132195
end
133196
end
134-
@test !any(isnew, src.code)
197+
@test is_scalar_replaced(src)
135198
end
136199
let src = code_typed1((Bool,)) do cond
137200
r = Ref{Any}()
@@ -142,7 +205,7 @@ let src = code_typed1((Bool,)) do cond
142205
end
143206
return r[]
144207
end
145-
@test !any(isnew, src.code)
208+
@test is_scalar_replaced(src)
146209
end
147210
let src = code_typed1((Bool,Bool,Any,Any,Any)) do c1, c2, x, y, z
148211
r = Ref{Any}()
@@ -157,7 +220,16 @@ let src = code_typed1((Bool,Bool,Any,Any,Any)) do c1, c2, x, y, z
157220
end
158221
return r[]
159222
end
160-
@test !any(isnew, src.code)
223+
@test is_scalar_replaced(src)
224+
end
225+
226+
# unsafe cases
227+
let src = code_typed1() do
228+
r = Ref{Any}()
229+
return r[]
230+
end
231+
@test count(isnew, src.code) == 1
232+
@test count(iscall((src, getfield)), src.code) == 1
161233
end
162234
let src = code_typed1((Bool,)) do cond
163235
r = Ref{Any}()
@@ -167,7 +239,9 @@ let src = code_typed1((Bool,)) do cond
167239
return r[]
168240
end
169241
# N.B. `r` should be allocated since `cond` might be `false` and then it will be thrown
170-
@test any(isnew, src.code)
242+
@test count(isnew, src.code) == 1
243+
@test count(iscall((src, setfield!)), src.code) == 1
244+
@test count(iscall((src, getfield)), src.code) == 1
171245
end
172246
let src = code_typed1((Bool,Bool,Any,Any)) do c1, c2, x, y
173247
r = Ref{Any}()
@@ -181,12 +255,16 @@ let src = code_typed1((Bool,Bool,Any,Any)) do c1, c2, x, y
181255
return r[]
182256
end
183257
# N.B. `r` should be allocated since `c2` might be `false` and then it will be thrown
184-
@test any(isnew, src.code)
258+
@test count(isnew, src.code) == 1
259+
@test count(iscall((src, setfield!)), src.code) == 2
260+
@test count(iscall((src, getfield)), src.code) == 1
185261
end
186262

187-
# should include a simple alias analysis
188-
struct ImmutableOuter{T}; x::T; y::T; z::T; end
189-
mutable struct MutableOuter{T}; x::T; y::T; z::T; end
263+
# aliased load forwarding
264+
# -----------------------
265+
# TODO fix broken examples with EscapeAnalysis
266+
267+
# OK: immutable(immutable(...)) case
190268
let src = code_typed1((Any,Any,Any)) do x, y, z
191269
xyz = ImmutableXYZ(x, y, z)
192270
outer = ImmutableOuter(xyz, xyz, xyz)
@@ -214,22 +292,21 @@ let src = code_typed1((Any,Any,Any)) do x, y, z
214292
end
215293
end
216294

217-
# FIXME our analysis isn't yet so powerful at this moment: may be unable to handle nested objects well
218-
# OK: mutable(immutable(...)) case
295+
# OK (mostly): immutable(mutable(...)) case
219296
let src = code_typed1((Any,Any,Any)) do x, y, z
220297
xyz = MutableXYZ(x, y, z)
221298
t = (xyz,)
222299
v = t[1].x
223300
v, v, v
224301
end
225-
@test !any(isnew, src.code)
302+
@test is_scalar_replaced(src)
226303
end
227304
let src = code_typed1((Any,Any,Any)) do x, y, z
228305
xyz = MutableXYZ(x, y, z)
229306
outer = ImmutableOuter(xyz, xyz, xyz)
230307
outer.x.x, outer.y.y, outer.z.z
231308
end
232-
@test !any(isnew, src.code)
309+
@test is_scalar_replaced(src)
233310
@test any(src.code) do @nospecialize x
234311
iscall((src, tuple), x) &&
235312
x.args[2:end] == Any[#=x=# Core.Argument(2), #=y=# Core.Argument(3), #=y=# Core.Argument(4)]
@@ -240,12 +317,27 @@ let # this is a simple end to end test case, which demonstrates allocation elimi
240317
# NOTE this test case isn't so robust and might be subject to future changes of the broadcasting implementation,
241318
# in that case you don't really need to stick to keeping this test case around
242319
simple_sroa(s) = broadcast(identity, Ref(s))
320+
let src = code_typed1(simple_sroa, (String,))
321+
@test is_scalar_replaced(src)
322+
end
243323
s = Base.inferencebarrier("julia")::String
244324
simple_sroa(s)
245325
# NOTE don't hard-code `"julia"` in `@allocated` clause and make sure to execute the
246326
# compiled code for `simple_sroa`, otherwise everything can be folded even without SROA
247327
@test @allocated(simple_sroa(s)) == 0
248328
end
329+
let # FIXME: some nested example
330+
src = code_typed1((Int,)) do x
331+
Ref(Ref(x))[][]
332+
end
333+
@test_broken is_scalar_replaced(src)
334+
335+
src = code_typed1((Int,)) do x
336+
Ref(Ref(Ref(Ref(Ref(Ref(Ref(Ref(Ref(Ref((x)))))))))))[][][][][][][][][][]
337+
end
338+
@test_broken is_scalar_replaced(src)
339+
end
340+
249341
# FIXME: immutable(mutable(...)) case
250342
let src = code_typed1((Any,Any,Any)) do x, y, z
251343
xyz = ImmutableXYZ(x, y, z)
@@ -314,6 +406,25 @@ let src = code_typed1(compute_points)
314406
@test !any(isnew, src.code)
315407
end
316408

409+
# preserve elimination
410+
# --------------------
411+
412+
let src = code_typed1((String,)) do s
413+
ccall(:some_ccall, Cint, (Ptr{String},), Ref(s))
414+
end
415+
@test count(isnew, src.code) == 0
416+
end
417+
418+
# if the mutable struct is directly used, we shouldn't eliminate it
419+
let src = code_typed1() do
420+
a = MutableXYZ(-512275808,882558299,-2133022131)
421+
b = Int32(42)
422+
ccall(:some_ccall, Cvoid, (MutableXYZ, Int32), a, b)
423+
return a.x
424+
end
425+
@test count(isnew, src.code) == 1
426+
end
427+
317428
# comparison lifting
318429
# ==================
319430

@@ -454,7 +565,7 @@ end
454565
# A SSAValue after the compaction line
455566
let m = Meta.@lower 1 + 1
456567
@assert Meta.isexpr(m, :thunk)
457-
src = m.args[1]::Core.CodeInfo
568+
src = m.args[1]::CodeInfo
458569
src.code = Any[
459570
# block 1
460571
nothing,
@@ -517,7 +628,7 @@ end
517628
let m = Meta.@lower 1 + 1
518629
# Test that CFG simplify combines redundant basic blocks
519630
@assert Meta.isexpr(m, :thunk)
520-
src = m.args[1]::Core.CodeInfo
631+
src = m.args[1]::CodeInfo
521632
src.code = Any[
522633
Core.Compiler.GotoNode(2),
523634
Core.Compiler.GotoNode(3),
@@ -542,7 +653,7 @@ end
542653
let m = Meta.@lower 1 + 1
543654
# Test that CFG simplify doesn't mess up when chaining past return blocks
544655
@assert Meta.isexpr(m, :thunk)
545-
src = m.args[1]::Core.CodeInfo
656+
src = m.args[1]::CodeInfo
546657
src.code = Any[
547658
Core.Compiler.GotoIfNot(Core.Compiler.Argument(2), 3),
548659
Core.Compiler.GotoNode(4),
@@ -572,7 +683,7 @@ let m = Meta.@lower 1 + 1
572683
# Test that CFG simplify doesn't try to merge every block in a loop into
573684
# its predecessor
574685
@assert Meta.isexpr(m, :thunk)
575-
src = m.args[1]::Core.CodeInfo
686+
src = m.args[1]::CodeInfo
576687
src.code = Any[
577688
# Block 1
578689
Core.Compiler.GotoNode(2),

0 commit comments

Comments
 (0)