Skip to content

Commit

Permalink
Move Declaration, Specification and ClassProvides to __slots__.
Browse files Browse the repository at this point in the history
In a test of 6000 modules that load 2245 InterfaceClass objects and produce 2233 ClassProvides instances, this saves about 1% total memory usage in Python 2.7.
  • Loading branch information
jamadden committed Jan 23, 2020
1 parent 02e11f2 commit 2d98b8e
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 37 deletions.
34 changes: 32 additions & 2 deletions src/zope/interface/_zope_interface_coptimizations.c
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ providedBy(PyObject *ignored, PyObject *ob)

typedef struct {
PyObject_HEAD
PyObject* weakreflist;
/*
In the past, these fields were stored in the __dict__
and were technically allowed to contain any Python object, though
Expand All @@ -267,6 +268,15 @@ typedef struct {
make any assumptions about contents.
*/
PyObject* _implied;
/*
The remainder aren't used in C code but must be stored here
to prevent instance layout conflicts.
*/
PyObject* dependents;
PyObject* _bases;
PyObject* _v_attrs;
PyObject* __iro__;
PyObject* __sro__;
} Spec;

/*
Expand All @@ -277,19 +287,30 @@ static int
Spec_traverse(Spec* self, visitproc visit, void* arg)
{
Py_VISIT(self->_implied);
Py_VISIT(self->dependents);
Py_VISIT(self->_v_attrs);
Py_VISIT(self->__iro__);
Py_VISIT(self->__sro__);
return 0;
}

static int
Spec_clear(Spec* self)
{
Py_CLEAR(self->_implied);
Py_CLEAR(self->dependents);
Py_CLEAR(self->_v_attrs);
Py_CLEAR(self->__iro__);
Py_CLEAR(self->__sro__);
return 0;
}

static void
Spec_dealloc(Spec* self)
{
if (self->weakreflist != NULL) {
PyObject_ClearWeakRefs(OBJECT(self));
}
Spec_clear(self);
Py_TYPE(self)->tp_free(OBJECT(self));
}
Expand Down Expand Up @@ -387,7 +408,12 @@ static struct PyMethodDef Spec_methods[] = {

static PyMemberDef Spec_members[] = {
{"_implied", T_OBJECT_EX, offsetof(Spec, _implied), 0, ""},
{NULL}
{"dependents", T_OBJECT_EX, offsetof(Spec, dependents), 0, ""},
{"_bases", T_OBJECT_EX, offsetof(Spec, _bases), 0, ""},
{"_v_attrs", T_OBJECT_EX, offsetof(Spec, _v_attrs), 0, ""},
{"__iro__", T_OBJECT_EX, offsetof(Spec, __iro__), 0, ""},
{"__sro__", T_OBJECT_EX, offsetof(Spec, __sro__), 0, ""},
{NULL},
};


Expand Down Expand Up @@ -417,7 +443,7 @@ static PyTypeObject SpecType = {
/* tp_traverse */ (traverseproc)Spec_traverse,
/* tp_clear */ (inquiry)Spec_clear,
/* tp_richcompare */ (richcmpfunc)0,
/* tp_weaklistoffset */ (long)0,
/* tp_weaklistoffset */ offsetof(Spec, weakreflist),
/* tp_iter */ (getiterfunc)0,
/* tp_iternext */ (iternextfunc)0,
/* tp_methods */ Spec_methods,
Expand Down Expand Up @@ -1779,3 +1805,7 @@ PyInit__zope_interface_coptimizations(void)
return init();
}
#endif

#ifdef __clang__
#pragma clang diagnostic pop
#endif
22 changes: 11 additions & 11 deletions src/zope/interface/declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,11 @@ def __call__(self, ob):
class Declaration(Specification):
"""Interface declarations"""

__slots__ = ()

def __init__(self, *interfaces):
Specification.__init__(self, _normalizeargs(interfaces))

def changed(self, originally_changed):
Specification.changed(self, originally_changed)
try:
del self._v_attrs
except AttributeError:
pass

def __contains__(self, interface):
"""Test whether an interface is in the specification
"""
Expand Down Expand Up @@ -625,9 +620,12 @@ def noLongerProvides(object, interface):


@_use_c_impl
class ClassProvidesBase(object):
# In C, this extends SpecificationBase, so its kind of weird here that it
# doesn't.
class ClassProvidesBase(SpecificationBase):

__slots__ = (
'_cls',
'_implements',
)

def __get__(self, inst, cls):
if cls is self._cls:
Expand Down Expand Up @@ -916,6 +914,8 @@ def _normalizeargs(sequence, output=None):

return output

_empty = Declaration()
# XXX: Declarations are mutable, allowing adjustments to their __bases__
# so having one as a singleton may not be a great idea.
_empty = Declaration() # type: Declaration

objectSpecificationDescriptor = ObjectSpecificationDescriptor()
58 changes: 42 additions & 16 deletions src/zope/interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,27 @@ def setTaggedValue(self, tag, value):

@_use_c_impl
class SpecificationBase(object):

# This object is the base of the inheritance hierarchy for ClassProvides:
#
# ClassProvides < ClassProvidesBase, Declaration
# Declaration < Specification < SpecificationBase
# ClassProvidesBase < SpecificationBase
#
# In order to have compatible instance layouts, we need to declare
# the storage used by Specification and Declaration here (and
# those classes must have ``__slots__ = ()``); fortunately this is
# not a waste of space because those are the only two inheritance
# trees. These all translate into tp_members in C.
__slots__ = (
# Things used here.
'_implied',
# Things used in Specification.
'dependents',
'_bases',
'_v_attrs',
'__iro__',
'__sro__',
'__weakref__',
)

def providedBy(self, ob):
Expand Down Expand Up @@ -128,6 +146,11 @@ class InterfaceBase(object):
"""Base class that wants to be replaced with a C base :)
"""

__slots__ = ()

def _call_conform(self, conform):
raise NotImplementedError

def __call__(self, obj, alternate=_marker):
"""Adapt an object to the interface
"""
Expand Down Expand Up @@ -173,14 +196,20 @@ class Specification(SpecificationBase):
Specifications are mutable. If you reassign their bases, their
relations with other specifications are adjusted accordingly.
"""
__slots__ = ()

# Copy some base class methods for speed
isOrExtends = SpecificationBase.isOrExtends
providedBy = SpecificationBase.providedBy

def __init__(self, bases=()):
self._implied = {}
self.dependents = weakref.WeakKeyDictionary()
self._bases = ()
self._implied = {}
self._v_attrs = None
self.__iro__ = ()
self.__sro__ = ()

self.__bases__ = tuple(bases)

def subscribe(self, dependent):
Expand All @@ -201,25 +230,21 @@ def __setBases(self, bases):
b.unsubscribe(self)

# Register ourselves as a dependent of our bases
self.__dict__['__bases__'] = bases
self._bases = bases
for b in bases:
b.subscribe(self)

self.changed(self)

__bases__ = property(

lambda self: self.__dict__.get('__bases__', ()),
lambda self: self._bases,
__setBases,
)

def changed(self, originally_changed):
"""We, or something we depend on, have changed
"""
try:
del self._v_attrs
except AttributeError:
pass
self._v_attrs = None

implied = self._implied
implied.clear()
Expand All @@ -245,6 +270,10 @@ def changed(self, originally_changed):
for dependent in tuple(self.dependents.keys()):
dependent.changed(originally_changed)

# Just in case something called get() at some point
# during that process and we have a cycle of some sort
# make sure we didn't cache incomplete results.
self._v_attrs = None

def interfaces(self):
"""Return an iterator for the interfaces in the specification.
Expand Down Expand Up @@ -274,9 +303,8 @@ def weakref(self, callback=None):
def get(self, name, default=None):
"""Query for an attribute description
"""
try:
attrs = self._v_attrs
except AttributeError:
attrs = self._v_attrs
if attrs is None:
attrs = self._v_attrs = {}
attr = attrs.get(name)
if attr is None:
Expand All @@ -286,10 +314,8 @@ def get(self, name, default=None):
attrs[name] = attr
break

if attr is None:
return default
else:
return attr
return default if attr is None else attr


class InterfaceClass(Element, InterfaceBase, Specification):
"""Prototype (scarecrow) Interfaces Implementation."""
Expand Down
14 changes: 7 additions & 7 deletions src/zope/interface/tests/test_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,31 +101,31 @@ def test_ctor_w_implements_in_bases(self):
def test_changed_wo_existing__v_attrs(self):
decl = self._makeOne()
decl.changed(decl) # doesn't raise
self.assertFalse('_v_attrs' in decl.__dict__)
self.assertIsNone(decl._v_attrs)

def test_changed_w_existing__v_attrs(self):
decl = self._makeOne()
decl._v_attrs = object()
decl.changed(decl)
self.assertFalse('_v_attrs' in decl.__dict__)
self.assertIsNone(decl._v_attrs)

def test___contains__w_self(self):
from zope.interface.interface import InterfaceClass
IFoo = InterfaceClass('IFoo')
decl = self._makeOne()
self.assertFalse(decl in decl)
self.assertNotIn(decl, decl)

def test___contains__w_unrelated_iface(self):
from zope.interface.interface import InterfaceClass
IFoo = InterfaceClass('IFoo')
decl = self._makeOne()
self.assertFalse(IFoo in decl)
self.assertNotIn(IFoo, decl)

def test___contains__w_base_interface(self):
from zope.interface.interface import InterfaceClass
IFoo = InterfaceClass('IFoo')
decl = self._makeOne(IFoo)
self.assertTrue(IFoo in decl)
self.assertIn(IFoo, decl)

def test___iter___empty(self):
decl = self._makeOne()
Expand Down Expand Up @@ -454,7 +454,7 @@ def __call__(self):
self.assertTrue(spec.inherit is foo)
self.assertTrue(foo.__implemented__ is spec)
self.assertTrue(foo.__providedBy__ is objectSpecificationDescriptor)
self.assertFalse('__provides__' in foo.__dict__)
self.assertNotIn('__provides__', foo.__dict__)

def test_w_None_no_bases_w_class(self):
from zope.interface.declarations import ClassProvides
Expand Down Expand Up @@ -601,7 +601,7 @@ def test_no_existing_implements(self):
class Foo(object):
__implements_advice_data__ = ((IFoo,), classImplements)
self._callFUT(Foo)
self.assertFalse('__implements_advice_data__' in Foo.__dict__)
self.assertNotIn('__implements_advice_data__', Foo.__dict__)
self.assertIsInstance(Foo.__implemented__, Implements)
self.assertEqual(list(Foo.__implemented__), [IFoo])

Expand Down
2 changes: 1 addition & 1 deletion src/zope/interface/tests/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class I(Interface):
spec._v_attrs = 'Foo'
spec._implied[I] = ()
spec.changed(spec)
self.assertTrue(getattr(spec, '_v_attrs', self) is self)
self.assertIsNone(spec._v_attrs)
self.assertFalse(I in spec._implied)

def test_interfaces_skips_already_seen(self):
Expand Down

0 comments on commit 2d98b8e

Please sign in to comment.