Commit 847630cc authored by Rudi Chen's avatar Rudi Chen

Implement PyPy's finalization ordering algorithm.

- Call all finalizers properly, in a safe-to-do-so order.
- Make finalizers work together with weak reference callbacks - namely,
  both should be able to get called, and weakref callbacks should be
  called before finalizers.
- Old style classes are also supported, but we always do an attribute
  lookup for tp_del because there's no tp_del slot. Since it would be
  too slow to do an attribute lookup all the time in GC, we just assume
  all old-style classes have an ordered finalizer.
parent debf2077
......@@ -2650,13 +2650,9 @@ public:
}
void doSafePoint(AST_stmt* next_statement) override {
// If the sampling profiler is turned on (and eventually, destructors), we need frame-introspection
// support while in allowGLReadPreemption:
#if ENABLE_SAMPLING_PROFILER
// Unwind info is always needed in allowGLReadPreemption if it has any chance of
// running arbitrary code like finalizers.
emitter.createCall(UnwindInfo(next_statement, NULL), g.funcs.allowGLReadPreemption);
#else
emitter.getBuilder()->CreateCall(g.funcs.allowGLReadPreemption);
#endif
}
};
......
......@@ -23,6 +23,7 @@
#include "core/common.h"
#include "core/thread_utils.h"
#include "gc/collector.h"
namespace pyston {
class Box;
......@@ -108,6 +109,16 @@ extern "C" inline void allowGLReadPreemption() {
}
#endif
// We need to call the finalizers on dead objects at some point. This is a safe place to do so.
// This needs to be done before checking for other threads waiting on the GIL since there could
// be only one thread doing a lot of work. Similarly for weakref callbacks.
//
// The conditional is an optimization - the function will do nothing if the lists are empty,
// but it's worth checking for to avoid the overhead of making a function call.
if (!gc::pending_finalization_list.empty() || !gc::weakrefs_needing_callback_list.empty()) {
gc::callPendingDestructionLogic();
}
// Double-checked locking: first read with no ordering constraint:
if (!threads_waiting_on_gil.load(std::memory_order_relaxed))
return;
......
......@@ -615,6 +615,7 @@ extern "C" PyObject* PystonType_GenericAlloc(BoxedClass* cls, Py_ssize_t nitems)
\
/* Don't allocate classes through this -- we need to keep track of all class objects. */ \
assert(default_cls != type_cls); \
assert(!gc::hasOrderedFinalizer(default_cls)); \
\
/* note: we want to use size instead of tp_basicsize, since size is a compile-time constant */ \
void* mem = gc_alloc(size, gc::GCKind::PYTHON); \
......
This diff is collapsed.
......@@ -15,6 +15,8 @@
#ifndef PYSTON_GC_COLLECTOR_H
#define PYSTON_GC_COLLECTOR_H
#include <deque>
#include <list>
#include <vector>
#include "core/types.h"
......@@ -30,6 +32,9 @@ extern FILE* trace_fp;
#define GC_TRACE_LOG(...)
#endif
extern std::deque<Box*> pending_finalization_list;
extern std::deque<PyWeakReference*> weakrefs_needing_callback_list;
// Mark this gc-allocated object as being a root, even if there are no visible references to it.
// (Note: this marks the gc allocation itself, not the pointer that points to one. For that, use
// a GCRootHandle)
......@@ -58,6 +63,7 @@ public:
Box* operator->() { return value; }
};
void callPendingDestructionLogic();
void runCollection();
// Python programs are allowed to pause the GC. This is supposed to pause automatic GC,
......@@ -72,6 +78,7 @@ bool isValidGCMemory(void* p); // if p is a valid gc-allocated pointer (or a non
bool isValidGCObject(void* p); // whether p is valid gc memory and is set to have Python destructor semantics applied
bool isNonheapRoot(void* p);
void registerPythonObject(Box* b);
void invalidateOrderedFinalizerList();
// Debugging/validation helpers: if a GC should not happen in certain sections (ex during unwinding),
// use these functions to mark that. This is different from disableGC/enableGC, since it causes an
......
......@@ -91,6 +91,7 @@ inline void sweepList(ListT* head, std::vector<Box*>& weakly_referenced, Free fr
auto cur = head;
while (cur) {
GCAllocation* al = cur->data;
clearOrderingState(al);
if (isMarked(al)) {
clearMark(al);
cur = cur->next;
......@@ -122,6 +123,39 @@ void _bytesAllocatedTripped() {
runCollection();
}
//////
/// Finalizers
bool hasOrderedFinalizer(BoxedClass* cls) {
if (cls->has_safe_tp_dealloc) {
assert(!cls->tp_del);
return false;
} else if (cls->hasNonDefaultTpDealloc()) {
return true;
} else {
// The default tp_dealloc calls tp_del if there is one.
return cls->tp_del != NULL;
}
}
void finalize(Box* b) {
GCAllocation* al = GCAllocation::fromUserData(b);
assert(!hasFinalized(al));
setFinalized(al);
b->cls->tp_dealloc(b);
}
__attribute__((always_inline)) bool isWeaklyReferenced(Box* b) {
if (PyType_SUPPORTS_WEAKREFS(b->cls)) {
PyWeakReference** list = (PyWeakReference**)PyObject_GET_WEAKREFS_LISTPTR(b);
if (list && *list) {
return true;
}
}
return false;
}
Heap global_heap;
__attribute__((always_inline)) bool _doFree(GCAllocation* al, std::vector<Box*>* weakly_referenced) {
......@@ -145,17 +179,23 @@ __attribute__((always_inline)) bool _doFree(GCAllocation* al, std::vector<Box*>*
#endif
assert(b->cls);
if (PyType_SUPPORTS_WEAKREFS(b->cls)) {
PyWeakReference** list = (PyWeakReference**)PyObject_GET_WEAKREFS_LISTPTR(b);
if (list && *list) {
assert(weakly_referenced && "attempting to free a weakly referenced object manually");
weakly_referenced->push_back(b);
return false;
}
if (isWeaklyReferenced(b)) {
assert(weakly_referenced && "attempting to free a weakly referenced object manually");
weakly_referenced->push_back(b);
return false;
}
ASSERT(!hasOrderedFinalizer(b->cls) || hasFinalized(al) || alloc_kind == GCKind::CONSERVATIVE_PYTHON, "%s",
getTypeName(b));
if (b->cls->tp_dealloc != dealloc_null && b->cls->has_safe_tp_dealloc) {
gc_safe_destructors.log();
GCAllocation* al = GCAllocation::fromUserData(b);
assert(!hasFinalized(al));
assert(!hasOrderedFinalizer(b->cls));
// Don't bother setting the finalized flag since the object is getting freed right now.
b->cls->tp_dealloc(b);
}
}
......@@ -452,6 +492,7 @@ SmallArena::Block** SmallArena::_freeChain(Block** head, std::vector<Box*>& weak
void* p = &b->atoms[atom_idx];
GCAllocation* al = reinterpret_cast<GCAllocation*>(p);
clearOrderingState(al);
if (isMarked(al)) {
clearMark(al);
} else {
......
......@@ -170,6 +170,10 @@ inline void clearOrderingState(GCAllocation* header) {
#undef FINALIZER_HAS_RUN_BIT
#undef ORDERING_BITS
bool hasOrderedFinalizer(BoxedClass* cls);
void finalize(Box* b);
bool isWeaklyReferenced(Box* b);
#define PAGE_SIZE 4096
template <uintptr_t arena_start, uintptr_t arena_size, uintptr_t initial_mapsize, uintptr_t increment> class Arena {
......
......@@ -20,6 +20,11 @@ namespace pyston {
static Box* gcCollect() {
gc::runCollection();
// I think it's natural that the user would expect the finalizers to get run here if we're forcing
// a GC pass. It should be safe to do, and makes testing easier also.
gc::callPendingDestructionLogic();
return None;
}
......
......@@ -335,12 +335,59 @@ void dealloc_null(Box* box) {
assert(box->cls->tp_del == NULL);
}
// Analoguous to CPython's implementation of subtype_dealloc, but having a GC
// saves us from complications involving "trashcan macros".
//
// This is the default destructor assigned to the tp_dealloc slot, the C/C++
// implementation of a Python object destructor. It may call the Python-implemented
// destructor __del__ stored in tp_del, if any.
//
// For now, we treat tp_del and tp_dealloc as one unit. In theory, we will only
// have both if we have a Python class with a __del__ method that subclasses from
// a C extension with a non-trivial tp_dealloc. We assert on that case for now
// until we run into actual code with this fairly rare situation.
//
// This case (having both tp_del and tp_dealloc) shouldn't be a problem if we
// remove the assert, except in the exceptional case where the __del__ method
// does object resurrection. The fix for this would be to spread out tp_del,
// tp_dealloc and sweeping over 3 GC passes. This would slightly impact the
// performance of Pyston as a whole for a case that may not exist in any
// production code, so we decide not to handle that edge case for now.
static void subtype_dealloc(Box* self) {
BoxedClass* type = self->cls;
if (type->tp_del) {
type->tp_del(self);
}
// Find nearest base with a different tp_dealloc.
BoxedClass* base = type;
while (base && base->tp_dealloc == subtype_dealloc) {
base = base->tp_base;
}
if (base && base->tp_dealloc && base->tp_dealloc != dealloc_null) {
RELEASE_ASSERT(!type->tp_del, "having both a tp_del and tp_dealloc not supported");
base->tp_dealloc(self);
}
}
// We don't need CPython's version of tp_free since we have GC.
// We still need to set tp_free to something and not a NULL pointer,
// because C extensions might still call tp_free from tp_dealloc.
void default_free(void*) {
}
bool BoxedClass::hasNonDefaultTpDealloc() {
// Find nearest base with a different tp_dealloc.
BoxedClass* base = this;
while (base && base->tp_dealloc == subtype_dealloc) {
base = base->tp_base;
}
return base && base->tp_dealloc && base->tp_dealloc != dealloc_null;
}
void BoxedClass::freeze() {
assert(!is_constant);
assert(tp_name); // otherwise debugging will be very hard
......@@ -396,7 +443,21 @@ BoxedClass::BoxedClass(BoxedClass* base, gcvisit_func gc_visit, int attrs_offset
assert(cls == type_cls || isSubclass(cls, type_cls));
}
assert(tp_dealloc == NULL);
if (is_user_defined) {
tp_dealloc = subtype_dealloc;
} else {
// We don't want default types like dict to have subtype_dealloc as a destructor.
// In CPython, they would have their custom tp_dealloc, except that we don't
// need them in Pyston due to GC.
//
// What's the problem with having subtype_dealloc? In some cases like defdict_dealloc,
// the destructor calls another destructor thinking it's the parent, but ends up in the
// same destructor again (since child destructors are found first by subtype_dealloc)
// causing an infinite recursion loop.
tp_dealloc = dealloc_null;
has_safe_tp_dealloc = true;
}
tp_free = default_free;
if (gc_visit == NULL) {
assert(base);
......@@ -5006,15 +5067,24 @@ Box* typeNew(Box* _cls, Box* arg1, Box* arg2, Box** _args) {
else
made->tp_alloc = PyType_GenericAlloc;
// On some occasions, Python-implemented classes inherit from C-implement classes. For
// example, KeyedRef inherits from weakref, and needs to have it's finalizer called
// whenever weakref would. So we inherit the property that a class has a safe tp_dealloc
// too. However, we must be careful to do that only when nothing else invalidates that
// property, such as the presence of a __del__ (tp_del) method.
assert(!made->has_safe_tp_dealloc);
for (auto b : *bases) {
BoxedClass* base = static_cast<BoxedClass*>(b);
if (!isSubclass(base->cls, type_cls))
continue;
if (base->has_safe_tp_dealloc) {
made->tp_dealloc = base->tp_dealloc;
made->has_safe_tp_dealloc = true;
break;
if (!made->tp_del) {
for (auto b : *bases) {
BoxedClass* base = static_cast<BoxedClass*>(b);
if (!isSubclass(base->cls, type_cls))
continue;
if (base->tp_del) {
break;
}
if (base->has_safe_tp_dealloc) {
made->has_safe_tp_dealloc = true;
break;
}
}
}
......
......@@ -3029,6 +3029,12 @@ static void setupDefaultClassGCParticipation() {
setTypeGCNone(&Match_Type);
setTypeGCNone(&Pattern_Type);
setTypeGCNone(&PyCallIter_Type);
// We just changed the has_safe_tp_dealloc field on a few classes, changing
// them from having an ordered finalizer to an unordered one.
// If some instances of those classes have already been allocated (e.g.
// preallocated exceptions), they need to be invalidated.
gc::invalidateOrderedFinalizerList();
}
bool TRACK_ALLOCATIONS = false;
......
......@@ -203,8 +203,10 @@ public:
// 3) Won't take up a lot of memory (requiring another GC run).
// 4) Won't resurrect itself.
//
// We specify that such destructors are safe for optimization purposes. We call the tp_dealloc
// as the object gets freed.
// We specify that such destructors are safe for optimization purposes (in our GC, we try to
// emulate the order of destructor calls and support resurrection by calling them in topological
// order through multiple GC passes, which is potentially quite expensive). We call the tp_dealloc
// as the object gets freed rather than put it in a pending finalizer list.
bool has_safe_tp_dealloc;
// Whether this class object is constant or not, ie whether or not class-level
......@@ -246,6 +248,9 @@ public:
return true;
}
// Checks if this class or one of its parents has a non-default tp_dealloc
bool hasNonDefaultTpDealloc();
void freeze();
protected:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment