Skip to content

Commit

Permalink
Move Interface hashing and comparison to C; 2.5 to 15x speedup in mic…
Browse files Browse the repository at this point in the history
…ro benchmarks

Included benchmark numbers:

Current master, Python 3.8:

.....................
contains (empty dict): Mean +- std dev: 198 ns +- 5 ns
.....................
contains (populated dict): Mean +- std dev: 197 ns +- 6 ns
.....................
contains (populated list): Mean +- std dev: 53.1 us +- 1.2 us

This code:

.....................
contains (empty dict): Mean +- std dev: 77.9 ns +- 2.3 ns
.....................
contains (populated dict): Mean +- std dev: 78.4 ns +- 3.1 ns
.....................
contains (populated list): Mean +- std dev: 3.69 us +- 0.08 us

So anywhere from 2.5 to 15x faster. Not sure how that will translate to
larger benchmarks, but I'm hopeful.

It turns out that messing with ``__module__`` is nasty, tricky
business, especially when you do it from C. Everytime you define a new
subclass, the descriptors that you set get overridden by the type
machinery (PyType_Ready). I'm using a data descriptor and a meta class
right now to avoid that but I'm not super happy with that and would
like to find a better way. (At least, maybe the data part of the
descriptor isn't necessary?) It may be needed to move more code into
C, I don't want a slowdown accessing ``__module__`` either; copying
around the standard PyGetSet or PyMember descriptors isn't enough
because they don't work on the class object (so
``classImplements(InterfaceClass, IInterface)`` fails).
  • Loading branch information
jamadden committed Mar 10, 2020
1 parent 354facc commit 110f0f2
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 74 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ global-exclude coverage.xml
global-exclude appveyor.yml

prune docs/_build
prune benchmarks
42 changes: 42 additions & 0 deletions benchmarks/micro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pyperf

from zope.interface import Interface
from zope.interface.interface import InterfaceClass

ifaces = [
InterfaceClass('I' + str(i), (Interface,), {})
for i in range(100)
]

INNER = 1000

def bench_in(loops, o):
t0 = pyperf.perf_counter()
for _ in range(loops):
for _ in range(INNER):
o.__contains__(Interface)

return pyperf.perf_counter() - t0

runner = pyperf.Runner()

runner.bench_time_func(
'contains (empty dict)',
bench_in,
{},
inner_loops=INNER
)

runner.bench_time_func(
'contains (populated dict)',
bench_in,
{k: k for k in ifaces},
inner_loops=INNER
)

runner.bench_time_func(
'contains (populated list)',
bench_in,
ifaces,
inner_loops=INNER
)
212 changes: 205 additions & 7 deletions src/zope/interface/_zope_interface_coptimizations.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@

#if PY_MAJOR_VERSION >= 3
#define PY3K
#define PyNative_FromString PyUnicode_FromString
#else
#define PyNative_FromString PyString_FromString
#endif

static PyObject *str__dict__, *str__implemented__, *strextends;
Expand All @@ -45,6 +48,7 @@ static PyObject *str_uncached_subscriptions;
static PyObject *str_registry, *strro, *str_generation, *strchanged;
static PyObject *str__self__;


static PyTypeObject *Implements;

static int imported_declarations = 0;
Expand Down Expand Up @@ -709,6 +713,17 @@ __adapt__(PyObject *self, PyObject *obj)
return Py_None;
}

#ifndef PY3K
typedef long Py_hash_t;
#endif

typedef struct {
Spec spec;
PyObject* __name__;
PyObject* __module__;
Py_hash_t _v_cached_hash;
} IB;

static struct PyMethodDef ib_methods[] = {
{"__adapt__", (PyCFunction)__adapt__, METH_O,
"Adapt an object to the reciever"},
Expand Down Expand Up @@ -776,13 +791,188 @@ ib_call(PyObject *self, PyObject *args, PyObject *kwargs)
return NULL;
}


static int
IB_traverse(IB* self, visitproc visit, void* arg)
{
Py_VISIT(self->__name__);
Py_VISIT(self->__module__);
return 0;
}

static int
IB_clear(IB* self)
{
Py_CLEAR(self->__name__);
Py_CLEAR(self->__module__);
return 0;
}

static void
IB_dealloc(IB* self)
{
IB_clear(self);
Py_TYPE(self)->tp_free(OBJECT(self));
}

static PyMemberDef IB_members[] = {
{"__name__", T_OBJECT_EX, offsetof(IB, __name__), 0, ""},
{"__module__", T_OBJECT_EX, offsetof(IB, __module__), 0, ""},
{"__ibmodule__", T_OBJECT_EX, offsetof(IB, __module__), 0, ""},
{NULL}
};

static Py_hash_t
IB_hash(IB* self)
{
PyObject* tuple;
if (!self->__module__) {
PyErr_SetString(PyExc_AttributeError, "__module__");
return -1;
}
if (!self->__name__) {
PyErr_SetString(PyExc_AttributeError, "__name__");
return -1;
}

if (self->_v_cached_hash) {
return self->_v_cached_hash;
}

tuple = PyTuple_Pack(2, self->__name__, self->__module__);
if (!tuple) {
return -1;
}
self->_v_cached_hash = PyObject_Hash(tuple);
Py_CLEAR(tuple);
return self->_v_cached_hash;
}

static PyTypeObject InterfaceBaseType;

static PyObject*
IB_richcompare(IB* self, PyObject* other, int op)
{
PyObject* othername;
PyObject* othermod;
PyObject* oresult;
IB* otherib;
int result;

otherib = NULL;
oresult = othername = othermod = NULL;

if (OBJECT(self) == other) {
switch(op) {
case Py_EQ:
case Py_LE:
case Py_GE:
Py_RETURN_TRUE;
break;
case Py_NE:
Py_RETURN_FALSE;
}
}

if (other == Py_None) {
switch(op) {
case Py_LT:
case Py_LE:
case Py_NE:
Py_RETURN_TRUE;
default:
Py_RETURN_FALSE;
}
}

if (PyObject_TypeCheck(other, &InterfaceBaseType)) {
otherib = (IB*)other;
othername = otherib->__name__;
othermod = otherib->__module__;
}
else {
othername = PyObject_GetAttrString(other, "__name__");
// TODO: Optimize this case.
if (othername == NULL) {
PyErr_Clear();
othername = PyNative_FromString("");
}
othermod = PyObject_GetAttrString(other, "__module__");
if (othermod == NULL) {
PyErr_Clear();
othermod = PyNative_FromString("");
}
}
#if 0
// This is the simple, straightforward version of what Python does.
PyObject* pt1 = PyTuple_Pack(2, self->__name__, self->__module__);
PyObject* pt2 = PyTuple_Pack(2, othername, othermod);
oresult = PyObject_RichCompare(pt1, pt2, op);
#endif

// tuple comparison is decided by the first non-equal element.
result = PyObject_RichCompareBool(self->__name__, othername, Py_EQ);
if (result == 0) {
result = PyObject_RichCompareBool(self->__name__, othername, op);
}
else if (result == 1) {
result = PyObject_RichCompareBool(self->__module__, othermod, op);
}
if (result == -1) {
goto cleanup;
}

oresult = result ? Py_True : Py_False;
Py_INCREF(oresult);


cleanup:
if (!otherib) {
Py_XDECREF(othername);
Py_XDECREF(othermod);
}
return oresult;

}

static PyObject*
IB_module_get(IB* self, void* context)
{
return self->__module__;
}

static int
IB_module_set(IB* self, PyObject* value, void* context)
{
Py_XINCREF(value);
Py_XDECREF(self->__module__);
self->__module__ = value;
return 0;
}

static int
IB_init(IB* self, PyObject* args, PyObject* kwargs)
{
IB_clear(self);
self->__module__ = Py_None;
Py_INCREF(self->__module__);
self->__name__ = Py_None;
Py_INCREF(self->__name__);
return 0;
}

static PyGetSetDef IB_getsets[] = {
{"__module__", (getter)IB_module_get, (setter)IB_module_set, 0, NULL},
{NULL}
};

static PyTypeObject InterfaceBaseType = {
PyVarObject_HEAD_INIT(NULL, 0)
/* tp_name */ "_zope_interface_coptimizations."
"InterfaceBase",
/* tp_basicsize */ 0,
/* tp_basicsize */ sizeof(IB),
/* tp_itemsize */ 0,
/* tp_dealloc */ (destructor)0,
/* tp_dealloc */ (destructor)IB_dealloc,
/* tp_print */ (printfunc)0,
/* tp_getattr */ (getattrfunc)0,
/* tp_setattr */ (setattrfunc)0,
Expand All @@ -791,22 +981,30 @@ static PyTypeObject InterfaceBaseType = {
/* tp_as_number */ 0,
/* tp_as_sequence */ 0,
/* tp_as_mapping */ 0,
/* tp_hash */ (hashfunc)0,
/* tp_hash */ (hashfunc)IB_hash,
/* tp_call */ (ternaryfunc)ib_call,
/* tp_str */ (reprfunc)0,
/* tp_getattro */ (getattrofunc)0,
/* tp_setattro */ (setattrofunc)0,
/* tp_as_buffer */ 0,
/* tp_flags */ Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_BASETYPE ,
| Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
/* tp_doc */ "Interface base type providing __call__ and __adapt__",
/* tp_traverse */ (traverseproc)0,
/* tp_clear */ (inquiry)0,
/* tp_richcompare */ (richcmpfunc)0,
/* tp_traverse */ (traverseproc)IB_traverse,
/* tp_clear */ (inquiry)IB_clear,
/* tp_richcompare */ (richcmpfunc)IB_richcompare,
/* tp_weaklistoffset */ (long)0,
/* tp_iter */ (getiterfunc)0,
/* tp_iternext */ (iternextfunc)0,
/* tp_methods */ ib_methods,
/* tp_members */ IB_members,
/* tp_getset */ IB_getsets,
/* tp_base */ &SpecType,
/* tp_dict */ 0,
/* tp_descr_get */ 0,
/* tp_descr_set */ 0,
/* tp_dictoffset */ 0,
/* tp_init */ (initproc)IB_init,
};

/* =================== End: __call__ and __adapt__ ==================== */
Expand Down
Loading

0 comments on commit 110f0f2

Please sign in to comment.