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

avoid invalidations when the overlap is ambiguous #35877

Closed
wants to merge 3 commits into from

Conversation

JeffBezanson
Copy link
Sponsor Member

In these cases, the new method would not be called because the call would be an ambiguity error instead.

At least, that's what I'm trying to do. I'm not sure it is complete and correct, but so far it does reduce the output from jl_debug_method_invalidation when loading FixedPointNumbers from ~600 lines to ~200.

@JeffBezanson JeffBezanson added the compiler:latency Compiler latency label May 13, 2020
src/gf.c Outdated
for (size_t i = 0; i < jl_array_len(m->ambig); i++) {
jl_typemap_entry_t *mambig = (jl_typemap_entry_t*)jl_array_ptr_ref(m->ambig, i);
if (mambig->min_world <= jl_world_counter && jl_world_counter <= mambig->max_world)
if (jl_subtype((jl_value_t*)types, (jl_value_t*)mambig->sig))
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

I'm not at all certain, but do you have to wait until this condition has been satisfied more than once?

@vtjnash
Copy link
Sponsor Member

vtjnash commented May 14, 2020

IIUC, the failing test (in core) is indeed the counterfactual to this idea, unfortunately :/

src/gf.c Outdated
Comment on lines 1700 to 1705
jl_methtable_t *mt = jl_first_argument_datatype(types)->name->mt;
struct jl_typemap_assoc search = {types, world, NULL, 0, ~(size_t)0};
jl_typemap_entry_t *entry = jl_typemap_assoc_by_type(mt->defs, &search, 0, 1);
if (!entry)
return 0;
return jl_is_call_ambiguous(types, entry->func.method);
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

It seems like this code should be similar to jl_gf_invoke_lookup, but with one of the return values swapped:

Suggested change
jl_methtable_t *mt = jl_first_argument_datatype(types)->name->mt;
struct jl_typemap_assoc search = {types, world, NULL, 0, ~(size_t)0};
jl_typemap_entry_t *entry = jl_typemap_assoc_by_type(mt->defs, &search, 0, 1);
if (!entry)
return 0;
return jl_is_call_ambiguous(types, entry->func.method);
jl_methtable_t *mt = jl_method_table_for(types); // TODO: this should probably just be an argument to this function
assert((jl_value_t*)mt != jl_nothing);
struct jl_typemap_assoc search = {(jl_value_t*)types, world, NULL, 0, ~(size_t)0};
jl_typemap_entry_t *entry = jl_typemap_assoc_by_type(mt->defs, &search, /*offs*/0, /*subtype*/1);
if (entry == NULL)
return 0; // we added a new definition to consider
jl_typemap_entry_t *m = jl_typemap_morespecific_by_type(entry, (jl_value_t*)types, NULL, world);
if (m == NULL) // TODO: this isn't quite right, since we just ask if the entry dominates over all subtypes of `types`, but wanted to ask the inverse question
return 1; // a new ambiguity doesn't change the result of method lookup
return 0; // existing and/or method already covered this intersection (TODO: which is it?)

Though I think the ml_matches_visitor has the more complete and correct version.

In these cases, the new method would not be called because the
call would be an ambiguity error instead.
@JeffBezanson
Copy link
Sponsor Member Author

Ok, this should hopefully work now. It passes the ambiguity-related tests in #35855.

@JeffBezanson JeffBezanson changed the title WIP: avoid invalidations when the overlap is ambiguous avoid invalidations when the overlap is ambiguous May 15, 2020
@timholy
Copy link
Sponsor Member

timholy commented May 15, 2020

This helps a lot with FixedPointNumbers! It also fixes 2/56 cases in #35855.

Being greedy, I should ask whether there anything that can be done about this?

julia> using SnoopCompile

julia> abstract type SomeType end

julia> _invs = @snoopr Base.convert(::Type{T}, x) where T<:SomeType = nothing;

julia> length(unique(filter(x->isa(x, Core.MethodInstance), _invs)))
797

For example, intersection Tuple{typeof(convert),Type{T} where T<:Symbol,Any} triggers invalidation of MethodInstance for setindex!(::Array{_A,1} where _A<:Symbol, ::Any, ::Int64). Since Symbol is a concrete type that is not a subtype of SomeType, it seems that in theory this should be avoidable:

julia> which(convert, (Type{T} where T<:Symbol,Any))
ERROR: no unique matching method found for the specified argument types
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] which(::Any, ::Any) at ./reflection.jl:1138
 [3] top-level scope at REPL[9]:1

julia> methods(convert, (Type{T} where T<:Symbol,Any))
# 3 methods for generic function "convert":
[1] convert(::Type{Union{}}, x) in Base at essentials.jl:169
[2] convert(::Type{T}, x::T) where T in Base at essentials.jl:171
[3] convert(::Type{T}, arg) where T<:VecElement in Base at baseext.jl:8

This particular invalidation is not consequential (that setindex! MethodInstance apparently has no callers), but intersection Tuple{typeof(convert),Type{T} where T<:Tuple,Tuple{}} alone leads to about 550 unique invalidations.

julia> which(convert, (Type{T} where T<:Tuple,Tuple{}))
ERROR: no unique matching method found for the specified argument types
Stacktrace:
 [1] error(::String) at ./error.jl:33
 [2] which(::Any, ::Any) at ./reflection.jl:1138
 [3] top-level scope at REPL[2]:1

julia> methods(convert, (Type{T} where T<:Tuple,Tuple{}))
# 6 methods for generic function "convert":
[1] convert(::Type{Tuple{}}, ::Tuple{}) in Base at essentials.jl:305
[2] convert(::Type{Union{}}, x) in Base at essentials.jl:169
[3] convert(::Type{Tuple{Vararg{V,N} where N}}, x::Tuple{Vararg{V,N} where N}) where V in Base at essentials.jl:314
[4] convert(T::Type{Tuple{Vararg{V,N} where N}}, x::Tuple) where V in Base at essentials.jl:315
[5] convert(::Type{T}, x::T) where T in Base at essentials.jl:171
[6] convert(::Type{T}, arg) where T<:VecElement in Base at baseext.jl:8

I added a concrete test case for this in #35855.

@timholy
Copy link
Sponsor Member

timholy commented Jun 3, 2020

I fixed conflicts from #35768. This PR doesn't have tests, but they are coming in #35855, which I've slimmed down to currently-failing tests. Should we merge that and then this? Vice versa?

The issue I point out in #35877 (comment) turns out to be mostly due to this method. I ran some tests locally that suggest we can delete it without needing other changes. So it's not obvious this needs to wait on other improvements. Thoughts, Jeff?

@JeffBezanson
Copy link
Sponsor Member Author

@vtjnash might have some concerns --- is this at least consistent with the ambiguity detection we're capable of now, or does it make things worse?

@timholy
Copy link
Sponsor Member

timholy commented Jun 3, 2020

Worth waiting for. Maybe merge #35855 now, and change @test_broken->@test when this gets finalized?

src/gf.c Outdated
jl_array_ptr_1d_push(_jl_debug_method_invalidation, (jl_value_t*)mi);
if (!loctag) {
loctag = jl_cstr_to_string("jl_method_table_insert");
jl_gc_wb(_jl_debug_method_invalidation, loctag);
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

This jl_gc_wb doesn't do anything (there's no memory being modified). Why is it here?

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

My fault, came from resolving conflict with #35768 and I resolved it into here.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

None of those do anything either. What are they supposed to do?

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

What are "those"? You mean all the jl_gc_wb in #35768? I thought from https://docs.julialang.org/en/latest/manual/embedding/#Updating-fields-of-GC-managed-objects-1 that I'd need to protect them but I guess that's not necessary for array elements, only fields? I can get rid of them.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Wouldn't work for that anyways

@vtjnash
Copy link
Sponsor Member

vtjnash commented Jun 3, 2020

Hm, that actually may point to a simpler algorithm which could fix the problems with this one. Here, we only consider if the intersection was also partly covered by an ambiguity. But instead, what we really wanted to know is if the intersection was fully covered by an existing method that is not less specific.

@timholy
Copy link
Sponsor Member

timholy commented Jun 8, 2020

As I pointed out in #36005 (comment), this and one other (#35915) are basically the only things standing in the way of "democratizing" the hunt for invalidations. Would love to see this finished off. If no one else has time I could give it a whirl, but I have gotten the sense that @vtjnash knows what should be done.

@JeffBezanson
Copy link
Sponsor Member Author

I can try implementing it.

@timholy
Copy link
Sponsor Member

timholy commented Jun 24, 2020

I'm sure this is laughable, but for me

index e5870bdb9b..65dea8c3b6 100644
--- a/base/essentials.jl
+++ b/base/essentials.jl
@@ -166,7 +166,6 @@ true
 """
 function convert end
 
-convert(::Type{Union{}}, x) = throw(MethodError(convert, (Union{}, x)))
 convert(::Type{Any}, x) = x
 convert(::Type{T}, x::T) where {T} = x
 convert(::Type{Type}, x::Type) = x # the ssair optimizer is strongly dependent on this method existing to avoid over-specialization
diff --git a/src/gf.c b/src/gf.c
index f801cfc976..0de2f7008f 100644
--- a/src/gf.c
+++ b/src/gf.c
@@ -1725,11 +1725,13 @@ static int is_call_ambiguous(jl_methtable_t *mt, jl_value_t *types JL_PROPAGATES
         return 0; // we added a new definition to consider
     jl_typemap_entry_t *m = jl_typemap_morespecific_by_type(entry, (jl_value_t*)types, NULL, world);
     if (m == NULL) {
-        // TODO: this isn't quite right, since we just ask if the entry dominates
-        // over all subtypes of `types`, but wanted to ask the inverse question
         return 1; // a new ambiguity doesn't change the result of method lookup
     }
-    return 0; // existing and/or method already covered this intersection (TODO: which is it?)
+    jl_value_t *isect = isect = jl_type_intersection(types, (jl_value_t*)m->sig);
+    size_t minworld = 0;
+    size_t maxworld = (size_t)world;
+    jl_value_t *mtchs = ml_matches(mt->defs, 0, (jl_tupletype_t*)isect, -1, 0, world, &minworld, &maxworld);
+    return jl_array_len(mtchs) == 0;
 }
 
 JL_DLLEXPORT void jl_method_table_insert(jl_methtable_t *mt, jl_method_t *method, jl_tupletype_t *simpletype)

seems promising in fixing the issue.

Would love to get this merged. Ambiguities are almost surely the single biggest "systematic" source of invalidations left. I can't tell for sure because SnoopCompile's analysis of this is crude and certainly wrong; if this is right, I can just trust it and move on with fixing specific invalidations. But the SNR is getting low enough that it's less satisfying to keep working on this without getting this case resolved first.

@vtjnash
Copy link
Sponsor Member

vtjnash commented Jun 24, 2020

I think that's also probably nearly right. One other observation I made while working on this code (61b1248a69d40c2fb931c35facf4e69e3a68fcd0) is that check_ambiguous_visitor is probably already visiting this total list of candidate prior ambiguities (everything for which ambig is true or shadowed is false). The tricky part is in implementing the compare method there (since it's not transitive)!

@timholy
Copy link
Sponsor Member

timholy commented Jun 25, 2020

Hmm, even with this I am seeing that

(::Type{T})(itr) where {T<:Tuple} = _totuple(T, itr)
mt-invalidates
convert(::Type{T}, a::AbstractArray) where {T<:Array} = a isa T ? a : T(a)
despite the fact that

julia> typeintersect(Tuple, Array)
Union{}

julia> methods(Union{}, (Any,))
# 3 methods:
[1] (::Type{T})(x::Tuple) where T<:Tuple in Base at tuple.jl:225
[2] (::Type{T})(nt::NamedTuple) where T<:Tuple in Base at namedtuple.jl:128
[3] (::Type{T})(itr) where T<:Tuple in Base at tuple.jl:230

But I guess

julia> methods(Union{}, (AbstractArray,))
# 1 method:
[1] (::Type{T})(itr) where T<:Tuple in Base at tuple.jl:230

is the only method, so maybe this is just something we need to look for explicitly? Not quite sure what the best way to implement this would be.

@timholy
Copy link
Sponsor Member

timholy commented Jun 25, 2020

Do we also need to throw in some module logic? Currently

(::Type{T})(x::Float16) where {T<:Integer} = T(Float32(x))
mt-invalidates Core.Compiler's copy of
convert(::Type{T}, x::Number) where {T<:Number} = T(x)
despite the fact that Core.Compiler never loads float.jl.

xref #36374, see comment about loading Plots invalidating 94 MethodInstances in Core.Compiler.

@timholy
Copy link
Sponsor Member

timholy commented Jun 25, 2020

This diff seems to exclude methods in Core from other modules:

diff --git a/src/gf.c b/src/gf.c
index eb293b77b3..d84f1c5b69 100644
--- a/src/gf.c
+++ b/src/gf.c
@@ -1774,10 +1774,17 @@ static int is_call_ambiguous(jl_methtable_t *mt, jl_value_t *types JL_PROPAGATES
     jl_value_t *isect = isect = jl_type_intersection(types, (jl_value_t*)m->sig);
     size_t minworld = 0;
     size_t maxworld = (size_t)world;
-    jl_value_t *mtchs = ml_matches(mt->defs, 0, (jl_tupletype_t*)isect, -1, 0, world, &minworld, &maxworld);
+    jl_value_t *mtchs = ml_matches(mt, 0, (jl_tupletype_t*)isect, -1, 0, world, &minworld, &maxworld, 0);
     return jl_array_len(mtchs) == 0;
 }
 
+jl_module_t* top_module(jl_module_t* mod)
+{
+    while (mod->parent != mod)
+        mod = mod->parent;
+    return mod;
+}
+
 JL_DLLEXPORT void jl_method_table_insert(jl_methtable_t *mt, jl_method_t *method, jl_tupletype_t *simpletype)
 {
     JL_TIMING(ADD_METHOD);
@@ -1816,9 +1823,12 @@ JL_DLLEXPORT void jl_method_table_insert(jl_methtable_t *mt, jl_method_t *method
             size_t ins = 0;
             for (i = 1; i < na; i += 2) {
                 jl_value_t *backedgetyp = backedges[i - 1];
+                jl_method_instance_t *backedge = (jl_method_instance_t*)backedges[i];
+                if (top_module(backedge->def.module) == jl_core_module &&
+                    top_module(method->module) != jl_core_module)
+                    continue;
                 isect = jl_type_intersection(backedgetyp, (jl_value_t*)type);
                 if (isect != jl_bottom_type && !is_call_ambiguous(mt, isect, max_world)) {
-                    jl_method_instance_t *backedge = (jl_method_instance_t*)backedges[i];
                     invalidate_method_instance(backedge, max_world, 0);
                     invalidated = 1;
                     if (_jl_debug_method_invalidation)

(First changed line of the diff is from rebasing.)

@codecov-commenter
Copy link

Codecov Report

Merging #35877 into master will increase coverage by 0.00%.
The diff coverage is 85.73%.

Impacted file tree graph

@@           Coverage Diff            @@
##           master   #35877    +/-   ##
========================================
  Coverage   86.11%   86.12%            
========================================
  Files         348      349     +1     
  Lines       64975    65401   +426     
========================================
+ Hits        55955    56324   +369     
- Misses       9020     9077    +57     
Impacted Files Coverage Δ
base/compiler/compiler.jl 27.27% <ø> (ø)
base/complex.jl 99.59% <ø> (ø)
base/gcutils.jl 80.00% <ø> (ø)
base/indices.jl 79.48% <ø> (+0.64%) ⬆️
base/initdefs.jl 55.65% <0.00%> (-0.49%) ⬇️
base/regex.jl 80.78% <ø> (ø)
base/strings/io.jl 89.00% <ø> (ø)
base/strings/substring.jl 93.57% <ø> (ø)
base/strings/unicode.jl 94.73% <ø> (ø)
stdlib/Distributed/src/clusterserialize.jl 55.81% <0.00%> (ø)
... and 147 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f65561f...f65a8a2. Read the comment docs.

@timholy
Copy link
Sponsor Member

timholy commented Jun 25, 2020

master:

master:
julia> @time using Plots
  6.092184 seconds (13.64 M allocations: 797.681 MiB, 4.29% gc time)

julia> @time display(plot(rand(10)))
  6.243096 seconds (9.41 M allocations: 526.851 MiB, 3.82% gc time)

This branch, together with the patches provided above:

julia> @time using Plots
  4.939772 seconds (9.16 M allocations: 570.798 MiB, 4.28% gc time)

julia> @time display(plot(rand(10)))
  6.210409 seconds (9.45 M allocations: 528.374 MiB, 1.86% gc time)

(Caveat: also with #36427 but I doubt that changes anything.)

The savings are quite reproducible. Let's do this! Not only is it useful on its own, but it will clear out a bunch of useless clutter from the diagnostics of how to keep fixing other invalidations.

@vtjnash
Copy link
Sponsor Member

vtjnash commented Jun 25, 2020

Invalidations of Core.Compiler are not generally an issue, since they might affect some of the show code inside base, but they won't require reanalysis for the compiler itself.

The main reason I asked to stall this, during this stage of the release cycle, is I'm testing a fairly significant overhaul of this system in #35983, which is nearing possible completion, and might need a slightly different approach.

@timholy
Copy link
Sponsor Member

timholy commented Jun 25, 2020

Invalidations of Core.Compiler are not generally an issue, since they might affect some of the show code inside base, but they won't require reanalysis for the compiler itself.

I wondered about that. I'd seen a bit of re-inference via @snoopi but I wasn't sure how to interpret it.

The main reason I asked to stall this,

Oh, I didn't know you'd asked to stall it. I was puzzled by the lack of movement, thanks for explaining, it makes a difference. Good luck with #35983, that does seem like an interesting experiment.

I will close #36374 as it was basically lobbying for finishing this 😄.

@JeffBezanson
Copy link
Sponsor Member Author

I don't believe the module-based logic is technically valid, since Core and Base share the Type type. This is a perennial source of ugliness. We'll need some new mechanism to separate such methods (possibly based on world ages?).

@timholy
Copy link
Sponsor Member

timholy commented Jun 25, 2020

Sounds like the Core.Compiler stuff isn't that much of an issue anyway...I just wasn't sure, and it looked noteworthy.

@timholy
Copy link
Sponsor Member

timholy commented Jul 13, 2020

xref JuliaLang/www.julialang.org#794 (comment), JuMP is highly recommended as a test case for measuring the effect of this PR. (It bundles StaticArrays, which would be the other one I'd recommend, so JuMP is 2 for the price of 1.)

The main reason I asked to stall this

Now that #35983 has been merged, are there other components of that work that this is still waiting on?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler:latency Compiler latency
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants