Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Effects-tune PersitentDict #52954

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Effects-tune PersitentDict
To in particular allow elimination of dead PersistentDict constructions.
  • Loading branch information
Keno committed Jan 19, 2024
commit 1fef6cb8004eeefcc5f66729fc5732915cc533ee
9 changes: 8 additions & 1 deletion base/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,9 @@ See also [`copy!`](@ref Base.copy!), [`copyto!`](@ref), [`deepcopy`](@ref).
copy

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

function _growbeg!(a::Vector, delta::Integer)
@_noub_meta
delta = Int(delta)
delta == 0 && return # avoid attempting to index off the end
delta >= 0 || throw(ArgumentError("grow requires delta >= 0"))
Expand Down Expand Up @@ -1077,6 +1081,7 @@ function _growbeg!(a::Vector, delta::Integer)
end

function _growend!(a::Vector, delta::Integer)
@_noub_meta
delta = Int(delta)
delta >= 0 || throw(ArgumentError("grow requires delta >= 0"))
ref = a.ref
Expand Down Expand Up @@ -1113,6 +1118,7 @@ function _growend!(a::Vector, delta::Integer)
end

function _growat!(a::Vector, i::Integer, delta::Integer)
@_terminates_globally_noub_meta
delta = Int(delta)
i = Int(i)
i == 1 && return _growbeg!(a, delta)
Expand Down Expand Up @@ -1715,10 +1721,11 @@ julia> insert!(Any[1:6;], 3, "here")
```
"""
function insert!(a::Array{T,1}, i::Integer, item) where T
@_noub_meta
# Throw convert error before changing the shape of the array
_item = item isa T ? item : convert(T, item)::T
_growat!(a, i, 1)
# _growat! already did bound check
# :noub, because _growat! already did bound check
@inbounds a[i] = _item
return a
end
Expand Down
2 changes: 2 additions & 0 deletions base/compiler/tfuncs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3128,6 +3128,8 @@ function foreigncall_effects(@specialize(abstract_eval), e::Expr)
if name === :jl_alloc_genericmemory
nothrow = new_genericmemory_nothrow(abstract_eval, args)
return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow)
elseif name === :jl_genericmemory_copy_slice
return Effects(EFFECTS_TOTAL; consistent=CONSISTENT_IF_NOTRETURNED, nothrow=false)
end
return EFFECTS_UNKNOWN
end
Expand Down
21 changes: 19 additions & 2 deletions base/dict.jl
Original file line number Diff line number Diff line change
Expand Up @@ -901,14 +901,31 @@ struct PersistentDict{K,V} <: AbstractDict{K,V}
@noinline function KeyValue.set(::Type{PersistentDict{K, V}}, ::Nothing, key, val) where {K, V}
new{K, V}(HAMT.HAMT{K, V}(key => val))
end
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key, val) where {K, V}
@noinline @Base.assume_effects :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key, val) where {K, V}
trie = dict.trie
h = HAMT.HashState(key)
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
HAMT.insert!(found, present, trie, i, bi, hs, val)
return new{K, V}(top)
end
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key) where {K, V}
@noinline @Base.assume_effects :nothrow :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key::K, val::V) where {K, V}
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further investigation, it appears that modifying this single line is enough to remove the call to the dead PersistentDict constructor. But, this seems to stem from an inference bug, where we're deleting statements without actually proving terminates.

julia> @noinline Base.@assume_effects :effect_free :nothrow function foo(n)
           s = 0
           while true
               if n - rand(1:10) > 0
                   s += 1
               else
                   break
               end
           end
           return s
       end^C

julia> code_typed((Int,)) do n
           foo(n)
           nothing
       end
1-element Vector{Any}:
 CodeInfo(
1return Main.nothing
) => Nothing

So, I'd like to address this bug first and then revisit this PR.

trie = dict.trie
h = HAMT.HashState(key)
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
HAMT.insert!(found, present, trie, i, bi, hs, val)
return new{K, V}(top)
end
@noinline @Base.assume_effects :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key) where {K, V}
trie = dict.trie
h = HAMT.HashState(key)
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
if found && present
deleteat!(trie.data, i)
HAMT.unset!(trie, bi)
end
return new{K, V}(top)
end
@noinline @Base.assume_effects :nothrow :effect_free function KeyValue.set(dict::PersistentDict{K, V}, key::K) where {K, V}
trie = dict.trie
h = HAMT.HashState(key)
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
Expand Down
65 changes: 65 additions & 0 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,32 @@ macro _terminates_globally_meta()
#=:noub=#false,
#=:noub_if_noinbounds=#false))
end
# can be used in place of `@assume_effects :terminates_globally :notaskstate` (supposed to be used for bootstrapping)
macro _terminates_globally_notaskstate_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
#=:consistent=#false,
#=:effect_free=#false,
#=:nothrow=#false,
#=:terminates_globally=#true,
#=:terminates_locally=#true,
#=:notaskstate=#true,
#=:inaccessiblememonly=#false,
#=:noub=#false,
#=:noub_if_noinbounds=#false))
end
# can be used in place of `@assume_effects :terminates_globally :noub` (supposed to be used for bootstrapping)
macro _terminates_globally_noub_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
#=:consistent=#false,
#=:effect_free=#false,
#=:nothrow=#false,
#=:terminates_globally=#true,
#=:terminates_locally=#true,
#=:notaskstate=#false,
#=:inaccessiblememonly=#false,
#=:noub=#true,
#=:noub_if_noinbounds=#false))
end
# can be used in place of `@assume_effects :effect_free :terminates_locally` (supposed to be used for bootstrapping)
macro _effect_free_terminates_locally_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
Expand All @@ -279,6 +305,45 @@ macro _nothrow_noub_meta()
#=:noub=#true,
#=:noub_if_noinbounds=#false))
end
# can be used in place of `@assume_effects :nothrow` (supposed to be used for bootstrapping)
macro _nothrow_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
#=:consistent=#false,
#=:effect_free=#false,
#=:nothrow=#true,
#=:terminates_globally=#false,
#=:terminates_locally=#false,
#=:notaskstate=#false,
#=:inaccessiblememonly=#false,
#=:noub=#false,
#=:noub_if_noinbounds=#false))
end
# can be used in place of `@assume_effects :nothrow` (supposed to be used for bootstrapping)
macro _noub_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
#=:consistent=#false,
#=:effect_free=#false,
#=:nothrow=#false,
#=:terminates_globally=#false,
#=:terminates_locally=#false,
#=:notaskstate=#false,
#=:inaccessiblememonly=#false,
#=:noub=#true,
#=:noub_if_noinbounds=#false))
end
# can be used in place of `@assume_effects :notaskstate` (supposed to be used for bootstrapping)
macro _notaskstate_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
#=:consistent=#false,
#=:effect_free=#false,
#=:nothrow=#false,
#=:terminates_globally=#false,
#=:terminates_locally=#false,
#=:notaskstate=#true,
#=:inaccessiblememonly=#false,
#=:noub=#false,
#=:noub_if_noinbounds=#false))
end
# can be used in place of `@assume_effects :noub_if_noinbounds` (supposed to be used for bootstrapping)
macro _noub_if_noinbounds_meta()
return _is_internal(__module__) && Expr(:meta, Expr(:purity,
Expand Down
2 changes: 1 addition & 1 deletion base/genericmemory.jl
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ isassigned(a::GenericMemoryRef) = memoryref_isassigned(a, :not_atomic, @_boundsc

## copy ##
function unsafe_copyto!(dest::MemoryRef{T}, src::MemoryRef{T}, n) where {T}
@_terminates_globally_meta
@_terminates_globally_notaskstate_meta
n == 0 && return dest
@boundscheck GenericMemoryRef(dest, n), GenericMemoryRef(src, n)
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))
Expand Down
10 changes: 6 additions & 4 deletions base/hamt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ struct HashState{K}
end
HashState(key) = HashState(key, objectid(key), 0, 0)
# Reconstruct
function HashState(other::HashState, key)
Base.@assume_effects :terminates_locally function HashState(other::HashState, key)
h = HashState(key)
while h.depth !== other.depth
h = next(h)
Expand All @@ -103,7 +103,8 @@ end
function next(h::HashState)
depth = h.depth + 1
shift = h.shift + BITS_PER_LEVEL
@assert h.shift <= MAX_SHIFT
# Assert disabled for effect precision
# @assert h.shift <= MAX_SHIFT
if shift > MAX_SHIFT
# Note we use `UInt(depth ÷ BITS_PER_LEVEL)` to seed the hash function
# the hash docs, do we need to hash `UInt(depth ÷ BITS_PER_LEVEL)` first?
Expand Down Expand Up @@ -154,7 +155,7 @@ as the current `level`.
If a copy function is provided `copyf` use the return `top` for the
new persistent tree.
"""
@inline function path(trie::HAMT{K,V}, key, h::HashState, copy=false) where {K, V}
@inline @Base.assume_effects :noub :terminates_locally function path(trie::HAMT{K,V}, key, h::HashState, copy=false) where {K, V}
if copy
trie = top = HAMT{K,V}(Base.copy(trie.data), trie.bitmap)
else
Expand All @@ -172,6 +173,7 @@ new persistent tree.
end
if copy
next = HAMT{K,V}(Base.copy(next.data), next.bitmap)
# :noub because entry_index is guaranteed to be inbounds for trie.data
@inbounds trie.data[i] = next
end
trie = next::HAMT{K,V}
Expand All @@ -187,7 +189,7 @@ end
Internal function that given an obtained path, either set the value
or grows the HAMT by inserting a new trie instead.
"""
@inline function insert!(found, present, trie::HAMT{K,V}, i, bi, h, val) where {K,V}
@inline @Base.assume_effects :terminates_locally function insert!(found, present, trie::HAMT{K,V}, i, bi, h, val) where {K,V}
if found # we found a slot, just set it to the new leaf
# replace or insert
if present # replace
Expand Down
5 changes: 4 additions & 1 deletion test/compiler/irpasses.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1561,7 +1561,10 @@ function persistent_dict_elim_multiple()
return b[:a]
end
@test_broken fully_eliminated(persistent_dict_elim_multiple)
@test code_typed(persistent_dict_elim_multiple)[1][1].code[end] == Core.ReturnNode(1)
let code = code_typed(persistent_dict_elim_multiple)[1][1].code
@test count(x->isexpr(x, :invoke), code) == 0
@test code[end] == Core.ReturnNode(1)
end

function persistent_dict_elim_multiple_phi(c::Bool)
if c
Expand Down