Skip to content

Commit f5e821f

Browse files
committed
RFC: Compiler support for optimizing PersistentDict
This is part of the work to address #51352 by attempting to allow the compiler to perform SRAO on persistent data structures like `PersistentDict` as if they were regular immutable data structures. These sorts of data structures have very complicated internals (with lots of mutation, memory sharing, etc.), but a relatively simple interface. As such, it is unlikely that our compiler will have sufficient power to optimize this interface by analyzing the implementation. We thus need to come up with some other mechanism that gives the compiler license to perform the requisite optimization. One way would be to just hardcode `PersistentDict` into the compiler, optimizing it like any of the other builtin datatypes. However, this is of course very unsatisfying. At the other end of the spectrum would be something like a generic rewrite rule system (e-graphs anyone?) that would let the PersistentDict implementation declare its interface to the compiler and the compiler would use this for optimization (in a perfect world, the actual rewrite would then be checked using some sort of formal methods). I think that would be interesting, but we're very far from even being able to design something like that (at least in Base - experiments with external AbstractInterpreters in this direction are encouraged). This PR tries to come up with a reasonable middle ground, where the compiler gets some knowledge of the protocol hardcoded without having to know about the implementation details of the data structure. The basic ideas is that `Core` provides some magic generic functions that implementations can extend. Semantically, they are not special. They dispatch as usual, and implementations are expected to work properly even in the absence of any compiler optimizations. However, the compiler is semantically permitted to perform structural optimization using these magic generic functions. In the concrete case, this PR introduces the `KeyValue` interface which consists of two generic functions, `get` and `set`. The core optimization is that the compiler is allowed to rewrite any occurrence of `get(set(x, k, v), k)` into `v` without additional legality checks. In particular, the compiler performs no type checks, conversions, etc. The higher level implementation code is expected to do all that. This approach closely matches the general direction we've been taking in external AbstractInterpreters for embedding additional semantics and optimization opportunities into Julia code (although we generally use methods there, rather than full generic functions), so I think we have some evidence that this sort of approach works reasonably well. Nevertheless, this is certainly an experiment and the interface is explicitly declared unstable. ## Current Status This is fully working and implemented, but the optimization currently bails on anything but the simplest cases. Filling all those cases in is not particularly hard, but should be done along with a more invasive refactoring of SROA, so we should figure out the general direction here first and then we can finish all that up in a follow-up cleanup. ## Obligatory benchmark Before: ``` julia> using BenchmarkTools julia> function foo() a = Base.PersistentDict(:a => 1) return a[:a] end foo (generic function with 1 method) julia> @benchmark foo() BenchmarkTools.Trial: 10000 samples with 993 evaluations. Range (min … max): 32.940 ns … 28.754 μs ┊ GC (min … max): 0.00% … 99.76% Time (median): 49.647 ns ┊ GC (median): 0.00% Time (mean ± σ): 57.519 ns ± 333.275 ns ┊ GC (mean ± σ): 10.81% ± 2.22% ▃█▅ ▁▃▅▅▃▁ ▁▃▂ ▂ ▁▂▄▃▅▇███▇▃▁▂▁▁▁▁▁▁▁▁▂▂▅██████▅▂▁▁▁▁▁▁▁▁▁▁▂▃▃▇███▇▆███▆▄▃▃▂▂ ▃ 32.9 ns Histogram: frequency by time 68.6 ns < Memory estimate: 128 bytes, allocs estimate: 4. julia> @code_typed foo() CodeInfo( 1 ─ %1 = invoke Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}}(Base.HashArrayMappedTries.undef::UndefInitializer, 1::Int64)::Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ %2 = %new(Base.HashArrayMappedTries.HAMT{Symbol, Int64}, %1, 0x00000000)::Base.HashArrayMappedTries.HAMT{Symbol, Int64} │ %3 = %new(Base.HashArrayMappedTries.Leaf{Symbol, Int64}, :a, 1)::Base.HashArrayMappedTries.Leaf{Symbol, Int64} │ %4 = Base.getfield(%2, :data)::Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ %5 = $(Expr(:boundscheck, true))::Bool └── goto #5 if not %5 2 ─ %7 = Base.sub_int(1, 1)::Int64 │ %8 = Base.bitcast(UInt64, %7)::UInt64 │ %9 = Base.getfield(%4, :size)::Tuple{Int64} │ %10 = $(Expr(:boundscheck, true))::Bool │ %11 = Base.getfield(%9, 1, %10)::Int64 │ %12 = Base.bitcast(UInt64, %11)::UInt64 │ %13 = Base.ult_int(%8, %12)::Bool └── goto #4 if not %13 3 ─ goto #5 4 ─ %16 = Core.tuple(1)::Tuple{Int64} │ invoke Base.throw_boundserror(%4::Vector{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}}, %16::Tuple{Int64})::Union{} └── unreachable 5 ┄ %19 = Base.getfield(%4, :ref)::MemoryRef{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ %20 = Base.memoryref(%19, 1, false)::MemoryRef{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} │ Base.memoryrefset!(%20, %3, :not_atomic, false)::MemoryRef{Union{Base.HashArrayMappedTries.HAMT{Symbol, Int64}, Base.HashArrayMappedTries.Leaf{Symbol, Int64}}} └── goto #6 6 ─ %23 = Base.getfield(%2, :bitmap)::UInt32 │ %24 = Base.or_int(%23, 0x00010000)::UInt32 │ Base.setfield!(%2, :bitmap, %24)::UInt32 └── goto #7 7 ─ %27 = %new(Base.PersistentDict{Symbol, Int64}, %2)::Base.PersistentDict{Symbol, Int64} └── goto #8 8 ─ %29 = invoke Base.getindex(%27::Base.PersistentDict{Symbol, Int64}, 🅰️:Symbol)::Int64 └── return %29 ``` After: ``` julia> using BenchmarkTools julia> function foo() a = Base.PersistentDict(:a => 1) return a[:a] end foo (generic function with 1 method) julia> @benchmark foo() BenchmarkTools.Trial: 10000 samples with 1000 evaluations. Range (min … max): 2.459 ns … 11.320 ns ┊ GC (min … max): 0.00% … 0.00% Time (median): 2.460 ns ┊ GC (median): 0.00% Time (mean ± σ): 2.469 ns ± 0.183 ns ┊ GC (mean ± σ): 0.00% ± 0.00% ▂ █ ▁ █ ▂ █▁▁▁▁█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█▁▁▁▁█ █ 2.46 ns Histogram: log(frequency) by time 2.47 ns < Memory estimate: 0 bytes, allocs estimate: 0. julia> @code_typed foo() CodeInfo( 1 ─ return 1 ```
1 parent 3a6c418 commit f5e821f

File tree

7 files changed

+222
-61
lines changed

7 files changed

+222
-61
lines changed

base/boot.jl

+2
Original file line numberDiff line numberDiff line change
@@ -959,4 +959,6 @@ arraysize(a::Array) = a.size
959959
arraysize(a::Array, i::Int) = sle_int(i, nfields(a.size)) ? getfield(a.size, i) : 1
960960
export arrayref, arrayset, arraysize, const_arrayref
961961

962+
include(Core, "optimized_generics.jl")
963+
962964
ccall(:jl_set_istopmod, Cvoid, (Any, Bool), Core, true)

base/compiler/ssair/passes.jl

+86-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ function is_known_call(@nospecialize(x), @nospecialize(func), ir::Union{IRCode,I
66
return singleton_type(ft) === func
77
end
88

9+
function is_known_invoke_or_call(@nospecialize(x), @nospecialize(func), ir::Union{IRCode,IncrementalCompact})
10+
isinvoke = isexpr(x, :invoke)
11+
(isinvoke || isexpr(x, :call)) || return false
12+
ft = argextype(x.args[isinvoke ? 2 : 1], ir)
13+
return singleton_type(ft) === func
14+
end
15+
916
struct SSAUse
1017
kind::Symbol
1118
idx::Int
@@ -817,6 +824,81 @@ function lift_svec_ref!(compact::IncrementalCompact, idx::Int, stmt::Expr)
817824
return
818825
end
819826

827+
function lift_leaves_keyvalue(compact::IncrementalCompact, @nospecialize(key),
828+
leaves::Vector{Any}, 𝕃ₒ::AbstractLattice)
829+
# For every leaf, the lifted value
830+
lifted_leaves = LiftedLeaves()
831+
maybe_undef = false
832+
for i = 1:length(leaves)
833+
leaf = leaves[i]
834+
cache_key = leaf
835+
if isa(leaf, AnySSAValue)
836+
(def, leaf) = walk_to_def(compact, leaf)
837+
if is_known_invoke_or_call(def, Core.OptimizedGenerics.KeyValue.set, compact)
838+
@assert isexpr(def, :invoke)
839+
if length(def.args) in (5, 6)
840+
collection = def.args[end-2]
841+
set_key = def.args[end-1]
842+
set_val_idx = length(def.args)
843+
elseif length(def.args) == 4
844+
collection = def.args[end-1]
845+
# Key is deleted
846+
# TODO: Model this
847+
return nothing
848+
elseif length(def.args) == 3
849+
collection = def.args[end]
850+
# The whole collection is deleted
851+
# TODO: Model this
852+
return nothing
853+
else
854+
return nothing
855+
end
856+
if set_key === key || (egal_tfunc(𝕃ₒ, argextype(key, compact), argextype(set_key, compact)) == Const(true))
857+
lift_arg!(compact, leaf, cache_key, def, set_val_idx, lifted_leaves)
858+
continue
859+
end
860+
# TODO: Continue walking the chain
861+
return nothing
862+
end
863+
end
864+
return nothing
865+
end
866+
return lifted_leaves, maybe_undef
867+
end
868+
869+
function lift_keyvalue_get!(compact::IncrementalCompact, idx::Int, stmt::Expr, 𝕃ₒ::AbstractLattice)
870+
# TODO: Support variants with callbacks
871+
#length(stmt.args) == 4 || return
872+
873+
collection = stmt.args[end-1]
874+
key = stmt.args[end]
875+
876+
leaves, visited_philikes = collect_leaves(compact, collection, Any, 𝕃ₒ, phi_or_ifelse_predecessors)
877+
isempty(leaves) && return
878+
879+
lifted_result = lift_leaves_keyvalue(compact, key, leaves, 𝕃ₒ)
880+
lifted_result === nothing && return
881+
lifted_leaves, any_undef = lifted_result
882+
883+
result_t = Union{}
884+
for v in values(lifted_leaves)
885+
v === nothing && return
886+
result_t = tmerge(𝕃ₒ, result_t, argextype(v.val, compact))
887+
end
888+
889+
lifted_val = perform_lifting!(compact,
890+
visited_philikes, key, result_t, lifted_leaves, collection, nothing)
891+
892+
compact[idx] = lifted_val === nothing ? nothing : lifted_val.val
893+
if lifted_val !== nothing
894+
if !(𝕃ₒ, compact[SSAValue(idx)][:type], result_t)
895+
compact[SSAValue(idx)][:flag] |= IR_FLAG_REFINED
896+
end
897+
end
898+
899+
return
900+
end
901+
820902
# TODO: We could do the whole lifing machinery here, but really all
821903
# we want to do is clean this up when it got inserted by inlining,
822904
# which always targets simple `svec` call or `_compute_sparams`,
@@ -1002,7 +1084,7 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing)
10021084
for ((_, idx), stmt) in compact
10031085
# check whether this statement is `getfield` / `setfield!` (or other "interesting" statement)
10041086
isa(stmt, Expr) || continue
1005-
is_setfield = is_isdefined = is_finalizer = false
1087+
is_setfield = is_isdefined = is_finalizer = is_keyvalue_get = false
10061088
field_ordering = :unspecified
10071089
if is_known_call(stmt, setfield!, compact)
10081090
4 <= length(stmt.args) <= 5 || continue
@@ -1092,6 +1174,9 @@ function sroa_pass!(ir::IRCode, inlining::Union{Nothing,InliningState}=nothing)
10921174
lift_comparison!(isa, compact, idx, stmt, 𝕃ₒ)
10931175
elseif is_known_call(stmt, Core.ifelse, compact)
10941176
fold_ifelse!(compact, idx, stmt)
1177+
elseif is_known_invoke_or_call(stmt, Core.OptimizedGenerics.KeyValue.get, compact)
1178+
4 <= length(stmt.args) <= 6 || continue
1179+
lift_keyvalue_get!(compact, idx, stmt, 𝕃ₒ)
10951180
elseif isexpr(stmt, :new)
10961181
refine_new_effects!(𝕃ₒ, compact, idx, stmt)
10971182
end

base/dict.jl

+59-56
Original file line numberDiff line numberDiff line change
@@ -889,10 +889,35 @@ _similar_for(c::AbstractDict, ::Type{T}, itr, isz, len) where {T} =
889889

890890
include("hamt.jl")
891891
using .HashArrayMappedTries
892+
using Core.OptimizedGenerics: KeyValue
892893
const HAMT = HashArrayMappedTries
893894

894895
struct PersistentDict{K,V} <: AbstractDict{K,V}
895896
trie::HAMT.HAMT{K,V}
897+
# Serves as a marker for an empty initialization
898+
@noinline function KeyValue.set(::Type{PersistentDict{K, V}}) where {K, V}
899+
new{K, V}(HAMT.HAMT{K,V}())
900+
end
901+
@noinline function KeyValue.set(::Type{PersistentDict{K, V}}, ::Nothing, key, val) where {K, V}
902+
new{K, V}(HAMT.HAMT{K, V}(key, val))
903+
end
904+
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key, val) where {K, V}
905+
trie = dict.trie
906+
h = hash(key)
907+
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
908+
HAMT.insert!(found, present, trie, i, bi, hs, val)
909+
return new{K, V}(top)
910+
end
911+
@noinline function KeyValue.set(dict::PersistentDict{K, V}, key) where {K, V}
912+
trie = dict.trie
913+
h = hash(key)
914+
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
915+
if found && present
916+
deleteat!(trie.data, i)
917+
HAMT.unset!(trie, bi)
918+
end
919+
return new{K, V}(top)
920+
end
896921
end
897922

898923
"""
@@ -923,19 +948,27 @@ Base.PersistentDict{Symbol, Int64} with 1 entry:
923948
"""
924949
PersistentDict
925950

926-
PersistentDict{K,V}() where {K,V} = PersistentDict(HAMT.HAMT{K,V}())
927-
PersistentDict{K,V}(KV::Pair) where {K,V} = PersistentDict(HAMT.HAMT{K,V}(KV...))
928-
PersistentDict(KV::Pair{K,V}) where {K,V} = PersistentDict(HAMT.HAMT{K,V}(KV...))
951+
PersistentDict{K,V}() where {K, V} = KeyValue.set(PersistentDict{K,V})
952+
function PersistentDict{K,V}(KV::Pair) where {K,V}
953+
KeyValue.set(
954+
PersistentDict{K, V},
955+
nothing,
956+
KV...)
957+
end
958+
function PersistentDict(KV::Pair{K,V}) where {K,V}
959+
KeyValue.set(
960+
PersistentDict{K, V},
961+
nothing,
962+
KV...)
963+
end
929964
PersistentDict(dict::PersistentDict, pair::Pair) = PersistentDict(dict, pair...)
930965
PersistentDict{K,V}(dict::PersistentDict{K,V}, pair::Pair) where {K,V} = PersistentDict(dict, pair...)
966+
967+
931968
function PersistentDict(dict::PersistentDict{K,V}, key, val) where {K,V}
932969
key = convert(K, key)
933970
val = convert(V, val)
934-
trie = dict.trie
935-
h = hash(key)
936-
found, present, trie, i, bi, top, hs = HAMT.path(trie, key, h, #=persistent=# true)
937-
HAMT.insert!(found, present, trie, i, bi, hs, val)
938-
return PersistentDict(top)
971+
return KeyValue.set(dict, key, val)
939972
end
940973

941974
function PersistentDict(kv::Pair, rest::Pair...)
@@ -950,84 +983,54 @@ end
950983
eltype(::PersistentDict{K,V}) where {K,V} = Pair{K,V}
951984

952985
function in(key_val::Pair{K,V}, dict::PersistentDict{K,V}, valcmp=(==)) where {K,V}
953-
trie = dict.trie
954-
if HAMT.islevel_empty(trie)
955-
return false
956-
end
957-
958986
key, val = key_val
959-
960-
h = hash(key)
961-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
962-
if found && present
963-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
964-
return valcmp(val, leaf.val) && return true
965-
end
966-
return false
987+
return KeyValue.get(found->valcmp(val, found), ()->false, dict, key)
967988
end
968989

969990
function haskey(dict::PersistentDict{K}, key::K) where K
970-
trie = dict.trie
971-
h = hash(key)
972-
found, present, _, _, _, _, _ = HAMT.path(trie, key, h)
973-
return found && present
991+
return KeyValue.get(_->true, ()->false, dict, key)
974992
end
975993

976994
function getindex(dict::PersistentDict{K,V}, key::K) where {K,V}
977-
trie = dict.trie
978-
if HAMT.islevel_empty(trie)
979-
throw(KeyError(key))
995+
return KeyValue.get(dict, key) do
996+
return throw(KeyError(key))
980997
end
981-
h = hash(key)
982-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
983-
if found && present
984-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
985-
return leaf.val
986-
end
987-
throw(KeyError(key))
988998
end
989999

9901000
function get(dict::PersistentDict{K,V}, key::K, default) where {K,V}
991-
trie = dict.trie
992-
if HAMT.islevel_empty(trie)
1001+
return KeyValue.get(dict, key) do
9931002
return default
9941003
end
995-
h = hash(key)
996-
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
997-
if found && present
998-
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
999-
return leaf.val
1000-
end
1001-
return default
10021004
end
10031005

1004-
function get(default::Callable, dict::PersistentDict{K,V}, key::K) where {K,V}
1006+
@noinline function KeyValue.get(transform, default, dict::PersistentDict{K, V}, key) where {K, V}
10051007
trie = dict.trie
10061008
if HAMT.islevel_empty(trie)
1007-
return default
1009+
return default()
10081010
end
10091011
h = hash(key)
10101012
found, present, trie, i, _, _, _ = HAMT.path(trie, key, h)
10111013
if found && present
10121014
leaf = @inbounds trie.data[i]::HAMT.Leaf{K,V}
1013-
return leaf.val
1015+
return transform(leaf.val)
10141016
end
10151017
return default()
10161018
end
10171019

1018-
iterate(dict::PersistentDict, state=nothing) = HAMT.iterate(dict.trie, state)
1020+
@noinline function KeyValue.get(default, dict::PersistentDict, key)
1021+
KeyValue.get(identity, default, dict, key)
1022+
end
1023+
1024+
function get(default::Callable, dict::PersistentDict{K,V}, key::K) where {K,V}
1025+
return KeyValue.get(default, dict, key)
1026+
end
10191027

10201028
function delete(dict::PersistentDict{K}, key::K) where K
1021-
trie = dict.trie
1022-
h = hash(key)
1023-
found, present, trie, i, bi, top, _ = HAMT.path(trie, key, h, #=persistent=# true)
1024-
if found && present
1025-
deleteat!(trie.data, i)
1026-
HAMT.unset!(trie, bi)
1027-
end
1028-
return PersistentDict(top)
1029+
return KeyValue.set(dict, key)
10291030
end
10301031

1032+
iterate(dict::PersistentDict, state=nothing) = HAMT.iterate(dict.trie, state)
1033+
10311034
length(dict::PersistentDict) = HAMT.length(dict.trie)
10321035
isempty(dict::PersistentDict) = HAMT.isempty(dict.trie)
10331036
empty(::PersistentDict, ::Type{K}, ::Type{V}) where {K, V} = PersistentDict{K, V}()

base/hamt.jl

+9-3
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,18 @@ mutable struct HAMT{K, V}
6464
bitmap::BITMAP
6565
end
6666
HAMT{K, V}() where {K, V} = HAMT(Vector{Union{Leaf{K, V}, HAMT{K, V}}}(undef, 0), zero(BITMAP))
67-
function HAMT{K,V}(k::K, v) where {K, V}
68-
v = convert(V, v)
67+
68+
@Base.assume_effects :nothrow function init_hamt(K, V, k, v)
6969
# For a single element we can't have a hash-collision
7070
trie = HAMT(Vector{Union{Leaf{K, V}, HAMT{K, V}}}(undef, 1), zero(BITMAP))
71-
trie.data[1] = Leaf{K,V}(k,v)
71+
@inbounds trie.data[1] = Leaf{K,V}(k,v)
72+
return trie
73+
end
74+
75+
function HAMT{K,V}(k::K, v) where {K, V}
76+
v = convert(V, v)
7277
bi = BitmapIndex(HashState(k))
78+
trie = init_hamt(K, V, k, v)
7379
set!(trie, bi)
7480
return trie
7581
end

base/optimized_generics.jl

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
module OptimizedGenerics
4+
5+
# This file defines interfaces that are recognized and optimized by the compiler
6+
# They are intended to be used by data structure implementations that wish to
7+
# opt into some level of compiler optimizations. These interfaces are
8+
# EXPERIMENTAL and currently intended for use by Base only. They are subject
9+
# to change or removal without notice. It is undefined behavior to add methods
10+
# to these generics that do not conform to the specified interface.
11+
#
12+
# The intended way to use these generics is that data structures will provide
13+
# appropriate implementations for a generic. In the absence of compiler
14+
# optimizations, these behave like regular methods. However, the compiler is
15+
# semantically allowed to perform certain structural optimizations on
16+
# appropriate combinations of these intrinsics without proving correctness.
17+
18+
# Compiler-recognized generics for immutable key-value stores (dicts, etc.)
19+
"""
20+
module KeyValue
21+
22+
Implements a key-value like interface where the compiler has liberty to perform
23+
the following transformations. The core optimization semantically allowed for
24+
the compiler is:
25+
26+
get(set(x, key, val), key) -> val
27+
28+
where the compiler will recursively look through `x`. Keys are compared by
29+
egality.
30+
31+
Implementations must observe the following constraints:
32+
33+
1. It is undefined behavior for `get` not to return the exact (by egality) val
34+
stored for a given `key`.
35+
"""
36+
module KeyValue
37+
"""
38+
set(collection, [key [, val]])
39+
set(T, collection, key, val)
40+
41+
Set the `key` in `collection` to `val`. If `val` is omitted, deletes the
42+
value from the collection. If `key` is omitted as well, deletes all elements
43+
of the collection.
44+
"""
45+
function set end
46+
47+
"""
48+
get([[transform,] default,] collection, key, val)
49+
50+
Retrieve the value corresponding to `key` in `collection`. Optionally takes
51+
a `default` callback that is executed if `key` is not found and a `transform`
52+
callback that is executed only if the value is found (but not on the return)
53+
value of `default`.
54+
"""
55+
function get end
56+
end
57+
58+
end

base/reflection.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ function objectid(x)
728728
return _objectid(x)
729729
end
730730
function _foldable_objectid(@nospecialize(x))
731-
@_foldable_meta
731+
@_total_meta
732732
_objectid(x)
733733
end
734734
_objectid(@nospecialize(x)) = ccall(:jl_object_id, UInt, (Any,), x)

test/compiler/irpasses.jl

+7
Original file line numberDiff line numberDiff line change
@@ -1568,3 +1568,10 @@ let m = Meta.@lower 1 + 1
15681568

15691569
Core.Compiler.verify_ir(ir)
15701570
end
1571+
1572+
# Test support for Core.OptimizedGenerics.KeyValue protocol
1573+
function persistent_dict_elim()
1574+
a = Base.PersistentDict(:a => 1)
1575+
return a[:a]
1576+
end
1577+
@test fully_eliminated(persistent_dict_elim)

0 commit comments

Comments
 (0)