Skip to content

Commit ed5a573

Browse files
Kenotopolarity
authored andcommitted
Effects-tune PersitentDict (#52954)
To in particular allow elimination of dead PersistentDict constructions.
1 parent 3ebaf6c commit ed5a573

File tree

7 files changed

+105
-9
lines changed

7 files changed

+105
-9
lines changed

base/array.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,9 @@ See also [`copy!`](@ref Base.copy!), [`copyto!`](@ref), [`deepcopy`](@ref).
343343
copy
344344

345345
@eval function copy(a::Array{T}) where {T}
346+
# `jl_genericmemory_copy_slice` only throws when the size exceeds the max allocation
347+
# size, but since we're copying an existing array, we're guaranteed that this will not happen.
348+
@_nothrow_meta
346349
ref = a.ref
347350
newmem = ccall(:jl_genericmemory_copy_slice, Ref{Memory{T}}, (Any, Ptr{Cvoid}, Int), ref.mem, ref.ptr_or_offset, length(a))
348351
return $(Expr(:new, :(typeof(a)), :(Core.memoryref(newmem)), :(a.size)))
@@ -1040,6 +1043,7 @@ end
10401043
array_new_memory(mem::Memory, newlen::Int) = typeof(mem)(undef, newlen) # when implemented, this should attempt to first expand mem
10411044

10421045
function _growbeg!(a::Vector, delta::Integer)
1046+
@_noub_meta
10431047
delta = Int(delta)
10441048
delta == 0 && return # avoid attempting to index off the end
10451049
delta >= 0 || throw(ArgumentError("grow requires delta >= 0"))
@@ -1077,6 +1081,7 @@ function _growbeg!(a::Vector, delta::Integer)
10771081
end
10781082

10791083
function _growend!(a::Vector, delta::Integer)
1084+
@_noub_meta
10801085
delta = Int(delta)
10811086
delta >= 0 || throw(ArgumentError("grow requires delta >= 0"))
10821087
ref = a.ref
@@ -1113,6 +1118,7 @@ function _growend!(a::Vector, delta::Integer)
11131118
end
11141119

11151120
function _growat!(a::Vector, i::Integer, delta::Integer)
1121+
@_terminates_globally_noub_meta
11161122
delta = Int(delta)
11171123
i = Int(i)
11181124
i == 1 && return _growbeg!(a, delta)
@@ -1715,10 +1721,11 @@ julia> insert!(Any[1:6;], 3, "here")
17151721
```
17161722
"""
17171723
function insert!(a::Array{T,1}, i::Integer, item) where T
1724+
@_noub_meta
17181725
# Throw convert error before changing the shape of the array
17191726
_item = item isa T ? item : convert(T, item)::T
17201727
_growat!(a, i, 1)
1721-
# _growat! already did bound check
1728+
# :noub, because _growat! already did bound check
17221729
@inbounds a[i] = _item
17231730
return a
17241731
end

base/compiler/tfuncs.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3127,6 +3127,8 @@ function foreigncall_effects(@specialize(abstract_eval), e::Expr)
31273127
if name === :jl_alloc_genericmemory
31283128
nothrow = new_genericmemory_nothrow(abstract_eval, args)
31293129
return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow)
3130+
elseif name === :jl_genericmemory_copy_slice
3131+
return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow=false)
31303132
end
31313133
return EFFECTS_UNKNOWN
31323134
end

base/dict.jl

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -901,14 +901,31 @@ struct PersistentDict{K,V} <: AbstractDict{K,V}
901901
@noinline function KeyValue.set(::Type{PersistentDict{K, V}}, ::Nothing, key, val) where {K, V}
902902
new{K, V}(HAMT.HAMT{K, V}(key => val))
903903
end
904-
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key, val) where {K, V}
904+
@noinline @Base.assume_effects :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key, val) where {K, V}
905905
trie = dict.trie
906906
h = HAMT.HashState(key)
907907
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
908908
HAMT.insert!(found, present, trie, i, bi, hs, val)
909909
return new{K, V}(top)
910910
end
911-
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key) where {K, V}
911+
@noinline @Base.assume_effects :nothrow :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key::K, val::V) where {K, V}
912+
trie = dict.trie
913+
h = HAMT.HashState(key)
914+
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
915+
HAMT.insert!(found, present, trie, i, bi, hs, val)
916+
return new{K, V}(top)
917+
end
918+
@noinline @Base.assume_effects :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key) where {K, V}
919+
trie = dict.trie
920+
h = HAMT.HashState(key)
921+
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
922+
if found && present
923+
deleteat!(trie.data, i)
924+
HAMT.unset!(trie, bi)
925+
end
926+
return new{K, V}(top)
927+
end
928+
@noinline @Base.assume_effects :nothrow :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key::K) where {K, V}
912929
trie = dict.trie
913930
h = HAMT.HashState(key)
914931
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)

base/essentials.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,32 @@ macro _terminates_globally_meta()
253253
#=:noub=#false,
254254
#=:noub_if_noinbounds=#false))
255255
end
256+
# can be used in place of `@assume_effects :terminates_globally :notaskstate` (supposed to be used for bootstrapping)
257+
macro _terminates_globally_notaskstate_meta()
258+
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
259+
#=:consistent=#false,
260+
#=:effect_free=#false,
261+
#=:nothrow=#false,
262+
#=:terminates_globally=#true,
263+
#=:terminates_locally=#true,
264+
#=:notaskstate=#true,
265+
#=:inaccessiblememonly=#false,
266+
#=:noub=#false,
267+
#=:noub_if_noinbounds=#false))
268+
end
269+
# can be used in place of `@assume_effects :terminates_globally :noub` (supposed to be used for bootstrapping)
270+
macro _terminates_globally_noub_meta()
271+
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
272+
#=:consistent=#false,
273+
#=:effect_free=#false,
274+
#=:nothrow=#false,
275+
#=:terminates_globally=#true,
276+
#=:terminates_locally=#true,
277+
#=:notaskstate=#false,
278+
#=:inaccessiblememonly=#false,
279+
#=:noub=#true,
280+
#=:noub_if_noinbounds=#false))
281+
end
256282
# can be used in place of `@assume_effects :effect_free :terminates_locally` (supposed to be used for bootstrapping)
257283
macro _effect_free_terminates_locally_meta()
258284
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
@@ -279,6 +305,45 @@ macro _nothrow_noub_meta()
279305
#=:noub=#true,
280306
#=:noub_if_noinbounds=#false))
281307
end
308+
# can be used in place of `@assume_effects :nothrow` (supposed to be used for bootstrapping)
309+
macro _nothrow_meta()
310+
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
311+
#=:consistent=#false,
312+
#=:effect_free=#false,
313+
#=:nothrow=#true,
314+
#=:terminates_globally=#false,
315+
#=:terminates_locally=#false,
316+
#=:notaskstate=#false,
317+
#=:inaccessiblememonly=#false,
318+
#=:noub=#false,
319+
#=:noub_if_noinbounds=#false))
320+
end
321+
# can be used in place of `@assume_effects :nothrow` (supposed to be used for bootstrapping)
322+
macro _noub_meta()
323+
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
324+
#=:consistent=#false,
325+
#=:effect_free=#false,
326+
#=:nothrow=#false,
327+
#=:terminates_globally=#false,
328+
#=:terminates_locally=#false,
329+
#=:notaskstate=#false,
330+
#=:inaccessiblememonly=#false,
331+
#=:noub=#true,
332+
#=:noub_if_noinbounds=#false))
333+
end
334+
# can be used in place of `@assume_effects :notaskstate` (supposed to be used for bootstrapping)
335+
macro _notaskstate_meta()
336+
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
337+
#=:consistent=#false,
338+
#=:effect_free=#false,
339+
#=:nothrow=#false,
340+
#=:terminates_globally=#false,
341+
#=:terminates_locally=#false,
342+
#=:notaskstate=#true,
343+
#=:inaccessiblememonly=#false,
344+
#=:noub=#false,
345+
#=:noub_if_noinbounds=#false))
346+
end
282347
# can be used in place of `@assume_effects :noub_if_noinbounds` (supposed to be used for bootstrapping)
283348
macro _noub_if_noinbounds_meta()
284349
return _is_internal(__module__) && Expr(:meta, Expr(:purity,

base/genericmemory.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ isassigned(a::GenericMemoryRef) = memoryref_isassigned(a, :not_atomic, @_boundsc
7070

7171
## copy ##
7272
function unsafe_copyto!(dest::MemoryRef{T}, src::MemoryRef{T}, n) where {T}
73-
@_terminates_globally_meta
73+
@_terminates_globally_notaskstate_meta
7474
n == 0 && return dest
7575
@boundscheck GenericMemoryRef(dest, n), GenericMemoryRef(src, n)
7676
ccall(:jl_genericmemory_copyto, Cvoid, (Any, Ptr{Cvoid}, Any, Ptr{Cvoid}, Int), dest.mem, dest.ptr_or_offset, src.mem, src.ptr_or_offset, Int(n))

base/hamt.jl

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ struct HashState{K}
9292
end
9393
HashState(key) = HashState(key, objectid(key), 0, 0)
9494
# Reconstruct
95-
function HashState(other::HashState, key)
95+
Base.@assume_effects :terminates_locally function HashState(other::HashState, key)
9696
h = HashState(key)
9797
while h.depth !== other.depth
9898
h = next(h)
@@ -103,7 +103,8 @@ end
103103
function next(h::HashState)
104104
depth = h.depth + 1
105105
shift = h.shift + BITS_PER_LEVEL
106-
@assert h.shift <= MAX_SHIFT
106+
# Assert disabled for effect precision
107+
# @assert h.shift <= MAX_SHIFT
107108
if shift > MAX_SHIFT
108109
# Note we use `UInt(depth ÷ BITS_PER_LEVEL)` to seed the hash function
109110
# the hash docs, do we need to hash `UInt(depth ÷ BITS_PER_LEVEL)` first?
@@ -154,7 +155,7 @@ as the current `level`.
154155
If a copy function is provided `copyf` use the return `top` for the
155156
new persistent tree.
156157
"""
157-
@inline function path(trie::HAMT{K,V}, key, h::HashState, copy=false) where {K, V}
158+
@inline @Base.assume_effects :noub :terminates_locally function path(trie::HAMT{K,V}, key, h::HashState, copy=false) where {K, V}
158159
if copy
159160
trie = top = HAMT{K,V}(Base.copy(trie.data), trie.bitmap)
160161
else
@@ -172,6 +173,7 @@ new persistent tree.
172173
end
173174
if copy
174175
next = HAMT{K,V}(Base.copy(next.data), next.bitmap)
176+
# :noub because entry_index is guaranteed to be inbounds for trie.data
175177
@inbounds trie.data[i] = next
176178
end
177179
trie = next::HAMT{K,V}
@@ -187,7 +189,7 @@ end
187189
Internal function that given an obtained path, either set the value
188190
or grows the HAMT by inserting a new trie instead.
189191
"""
190-
@inline function insert!(found, present, trie::HAMT{K,V}, i, bi, h, val) where {K,V}
192+
@inline @Base.assume_effects :terminates_locally function insert!(found, present, trie::HAMT{K,V}, i, bi, h, val) where {K,V}
191193
if found # we found a slot, just set it to the new leaf
192194
# replace or insert
193195
if present # replace

test/compiler/irpasses.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,10 @@ function persistent_dict_elim_multiple()
15611561
return b[:a]
15621562
end
15631563
@test_broken fully_eliminated(persistent_dict_elim_multiple)
1564-
@test code_typed(persistent_dict_elim_multiple)[1][1].code[end] == Core.ReturnNode(1)
1564+
let code = code_typed(persistent_dict_elim_multiple)[1][1].code
1565+
@test count(x->isexpr(x, :invoke), code) == 0
1566+
@test code[end] == Core.ReturnNode(1)
1567+
end
15651568

15661569
function persistent_dict_elim_multiple_phi(c::Bool)
15671570
if c

0 commit comments

Comments
 (0)