Speed regression in the decimal module due to using heap types
还没有人认领这个 Issue。
- 主要语言
- Python
- 星标
- 77.2k
- 派生
- 35.9k
- PR 合并指标
- PR 指标待抓取
描述
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 https://github.com/python/pyperformance/pull/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 https://github.com/python/cpython/pull/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.py
import pyperf
from operator import add
from example import xyz
x, y = map(xyz, [123, 321])
runner = pyperf.Runner()
s = repr(x) + " + " + repr(y)
runner.bench_func(s, add, x, y)
/* static type */
#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
PyObject_HEAD
long value;
} XYZ_Object;
PyTypeObject XYZ_Type;
#define XYZ_CheckExact(u) Py_IS_TYPE((u), &XYZ_Type)
static XYZ_Object *
XYZ_new(long value)
{
XYZ_Object *res = PyObject_New(XYZ_Object, &XYZ_Type);
if (res) {
res->value = value;
}
return res;
}
static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject *keywds)
{
Py_ssize_t argc = PyTuple_GET_SIZE(args);
if (argc == 1) {
PyObject *arg = PyTuple_GET_ITEM(args, 0);
long value = PyLong_AsLong(arg);
if (value == -1 && PyErr_Occurred()) {
return NULL;
}
return (PyObject *)XYZ_new(value);
}
PyErr_SetString(PyExc_TypeError, "value required");
return NULL;
}
static PyObject *
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;
}
static PyObject *
repr(PyObject *self)
{
return PyUnicode_FromFormat("xyz(%ld)", ((XYZ_Object *)self)->value);
}
static PyNumberMethods xyz_as_number = {
.nb_add = add,
};
PyTypeObject XYZ_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,
};
static int
example_exec(PyObject *module)
{
if (PyModule_AddType(module, &XYZ_Type) < 0) {
return -1;
}
return 0;
}
#ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpedantic"
#endif
static PyModuleDef_Slot example_slots[] = {
{Py_mod_exec, example_exec},
{0, NULL}};
#ifdef __GNUC__
# pragma GCC diagnostic pop
#endif
static struct PyModuleDef example_module = {
PyModuleDef_HEAD_INIT,
.m_name = "example",
.m_doc = "Test module.",
.m_size = 0,
.m_slots = example_slots,
};
PyMODINIT_FUNC
PyInit_example(void)
{
return PyModuleDef_Init(&example_module);
}
/* heap type */
#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
PyObject_HEAD
long value;
} XYZ_Object;
typedef struct {
PyTypeObject *XYZ_Type;
} example_state;
#define XYZ_CheckExact(st, u) Py_IS_TYPE((u), (st)->XYZ_Type)
static XYZ_Object *
XYZ_new(example_state *Py_UNUSED(state), PyTypeObject *type, long value)
{
XYZ_Object *res = PyObject_GC_New(XYZ_Object, type);
if (res) {
PyObject_GC_Track((PyObject *)res);
res->value = value;
}
return res;
}
static struct PyModuleDef example_module;
static example_state *
get_state(PyTypeObject *type)
{
PyObject *module = PyType_GetModuleByDef(type, &example_module);
return PyModule_GetState(module);
}
static PyObject *
new(PyTypeObject *type, PyObject *args, PyObject *keywds)
{
Py_ssize_t argc = PyTuple_GET_SIZE(args);
example_state *state = get_state(type);
if (argc == 1) {
PyObject *arg = PyTuple_GET_ITEM(args, 0);
long value = PyLong_AsLong(arg);
if (value == -1 && PyErr_Occurred()) {
return NULL;
}
return (PyObject *)XYZ_new(state, type, value);
}
PyErr_SetString(PyExc_TypeError, "value required");
return NULL;
}
static int
traverse(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(Py_TYPE(self));
return 0;
}
static void
dealloc(PyObject *self)
{
PyTypeObject *type = Py_TYPE(self);
PyObject_GC_UnTrack(self);
type->tp_free(self);
Py_DECREF(type);
}
static inline example_state *
find_state_left_or_right(PyObject *left, PyObject *right)
{
PyObject *module = PyType_GetModuleByDef(Py_TYPE(left), &example_module);
if (module) {
return PyModule_GetState(module);
}
PyErr_Clear();
return PyType_GetModuleState(Py_TYPE(right));
}
static PyObject *
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;
}
static PyObject *
repr(PyObject *self)
{
return PyUnicode_FromFormat("xyz(%ld)", ((XYZ_Object *)self)->value);
}
#ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpedantic"
#endif
static PyType_Slot xyz_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
#endif
static PyType_Spec xyz_spec = {
.name = "xyz",
.basicsize = sizeof(XYZ_Object),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE,
.slots = xyz_slots,
};
static int
example_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;
}
return 0;
}
static int
example_clear(PyObject *module)
{
example_state *state = PyModule_GetState(module);
Py_CLEAR(state->XYZ_Type);
return 0;
}
static int
example_traverse(PyObject *module, visitproc visit, void *arg)
{
example_state *state = PyModule_GetState(module);
Py_VISIT(state->XYZ_Type);
return 0;
}
#ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wpedantic"
#endif
static PyModuleDef_Slot example_slots[] = {
{Py_mod_exec, example_exec},
{0, NULL}};
#ifdef __GNUC__
# pragma GCC diagnostic pop
#endif
static struct PyModuleDef example_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_FUNC
PyInit_example(void)
{
return PyModuleDef_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: https://github.com/skirpichev/cpython/pull/16). It looks this will mitigate the problem, but not entirely fix regression.
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
首先,使用 pyperformance 基准测试和 toy 静态与堆扩展示例,复现 issue 中描述的 decimal_pi 和 decimal_factorial 结果。阅读与 CPython PR 106079 相关的 decimal 模块更改,并比较所提议的 freelist 补丁。完成的标准是识别出 regression 机制,并验证经过测量的缓解措施或修复,同时不使基准测试结果发生 regression。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- c, python
- 领域
- performance
- Issue 类型
- 缺陷
- 难度
- 5/5
- 预计耗时
- 一周以上
- 活跃度
- 冷清
- 描述清晰度
- 需要澄清
- 新手友好度
- 32/100