Skip to content

Speed regression in the decimal module due to using heap types #144650

Description

@skirpichev

This is essentially a rebirth of #114682, which was closed due to @skrah ban (unfortunately, such things usually don't fix issues).

The problem seems to be valid and I open a new issue per kindly @gpshead permission. I labeled issue as "type-feature", but IMO it's looks rather as a regression, i.e. "type-bug".

Here benchmarks results (with PGO) for 3.9-3.15:

Benchmark39310311312313314315
decimal_factorial804 ms843 ms: 1.05x slower763 ms: 1.05x faster794 ms: 1.01x faster1.02 sec: 1.27x slower950 ms: 1.18x slower951 ms: 1.18x slower
decimal_pi1.12 sec1.26 sec: 1.13x slower1.29 sec: 1.15x slower1.31 sec: 1.17x slower1.86 sec: 1.67x slower1.66 sec: 1.48x slower1.66 sec: 1.49x slower
Geometric mean(ref)1.09x slower1.05x slower1.07x slower1.45x slower1.32x slower1.33x slower

Benchmarks were available in the pyperformance package long time ago but are disabled, see python/pyperformance#453 (I use first commit from above PR to enable both benchmarks. Though, the "decimal_factorial" benchmark seems to be less affected, I guess because it has more large numbers).

N.B.: the "decimal_pi" benchmark do computation with low precision (9 and 19). Here is what happens with the default precision (28):

Benchmark39313
decimal_pi1.20 sec1.99 sec: 1.65x slower

Not much better.

It seems that major slowdown come in the 3.13 (around 1.67x slower for "decimal_pi" on my system) with #106079. Unfortunately, nobody asked to do performance measurements during PR review :(

Something like this happens already on toy extension types:

Benchmarkrefheap
xyz(123) + xyz(321)204 ns357 ns: 1.75x slower
code and benchmark
# bench.pyimportpyperffromoperatorimportaddfromexampleimportxyzx, y=map(xyz, [123, 321])
runner=pyperf.Runner()
s=repr(x) +" + "+repr(y)
runner.bench_func(s, add, x, y)
/* static type */#definePY_SSIZE_T_CLEAN#include<Python.h>typedefstruct {
PyObject_HEAD longvalue;
} XYZ_Object;
PyTypeObjectXYZ_Type;
#defineXYZ_CheckExact(u) Py_IS_TYPE((u), &XYZ_Type)
staticXYZ_Object*XYZ_new(longvalue)
{
XYZ_Object*res=PyObject_New(XYZ_Object, &XYZ_Type);
if (res) {
res->value=value;
}
returnres;
}
staticPyObject*new(PyTypeObject*type, PyObject*args, PyObject*keywds)
{
Py_ssize_targc=PyTuple_GET_SIZE(args);
if (argc==1) {
PyObject*arg=PyTuple_GET_ITEM(args, 0);
longvalue=PyLong_AsLong(arg);
if (value==-1&&PyErr_Occurred()) {
returnNULL;
}
return (PyObject*)XYZ_new(value);
}
PyErr_SetString(PyExc_TypeError, "value required");
returnNULL;
}
staticPyObject*add(PyObject*self, PyObject*other)
{
XYZ_Object*x= (XYZ_Object*)self;
XYZ_Object*y= (XYZ_Object*)other;
if (XYZ_CheckExact(x) &&XYZ_CheckExact(y)) {
return (PyObject*)XYZ_new(x->value+y->value);
}
Py_RETURN_NOTIMPLEMENTED;
}
staticPyObject*repr(PyObject*self)
{
returnPyUnicode_FromFormat("xyz(%ld)", ((XYZ_Object*)self)->value);
}
staticPyNumberMethodsxyz_as_number= {
.nb_add=add,
};
PyTypeObjectXYZ_Type= {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name="xyz",
.tp_basicsize=sizeof(XYZ_Object),
.tp_new=new,
.tp_repr=repr,
.tp_as_number=&xyz_as_number,
.tp_flags=Py_TPFLAGS_DEFAULT,
};
staticintexample_exec(PyObject*module)
{
if (PyModule_AddType(module, &XYZ_Type) <0) {
return-1;
}
return0;
}
#ifdef__GNUC__# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpedantic"
#endifstaticPyModuleDef_Slotexample_slots[] = {
{Py_mod_exec, example_exec},
{0, NULL}};
#ifdef__GNUC__# pragma GCC diagnostic pop
#endifstaticstructPyModuleDefexample_module= {
PyModuleDef_HEAD_INIT,
.m_name="example",
.m_doc="Test module.",
.m_size=0,
.m_slots=example_slots,
};
PyMODINIT_FUNCPyInit_example(void)
{
returnPyModuleDef_Init(&example_module);
}
/* heap type */#definePY_SSIZE_T_CLEAN#include<Python.h>typedefstruct {
PyObject_HEAD longvalue;
} XYZ_Object;
typedefstruct {
PyTypeObject*XYZ_Type;
} example_state;
#defineXYZ_CheckExact(st, u) Py_IS_TYPE((u), (st)->XYZ_Type)
staticXYZ_Object*XYZ_new(example_state*Py_UNUSED(state), PyTypeObject*type, longvalue)
{
XYZ_Object*res=PyObject_GC_New(XYZ_Object, type);
if (res) {
PyObject_GC_Track((PyObject*)res);
res->value=value;
}
returnres;
}
staticstructPyModuleDefexample_module;
staticexample_state*get_state(PyTypeObject*type)
{
PyObject*module=PyType_GetModuleByDef(type, &example_module);
returnPyModule_GetState(module);
}
staticPyObject*new(PyTypeObject*type, PyObject*args, PyObject*keywds)
{
Py_ssize_targc=PyTuple_GET_SIZE(args);
example_state*state=get_state(type);
if (argc==1) {
PyObject*arg=PyTuple_GET_ITEM(args, 0);
longvalue=PyLong_AsLong(arg);
if (value==-1&&PyErr_Occurred()) {
returnNULL;
}
return (PyObject*)XYZ_new(state, type, value);
}
PyErr_SetString(PyExc_TypeError, "value required");
returnNULL;
}
staticinttraverse(PyObject*self, visitprocvisit, void*arg)
{
Py_VISIT(Py_TYPE(self));
return0;
}
staticvoiddealloc(PyObject*self)
{
PyTypeObject*type=Py_TYPE(self);
PyObject_GC_UnTrack(self);
type->tp_free(self);
Py_DECREF(type);
}
staticinlineexample_state*find_state_left_or_right(PyObject*left, PyObject*right)
{
PyObject*module=PyType_GetModuleByDef(Py_TYPE(left), &example_module);
if (module) {
returnPyModule_GetState(module);
}
PyErr_Clear();
returnPyType_GetModuleState(Py_TYPE(right));
}
staticPyObject*add(PyObject*self, PyObject*other)
{
example_state*state=find_state_left_or_right(self, other);
XYZ_Object*x= (XYZ_Object*)self;
XYZ_Object*y= (XYZ_Object*)other;
if (XYZ_CheckExact(state, x) &&XYZ_CheckExact(state, y)) {
return (PyObject*)XYZ_new(state, state->XYZ_Type, x->value+y->value);
}
Py_RETURN_NOTIMPLEMENTED;
}
staticPyObject*repr(PyObject*self)
{
returnPyUnicode_FromFormat("xyz(%ld)", ((XYZ_Object*)self)->value);
}
#ifdef__GNUC__# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpedantic"
#endifstaticPyType_Slotxyz_slots[] = {
{Py_tp_repr, repr},
{Py_tp_new, new},
{Py_nb_add, add},
{Py_tp_traverse, traverse},
{Py_tp_dealloc, dealloc},
};
#ifdef__GNUC__# pragma GCC diagnostic pop
#endifstaticPyType_Specxyz_spec= {
.name="xyz",
.basicsize=sizeof(XYZ_Object),
.flags=Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE,
.slots=xyz_slots,
};
staticintexample_exec(PyObject*module)
{
example_state*state=PyModule_GetState(module);
state->XYZ_Type= (PyTypeObject*)PyType_FromModuleAndSpec(module,
&xyz_spec,
NULL);
if (!state->XYZ_Type||PyModule_AddType(module, state->XYZ_Type) <0) {
return-1;
}
return0;
}
staticintexample_clear(PyObject*module)
{
example_state*state=PyModule_GetState(module);
Py_CLEAR(state->XYZ_Type);
return0;
}
staticintexample_traverse(PyObject*module, visitprocvisit, void*arg)
{
example_state*state=PyModule_GetState(module);
Py_VISIT(state->XYZ_Type);
return0;
}
#ifdef__GNUC__# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpedantic"
#endifstaticPyModuleDef_Slotexample_slots[] = {
{Py_mod_exec, example_exec},
{0, NULL}};
#ifdef__GNUC__# pragma GCC diagnostic pop
#endifstaticstructPyModuleDefexample_module= {
PyModuleDef_HEAD_INIT,
.m_name="example",
.m_doc="Test module.",
.m_size=0,
.m_slots=example_slots,
.m_clear=example_clear,
.m_traverse=example_traverse,
};
PyMODINIT_FUNCPyInit_example(void)
{
returnPyModuleDef_Init(&example_module);
}

So, what we could do? Can heap types (at least immutable) be less costly c.f. static types? Can we convert the decimal module back to static types?

I tried to add a freelist for Decimal's (quick patch is there: skirpichev#16). It looks this will mitigate the problem, but not entirely fix regression.

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions