X Tutup
Skip to content

Commit ce8952b

Browse files
authored
Use borrowed pointers in TYPE_CACHE instead of strong references (RustPython#7384)
The method cache (TYPE_CACHE) was storing strong references (Arc clones) to cached attribute values, which inflated sys.getrefcount(). This caused intermittent test_memoryview failures where refcount assertions would fail depending on GC collection timing. Store borrowed raw pointers instead. Safety is guaranteed because: - type_cache_clear() nullifies all entries during GC collection, before the collector breaks cycles - type_cache_clear_version() nullifies entries when a type is modified, before the source dict entry is removed - Readers use try_to_owned_from_ptr (safe_inc) to atomically validate and increment the refcount on cache hit
1 parent cbcc197 commit ce8952b

File tree

1 file changed

+22
-35
lines changed

1 file changed

+22
-35
lines changed

crates/vm/src/builtins/type.rs

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ struct TypeCacheEntry {
8181
/// Interned attribute name pointer (pointer equality check).
8282
name: AtomicPtr<PyStrInterned>,
8383
/// Cached lookup result as raw pointer. null = empty.
84-
/// The cache holds a strong reference (refcount incremented).
84+
/// The cache holds a **borrowed** pointer (no refcount increment).
85+
/// Safety: `type_cache_clear()` nullifies all entries during GC,
86+
/// and `type_cache_clear_version()` nullifies entries when a type
87+
/// is modified — both before the source dict entry is removed.
88+
/// Types are always part of reference cycles (via `mro` self-reference)
89+
/// so they are always collected by the cyclic GC (never refcount-freed).
8590
value: AtomicPtr<PyObject>,
8691
}
8792

@@ -149,13 +154,11 @@ impl TypeCacheEntry {
149154
self.sequence.load(Ordering::Relaxed) == previous
150155
}
151156

152-
/// Take the value out of this entry, returning the owned PyObjectRef.
157+
/// Null out the cached value pointer.
153158
/// Caller must ensure no concurrent reads can observe this entry
154159
/// (version should be set to 0 first).
155-
fn take_value(&self) -> Option<PyObjectRef> {
156-
let ptr = self.value.swap(core::ptr::null_mut(), Ordering::Relaxed);
157-
// SAFETY: non-null ptr was stored via PyObjectRef::into_raw
158-
NonNull::new(ptr).map(|nn| unsafe { PyObjectRef::from_raw(nn) })
160+
fn clear_value(&self) {
161+
self.value.store(core::ptr::null_mut(), Ordering::Relaxed);
159162
}
160163
}
161164

@@ -180,45 +183,36 @@ fn type_cache_hash(version: u32, name: &'static PyStrInterned) -> usize {
180183
((version ^ name_hash) as usize) & TYPE_CACHE_MASK
181184
}
182185

183-
/// Invalidate cache entries for a specific version tag and release values.
186+
/// Invalidate cache entries for a specific version tag.
184187
/// Called from modified() when a type is changed.
185188
fn type_cache_clear_version(version: u32) {
186-
let mut to_drop = Vec::new();
187189
for entry in TYPE_CACHE.iter() {
188190
if entry.version.load(Ordering::Relaxed) == version {
189191
entry.begin_write();
190192
if entry.version.load(Ordering::Relaxed) == version {
191193
entry.version.store(0, Ordering::Release);
192-
if let Some(v) = entry.take_value() {
193-
to_drop.push(v);
194-
}
194+
entry.clear_value();
195195
}
196196
entry.end_write();
197197
}
198198
}
199-
drop(to_drop);
200199
}
201200

202201
/// Clear all method cache entries (_PyType_ClearCache).
203-
/// Called during GC collection to release strong references that might
204-
/// prevent cycle collection.
202+
/// Called during GC collection to nullify borrowed pointers before
203+
/// the collector breaks cycles.
205204
///
206205
/// Sets TYPE_CACHE_CLEARING to suppress cache re-population during the
207206
/// entire operation, preventing concurrent lookups from repopulating
208207
/// entries while we're clearing them.
209208
pub fn type_cache_clear() {
210209
TYPE_CACHE_CLEARING.store(true, Ordering::Release);
211-
// Invalidate all entries and collect values.
212-
let mut to_drop = Vec::new();
213210
for entry in TYPE_CACHE.iter() {
214211
entry.begin_write();
215212
entry.version.store(0, Ordering::Release);
216-
if let Some(v) = entry.take_value() {
217-
to_drop.push(v);
218-
}
213+
entry.clear_value();
219214
entry.end_write();
220215
}
221-
drop(to_drop);
222216
TYPE_CACHE_CLEARING.store(false, Ordering::Release);
223217
}
224218

@@ -430,9 +424,8 @@ impl PyType {
430424
return;
431425
}
432426
self.tp_version_tag.store(0, Ordering::SeqCst);
433-
// Release strong references held by cache entries for this version.
434-
// We hold owned refs that would prevent GC of class attributes after
435-
// type deletion.
427+
// Nullify borrowed pointers in cache entries for this version
428+
// so they don't dangle after the dict is modified.
436429
type_cache_clear_version(old_version);
437430
let subclasses = self.subclasses.read();
438431
for weak_ref in subclasses.iter() {
@@ -809,9 +802,9 @@ impl PyType {
809802
}
810803

811804
pub fn set_attr(&self, attr_name: &'static PyStrInterned, value: PyObjectRef) {
812-
// Invalidate caches BEFORE modifying attributes so that cached
813-
// descriptor pointers are still alive when type_cache_clear_version
814-
// drops the cache's strong references.
805+
// Invalidate caches BEFORE modifying attributes so that borrowed
806+
// pointers in cache entries are nullified while the source objects
807+
// are still alive.
815808
self.modified();
816809
self.attributes.write().insert(attr_name, value);
817810
}
@@ -974,19 +967,13 @@ impl PyType {
974967
entry.begin_write();
975968
// Invalidate first to prevent readers from seeing partial state
976969
entry.version.store(0, Ordering::Release);
977-
// Swap in new value (refcount held by cache)
978-
let new_ptr = found.clone().into_raw().as_ptr();
979-
let old_ptr = entry.value.swap(new_ptr, Ordering::Relaxed);
970+
// Store borrowed pointer (no refcount increment).
971+
let new_ptr = &**found as *const PyObject as *mut PyObject;
972+
entry.value.store(new_ptr, Ordering::Relaxed);
980973
entry.name.store(name_ptr, Ordering::Relaxed);
981974
// Activate entry — Release ensures value/name writes are visible
982975
entry.version.store(assigned, Ordering::Release);
983976
entry.end_write();
984-
// Drop previous occupant (its version was already invalidated)
985-
if !old_ptr.is_null() {
986-
unsafe {
987-
drop(PyObjectRef::from_raw(NonNull::new_unchecked(old_ptr)));
988-
}
989-
}
990977
}
991978

992979
result

0 commit comments

Comments
 (0)
X Tutup