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:
| Benchmark | 39 | 310 | 311 | 312 | 313 | 314 | 315 |
|---|
| decimal_factorial | 804 ms | 843 ms: 1.05x slower | 763 ms: 1.05x faster | 794 ms: 1.01x faster | 1.02 sec: 1.27x slower | 950 ms: 1.18x slower | 951 ms: 1.18x slower |
| decimal_pi | 1.12 sec | 1.26 sec: 1.13x slower | 1.29 sec: 1.15x slower | 1.31 sec: 1.17x slower | 1.86 sec: 1.67x slower | 1.66 sec: 1.48x slower | 1.66 sec: 1.49x slower |
| Geometric mean | (ref) | 1.09x slower | 1.05x slower | 1.07x slower | 1.45x slower | 1.32x slower | 1.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):
| Benchmark | 39 | 313 |
|---|
| decimal_pi | 1.20 sec | 1.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:
| Benchmark | ref | heap |
|---|
| xyz(123) + xyz(321) | 204 ns | 357 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.
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:
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):
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:
code and benchmark
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.