Skip to content

Commit b3829eb

Browse files
committed
Merge adjacent implicit binding partitions
After: ``` julia> convert(Core.Binding, GlobalRef(Base, :Intrinsics)) Binding Base.Intrinsics 617:∞ - implicit `using` resolved to constant Core.Intrinsics 0:616 - undefined binding - guard entry julia> convert(Core.Binding, GlobalRef(Base, :Math)) Binding Base.Math 22128:∞ - constant binding to Base.Math 0:22127 - backdated constant binding to Base.Math ``` There is a bit of trickiness here. In particular, the question is, "when do we check" whether the partition next to the one we currently looked at happens to have the same implicit resolution as our current one. The most obvious answer is that we should do it on access, but in practice that would require essentially scanning back and considering every possible world age state at every lookup. This is undesirable - the lookup is not crazy expensive, but it can add up and most world ages we never touch, so it is also wasteful. This instead implements a different approach where we only perform the resolution for world ages that somebody actually asked about, but can then subsequently merge partitions if we do find that they are identical. The logic for that is a bit involved, since we need to be careful to keep the datastructure valid at every point, but does address the issue. Fixes #57923
1 parent f211a77 commit b3829eb

File tree

4 files changed

+138
-11
lines changed

4 files changed

+138
-11
lines changed

src/jltypes.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3266,8 +3266,10 @@ void jl_init_types(void) JL_GC_DISABLED
32663266
jl_svec(5, jl_any_type,
32673267
jl_ulong_type, jl_ulong_type, jl_any_type/*jl_binding_partition_type*/, jl_ulong_type),
32683268
jl_emptysvec, 0, 1, 0);
3269-
const static uint32_t binding_partition_atomicfields[] = { 0b01101 }; // Set fields 1, 3, 4 as atomic
3269+
const static uint32_t binding_partition_atomicfields[] = { 0b01111 }; // Set fields 1, 2, 3, 4 as atomic
32703270
jl_binding_partition_type->name->atomicfields = binding_partition_atomicfields;
3271+
const static uint32_t binding_partition_constfields[] = { 0x100001 }; // Set fields 1, 5 as constant
3272+
jl_binding_partition_type->name->constfields = binding_partition_constfields;
32713273

32723274
jl_binding_type =
32733275
jl_new_datatype(jl_symbol("Binding"), core, jl_any_type, jl_emptysvec,

src/julia.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,7 @@ typedef struct JL_ALIGNED_ATTR(8) _jl_binding_partition_t {
760760
* } restriction;
761761
*/
762762
jl_value_t *restriction;
763-
size_t min_world;
763+
_Atomic(size_t) min_world;
764764
_Atomic(size_t) max_world;
765765
_Atomic(struct _jl_binding_partition_t *) next;
766766
size_t kind;

src/module.c

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,70 @@ static void update_implicit_resolution(struct implicit_search_resolution *to_upd
126126

127127
static jl_binding_partition_t *jl_implicit_import_resolved(jl_binding_t *b, struct implicit_search_gap gap, struct implicit_search_resolution resolution)
128128
{
129+
size_t new_kind = resolution.ultimate_kind | gap.inherited_flags;
130+
size_t new_max_world = gap.max_world < resolution.max_world ? gap.max_world : resolution.max_world;
131+
size_t new_min_world = gap.min_world > resolution.min_world ? gap.min_world : resolution.min_world;
132+
jl_binding_partition_t *next = gap.replace;
133+
if (jl_is_binding_partition(gap.parent)) {
134+
// Check if we can merge this into the previous binding partition
135+
jl_binding_partition_t *prev = (jl_binding_partition_t *)gap.parent;
136+
size_t expected_prev_min_world = new_max_world + 1;
137+
if (prev->restriction == resolution.binding_or_const && prev->kind == new_kind) {
138+
if (!jl_atomic_cmpswap(&prev->min_world, &expected_prev_min_world, new_min_world)) {
139+
if (expected_prev_min_world <= new_min_world) {
140+
return prev;
141+
}
142+
else if (expected_prev_min_world <= new_max_world) {
143+
// Concurrent modification by another thread - bail.
144+
return NULL;
145+
}
146+
// There remains a gap - proceed
147+
} else {
148+
if (next) {
149+
size_t next_min_world = jl_atomic_load_relaxed(&next->min_world);
150+
expected_prev_min_world = new_min_world;
151+
for (;;) {
152+
// We've updated the previous partition - check if we've closed a gap
153+
size_t next_max_world = jl_atomic_load_relaxed(&next->max_world);
154+
if (next_max_world == expected_prev_min_world-1 && next->kind == new_kind && next->restriction == resolution.binding_or_const) {
155+
if (jl_atomic_cmpswap(&prev->min_world, &expected_prev_min_world, next_min_world)) {
156+
jl_binding_partition_t *nextnext = jl_atomic_load_relaxed(&next->next);
157+
if (!jl_atomic_cmpswap(&prev->next, &next, nextnext)) {
158+
// `next` may have been merged into its subsequent partition - we need to retry
159+
assert(next);
160+
continue;
161+
}
162+
// N.B.: This can lose modifications to next->{min_world, next}.
163+
// However, those modifications could only have been for another implicit
164+
// partition, so we are ok to lose them and recompute them later if necessary.
165+
}
166+
assert(expected_prev_min_world <= new_min_world);
167+
}
168+
break;
169+
}
170+
}
171+
return prev;
172+
}
173+
}
174+
}
129175
jl_binding_partition_t *new_bpart = new_binding_partition();
130-
jl_atomic_store_relaxed(&new_bpart->max_world, gap.max_world < resolution.max_world ? gap.max_world : resolution.max_world);
131-
new_bpart->min_world = gap.min_world > resolution.min_world ? gap.min_world : resolution.min_world;
132-
new_bpart->kind = resolution.ultimate_kind | gap.inherited_flags;
176+
jl_atomic_store_relaxed(&new_bpart->max_world, new_max_world);
177+
new_bpart->kind = new_kind;
133178
new_bpart->restriction = resolution.binding_or_const;
134179
jl_gc_wb_fresh(new_bpart, new_bpart->restriction);
135-
jl_atomic_store_relaxed(&new_bpart->next, gap.replace);
180+
181+
if (next) {
182+
// See if we can merge the next partition into this one
183+
size_t next_max_world = jl_atomic_load_relaxed(&next->max_world);
184+
if (next_max_world == new_min_world - 1 && next->kind == new_kind && next->restriction == resolution.binding_or_const) {
185+
// See above for potentially losing modifications to next.
186+
new_min_world = jl_atomic_load_acquire(&next->min_world);
187+
next = jl_atomic_load_relaxed(&next->next);
188+
}
189+
}
190+
191+
new_bpart->min_world = new_min_world;
192+
jl_atomic_store_relaxed(&new_bpart->next, next);
136193
if (!jl_atomic_cmpswap(gap.insert, &gap.replace, new_bpart))
137194
return NULL;
138195
jl_gc_wb(gap.parent, new_bpart);
@@ -170,13 +227,11 @@ struct implicit_search_resolution jl_resolve_implicit_import(jl_binding_t *b, mo
170227
struct _jl_module_using data = *module_usings_getidx(m, i);
171228
JL_UNLOCK(&m->lock);
172229
if (data.min_world > world) {
173-
if (max_world > data.min_world)
174-
max_world = data.min_world - 1;
230+
max_world = WORLDMIN(max_world, data.min_world - 1);
175231
continue;
176232
}
177233
if (data.max_world < world) {
178-
if (min_world < data.max_world)
179-
min_world = data.max_world + 1;
234+
min_world = WORLDMAX(min_world, data.max_world + 1);
180235
continue;
181236
}
182237

@@ -279,8 +334,13 @@ JL_DLLEXPORT jl_binding_partition_t *jl_maybe_reresolve_implicit(jl_binding_t *b
279334
assert(bpart == jl_atomic_load_relaxed(&b->partitions));
280335
assert(bpart);
281336
struct implicit_search_resolution resolution = jl_resolve_implicit_import(b, NULL, new_max_world+1, 0);
337+
int resolution_unchanged = bpart->restriction == resolution.binding_or_const && jl_binding_kind(bpart) == resolution.ultimate_kind;
282338
if (resolution.min_world == bpart->min_world) {
283-
assert(bpart->restriction == resolution.binding_or_const && jl_binding_kind(bpart) == resolution.ultimate_kind);
339+
// The resolution has the same world bounds - it must be unchanged
340+
assert(resolution_unchanged);
341+
return bpart;
342+
} else if (resolution_unchanged) {
343+
// If the resolution is unchanged, we can still keep the bpart
284344
return bpart;
285345
}
286346
assert(resolution.min_world == new_max_world+1 && "Missed an invalidation or bad resolution bounds");

test/rebinding.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,68 @@ module UndefinedTransitions
318318
@test Base.Compiler.is_nothrow(Base.Compiler.decode_effects(ci.ipo_purity_bits))
319319
end
320320
end
321+
322+
# Identical implicit partitions should be merge (#57923)
323+
for binding in (convert(Core.Binding, GlobalRef(Base, :Math)),
324+
convert(Core.Binding, GlobalRef(Base, :Intrinsics)))
325+
# Test that these both only have two partitions
326+
@test isdefined(binding, :partitions)
327+
@test isdefined(binding.partitions, :next)
328+
@test !isdefined(binding.partitions.next, :next)
329+
end
330+
331+
# Test various scenarios for implicit partition merging
332+
module MergeStress
333+
for i = 1:5
334+
@eval module $(Symbol("M$i"))
335+
export x, y
336+
const x = 1
337+
const y = 2
338+
end
339+
end
340+
const before = Base.get_world_counter()
341+
using .M1
342+
const afterM1 = Base.get_world_counter()
343+
using .M2
344+
const afterM2 = Base.get_world_counter()
345+
using .M3
346+
const afterM3 = Base.get_world_counter()
347+
using .M4
348+
const afterM4 = Base.get_world_counter()
349+
using .M5
350+
const afterM5 = Base.get_world_counter()
351+
end
352+
353+
function count_partitions(b::Core.Binding)
354+
n = 0
355+
isdefined(b, :partitions) || return n
356+
bpart = b.partitions
357+
while true
358+
n += 1
359+
isdefined(bpart, :next) || break
360+
bpart = bpart.next
361+
end
362+
return n
363+
end
364+
using Base: invoke_in_world
365+
366+
const xbinding = convert(Core.Binding, GlobalRef(MergeStress, :x))
367+
function access_and_count(point)
368+
invoke_in_world(getglobal(MergeStress, point), getglobal, MergeStress, :x)
369+
count_partitions(xbinding)
370+
end
371+
372+
@test count_partitions(xbinding) == 0
373+
@test access_and_count(:afterM1) == 1
374+
# M2 is the first change to the `usings` table after M1. The partitions
375+
# can and should be merged
376+
@test access_and_count(:afterM2) == 1
377+
378+
# There is a gap between M2 and M5 - the partitions should not be merged
379+
@test access_and_count(:afterM5) == 2
380+
381+
# M4 and M5 are adjacent, these partitions should also be merged (in the opposite direction)
382+
@test access_and_count(:afterM4) == 2
383+
384+
# M3 connects all, so we should have a single partition
385+
@test access_and_count(:afterM3) == 1

0 commit comments

Comments
 (0)