Skip to content

Commit a304b25

Browse files
committed
bpo-42917: Made block stack for frame objects dynamically sizable
This removes the hardcoded block stack size of 20 since typical functions have far less than 20 nested blocks. When running an empty program, 256 frame objects get created with an old average size of 420.281 bytes. This change reduces the average size to less than half: 197.000 bytes on x86_64. The goal is not necessarily to reduce memory usage, but to make it more likely for stack frames to stay in L1 cache. In addition, the maximum of 20 blocks is removed. This should make it easier to write code generators that produce deeply nested code.
1 parent 5e29021 commit a304b25

19 files changed

+4554
-4351
lines changed

Doc/c-api/code.rst

+11-5
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,23 @@ bound into a function.
3535
3636
.. c:function:: PyCodeObject* PyCode_New(int argcount, int kwonlyargcount, int nlocals, int stacksize, int flags, PyObject *code, PyObject *consts, PyObject *names, PyObject *varnames, PyObject *freevars, PyObject *cellvars, PyObject *filename, PyObject *name, int firstlineno, PyObject *lnotab)
3737
38-
Return a new code object. If you need a dummy code object to create a frame,
39-
use :c:func:`PyCode_NewEmpty` instead. Calling :c:func:`PyCode_New` directly
40-
can bind you to a precise Python version since the definition of the bytecode
41-
changes often.
38+
New code should use PyCode_NewWithBlockStackSize instead.
4239
4340
.. c:function:: PyCodeObject* PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount, int nlocals, int stacksize, int flags, PyObject *code, PyObject *consts, PyObject *names, PyObject *varnames, PyObject *freevars, PyObject *cellvars, PyObject *filename, PyObject *name, int firstlineno, PyObject *lnotab)
4441
45-
Similar to :c:func:`PyCode_New`, but with an extra "posonlyargcount" for positional-only arguments.
42+
New code should use PyCode_NewWithBlockStackSize instead.
4643
4744
.. versionadded:: 3.8
4845
46+
.. c:function:: PyCodeObject* PyCode_NewWithBlockStackSize(int argcount, int posonlyargcount, int kwonlyargcount, int nlocals, int stacksize, int blockstacksize, int flags, PyObject *code, PyObject *consts, PyObject *names, PyObject *varnames, PyObject *freevars, PyObject *cellvars, PyObject *filename, PyObject *name, int firstlineno, PyObject *lnotab)
47+
48+
Return a new code object. If you need a dummy code object to create a frame,
49+
use :c:func:`PyCode_NewEmpty` instead. Calling :c:func:`PyCode_New` directly
50+
can bind you to a precise Python version since the definition of the bytecode
51+
changes often.
52+
53+
.. versionadded:: 3.10
54+
4955
.. c:function:: PyCodeObject* PyCode_NewEmpty(const char *filename, const char *funcname, int firstlineno)
5056
5157
Return a new empty code object with the specified filename,

Doc/data/refcounts.dat

+19
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,25 @@ PyCode_NewWithPosOnlyArgs:PyObject*:name:0:
252252
PyCode_NewWithPosOnlyArgs:int:firstlineno::
253253
PyCode_NewWithPosOnlyArgs:PyObject*:lnotab:0:
254254

255+
PyCode_NewWithBlockStackSize:PyCodeObject*::+1:
256+
PyCode_NewWithBlockStackSize:int:argcount::
257+
PyCode_NewWithBlockStackSize:int:posonlyargcount::
258+
PyCode_NewWithBlockStackSize:int:kwonlyargcount::
259+
PyCode_NewWithBlockStackSize:int:nlocals::
260+
PyCode_NewWithBlockStackSize:int:stacksize::
261+
PyCode_NewWithBlockStackSize:int:blockstacksize::
262+
PyCode_NewWithBlockStackSize:int:flags::
263+
PyCode_NewWithBlockStackSize:PyObject*:code:0:
264+
PyCode_NewWithBlockStackSize:PyObject*:consts:0:
265+
PyCode_NewWithBlockStackSize:PyObject*:names:0:
266+
PyCode_NewWithBlockStackSize:PyObject*:varnames:0:
267+
PyCode_NewWithBlockStackSize:PyObject*:freevars:0:
268+
PyCode_NewWithBlockStackSize:PyObject*:cellvars:0:
269+
PyCode_NewWithBlockStackSize:PyObject*:filename:0:
270+
PyCode_NewWithBlockStackSize:PyObject*:name:0:
271+
PyCode_NewWithBlockStackSize:int:firstlineno::
272+
PyCode_NewWithBlockStackSize:PyObject*:lnotab:0:
273+
255274
PyCode_New:PyCodeObject*::+1:
256275
PyCode_New:int:argcount::
257276
PyCode_New:int:kwonlyargcount::

Doc/whatsnew/3.10.rst

+15-1
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,9 @@ Other Language Changes
578578
``__globals__["__builtins__"]`` if it exists, else from the current builtins.
579579
(Contributed by Mark Shannon in :issue:`42990`.)
580580
581+
* The block stack size for :class:`frame` objects is now unlimited (previously
582+
was limited to 20).
583+
(Contributed by Thomas Anderson in :issue:`42917`.)
581584
582585
New Modules
583586
===========
@@ -914,7 +917,6 @@ Optimizations
914917
for more details. (Contributed by Victor Stinner and Pablo Galindo in
915918
:issue:`38980`.)
916919
917-
918920
* Function parameters and their annotations are no longer computed at runtime,
919921
but rather at compilation time. They are stored as a tuple of strings at the
920922
bytecode level. It is now around 2 times faster to create a function with
@@ -926,6 +928,11 @@ Optimizations
926928
algorithm to avoid quadratic behavior on long strings. (Contributed
927929
by Dennis Sweeney in :issue:`41972`)
928930
931+
* Reduced size of stack :class:`frame` objects by more than half for typical
932+
functions.
933+
(Contributed by Thomas Anderson in :issue:`42917`.)
934+
935+
929936
Deprecated
930937
==========
931938
@@ -1112,6 +1119,9 @@ Changes in the Python API
11121119
also inherits the current builtins.
11131120
(Contributed by Victor Stinner in :issue:`42990`.)
11141121
1122+
* :meth:`code.__new__` now takes a *co_blockstacksize* argument.
1123+
(Contributed by Thomas Anderson in :issue:`42917`.)
1124+
11151125
CPython bytecode changes
11161126
========================
11171127
@@ -1279,6 +1289,10 @@ Porting to Python 3.10
12791289
been included directly, consider including ``Python.h`` instead.
12801290
(Contributed by Nicholas Sim in :issue:`35134`)
12811291
1292+
* Usages of :c:func:`PyCode_New` and :c:func:`PyCode_NewWithPosOnlyArgs` should
1293+
be updated to use :c:func:`PyCode_NewWithBlockSize`.
1294+
(Contributed by Thomas Anderson in :issue:`42917`.)
1295+
12821296
Deprecated
12831297
----------
12841298

Include/cpython/code.h

+12-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ struct PyCodeObject {
2222
int co_kwonlyargcount; /* #keyword only arguments */
2323
int co_nlocals; /* #local variables */
2424
int co_stacksize; /* #entries needed for evaluation stack */
25+
int co_blockstacksize; /* #entries needed for block stack */
2526
int co_flags; /* CO_..., see below */
2627
int co_firstlineno; /* first source line number */
2728
PyObject *co_code; /* instruction opcodes */
@@ -106,24 +107,32 @@ struct PyCodeObject {
106107
*/
107108
#define PY_PARSER_REQUIRES_FUTURE_KEYWORD
108109

109-
#define CO_MAXBLOCKS 20 /* Max static block nesting within a function */
110-
111110
PyAPI_DATA(PyTypeObject) PyCode_Type;
112111

113112
#define PyCode_Check(op) Py_IS_TYPE(op, &PyCode_Type)
114113
#define PyCode_GetNumFree(op) (PyTuple_GET_SIZE((op)->co_freevars))
115114

116115
/* Public interface */
116+
117+
/* Used in 3.7 and earlier. New code should use PyCode_NewWithBlockStackSize
118+
instead. */
117119
PyAPI_FUNC(PyCodeObject *) PyCode_New(
118120
int, int, int, int, int, PyObject *, PyObject *,
119121
PyObject *, PyObject *, PyObject *, PyObject *,
120122
PyObject *, PyObject *, int, PyObject *);
121123

124+
/* Used between 3.8 and 3.9. New code should use PyCode_NewWithBlockStackSize
125+
instead. */
122126
PyAPI_FUNC(PyCodeObject *) PyCode_NewWithPosOnlyArgs(
123127
int, int, int, int, int, int, PyObject *, PyObject *,
124128
PyObject *, PyObject *, PyObject *, PyObject *,
125129
PyObject *, PyObject *, int, PyObject *);
126-
/* same as struct above */
130+
131+
/* Used in 3.10 and later */
132+
PyAPI_FUNC(PyCodeObject *) PyCode_NewWithBlockStackSize(
133+
int, int, int, int, int, int, int, PyObject *, PyObject *,
134+
PyObject *, PyObject *, PyObject *, PyObject *,
135+
PyObject *, PyObject *, int, PyObject *);
127136

128137
/* Creates a new empty code object with the specified source location. */
129138
PyAPI_FUNC(PyCodeObject *)

Include/cpython/frameobject.h

+28-2
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,34 @@ struct _frame {
4545
int f_lineno; /* Current line number. Only valid if non-zero */
4646
int f_iblock; /* index in f_blockstack */
4747
PyFrameState f_state; /* What state the frame is in */
48-
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
49-
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
48+
PyTryBlock *f_blockstack; /* points after the value stack */
49+
PyObject *f_localsplus[1]; /* locals+stacks, dynamically sized */
50+
/* The size of a PyFrameObject depends on the number of local
51+
* variables and the size of the value and block stacks.
52+
* _________________________________
53+
* | GC Header |
54+
* PyFrameObject * -> |---------------------------------|
55+
* | Fixed size PyFrameObject fields |
56+
* | |
57+
* | PyVarObject ob_base |
58+
* | ... |
59+
* | PyTryBlock* f_blockstack |
60+
* PyObject *f_localsplus -> |---------------------------------|
61+
* | Dynamically sized locals |
62+
* | |
63+
* | Local variables |
64+
* | Cell variables |
65+
* | Free variables |
66+
* PyObject **f_valuestack -> |---------------------------------|
67+
* | Dynamically sized value stack |
68+
* | | |
69+
* | V |
70+
* PyTryBlock *f_blockstack -> |---------------------------------|
71+
* | Dynamically sized block stack |
72+
* | | |
73+
* | V |
74+
* |_________________________________|
75+
*/
5076
};
5177

5278
static inline int _PyFrame_IsRunnable(struct _frame *f) {

Lib/ctypes/test/test_values.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ class struct_frozen(Structure):
8080
continue
8181
items.append((entry.name.decode("ascii"), entry.size))
8282

83-
expected = [("__hello__", 139),
84-
("__phello__", -139),
85-
("__phello__.spam", 139),
83+
expected = [("__hello__", 143),
84+
("__phello__", -143),
85+
("__phello__.spam", 143),
8686
]
8787
self.assertEqual(items, expected, "PyImport_FrozenModules example "
8888
"in Doc/library/ctypes.rst may be out of date")

Lib/test/test_code.py

+1
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ def func(): pass
218218
co.co_kwonlyargcount,
219219
co.co_nlocals,
220220
co.co_stacksize,
221+
co.co_blockstacksize,
221222
co.co_flags,
222223
co.co_code,
223224
co.co_consts,

Lib/test/test_syntax.py

-39
Original file line numberDiff line numberDiff line change
@@ -571,35 +571,6 @@
571571
This raises a SyntaxError, it used to raise a SystemError.
572572
Context for this change can be found on issue #27514
573573
574-
In 2.5 there was a missing exception and an assert was triggered in a debug
575-
build. The number of blocks must be greater than CO_MAXBLOCKS. SF #1565514
576-
577-
>>> while 1:
578-
... while 2:
579-
... while 3:
580-
... while 4:
581-
... while 5:
582-
... while 6:
583-
... while 8:
584-
... while 9:
585-
... while 10:
586-
... while 11:
587-
... while 12:
588-
... while 13:
589-
... while 14:
590-
... while 15:
591-
... while 16:
592-
... while 17:
593-
... while 18:
594-
... while 19:
595-
... while 20:
596-
... while 21:
597-
... while 22:
598-
... break
599-
Traceback (most recent call last):
600-
...
601-
SyntaxError: too many statically nested blocks
602-
603574
Misuse of the nonlocal and global statement can lead to a few unique syntax errors.
604575
605576
>>> def f():
@@ -1117,16 +1088,6 @@ def test_empty_line_after_linecont(self):
11171088
except SyntaxError:
11181089
self.fail("Empty line after a line continuation character is valid.")
11191090

1120-
@support.cpython_only
1121-
def test_nested_named_except_blocks(self):
1122-
code = ""
1123-
for i in range(12):
1124-
code += f"{' '*i}try:\n"
1125-
code += f"{' '*(i+1)}raise Exception\n"
1126-
code += f"{' '*i}except Exception as e:\n"
1127-
code += f"{' '*4*12}pass"
1128-
self._check_error(code, "too many statically nested blocks")
1129-
11301091
def test_barry_as_flufl_with_syntax_errors(self):
11311092
# The "barry_as_flufl" rule can produce some "bugs-at-a-distance" if
11321093
# is reading the wrong token in the presence of syntax errors later

Lib/test/test_sys.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1271,13 +1271,13 @@ class C(object): pass
12711271
check(sys.float_info, vsize('') + self.P * len(sys.float_info))
12721272
# frame
12731273
import inspect
1274-
CO_MAXBLOCKS = 20
12751274
x = inspect.currentframe()
12761275
ncells = len(x.f_code.co_cellvars)
12771276
nfrees = len(x.f_code.co_freevars)
1277+
nblocks = x.f_code.co_blockstacksize
12781278
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
1279-
ncells + nfrees - 1
1280-
check(x, vsize('4Pi2c4P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
1279+
ncells + nfrees
1280+
check(x, vsize('8Pi2cP3icP' + extras*'P' + nblocks*'3i'))
12811281
# function
12821282
def func(): pass
12831283
check(func, size('14P'))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make block stack for frame objects dynamically sizable and lift the old limit of 20 blocks.

0 commit comments

Comments
 (0)