Skip to content

Commit ae0a2b7

Browse files
authored
bpo-44590: Lazily allocate frame objects (GH-27077)
* Convert "specials" array to InterpreterFrame struct, adding f_lasti, f_state and other non-debug FrameObject fields to it. * Refactor, calls pushing the call to the interpreter upward toward _PyEval_Vector. * Compute f_back when on thread stack, only filling in value when frame object outlives stack invocation. * Move ownership of InterpreterFrame in generator from frame object to generator object. * Do not create frame objects for Python calls. * Do not create frame objects for generators.
1 parent 0363a40 commit ae0a2b7

27 files changed

+1036
-618
lines changed

Include/cpython/ceval.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ PyAPI_FUNC(PyObject *) _PyEval_GetBuiltinId(_Py_Identifier *);
1919
flag was set, else return 0. */
2020
PyAPI_FUNC(int) PyEval_MergeCompilerFlags(PyCompilerFlags *cf);
2121

22-
PyAPI_FUNC(PyObject *) _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int exc);
22+
PyAPI_FUNC(PyObject *) _PyEval_EvalFrameDefault(PyThreadState *tstate, struct _interpreter_frame *f, int exc);
2323

2424
PyAPI_FUNC(void) _PyEval_SetSwitchInterval(unsigned long microseconds);
2525
PyAPI_FUNC(unsigned long) _PyEval_GetSwitchInterval(void);

Include/cpython/frameobject.h

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,17 @@
44
# error "this header file must not be included directly"
55
#endif
66

7-
/* These values are chosen so that the inline functions below all
8-
* compare f_state to zero.
9-
*/
10-
enum _framestate {
11-
FRAME_CREATED = -2,
12-
FRAME_SUSPENDED = -1,
13-
FRAME_EXECUTING = 0,
14-
FRAME_RETURNED = 1,
15-
FRAME_UNWINDING = 2,
16-
FRAME_RAISED = 3,
17-
FRAME_CLEARED = 4
18-
};
19-
20-
typedef signed char PyFrameState;
21-
227
struct _frame {
238
PyObject_HEAD
249
struct _frame *f_back; /* previous frame, or NULL */
25-
PyObject **f_valuestack; /* points after the last local */
10+
struct _interpreter_frame *f_frame; /* points to the frame data */
2611
PyObject *f_trace; /* Trace function */
27-
/* Borrowed reference to a generator, or NULL */
28-
PyObject *f_gen;
29-
int f_stackdepth; /* Depth of value stack */
30-
int f_lasti; /* Last instruction if called */
3112
int f_lineno; /* Current line number. Only valid if non-zero */
32-
PyFrameState f_state; /* What state the frame is in */
3313
char f_trace_lines; /* Emit per-line trace events? */
3414
char f_trace_opcodes; /* Emit per-opcode trace events? */
3515
char f_own_locals_memory; /* This frame owns the memory for the locals */
36-
PyObject **f_localsptr; /* Pointer to locals, cells, free */
3716
};
3817

39-
static inline int _PyFrame_IsRunnable(struct _frame *f) {
40-
return f->f_state < FRAME_EXECUTING;
41-
}
42-
43-
static inline int _PyFrame_IsExecuting(struct _frame *f) {
44-
return f->f_state == FRAME_EXECUTING;
45-
}
46-
47-
static inline int _PyFrameHasCompleted(struct _frame *f) {
48-
return f->f_state > FRAME_EXECUTING;
49-
}
50-
5118
/* Standard object interface */
5219

5320
PyAPI_DATA(PyTypeObject) PyFrame_Type;
@@ -59,7 +26,7 @@ PyAPI_FUNC(PyFrameObject *) PyFrame_New(PyThreadState *, PyCodeObject *,
5926

6027
/* only internal use */
6128
PyFrameObject*
62-
_PyFrame_New_NoTrack(PyThreadState *, PyFrameConstructor *, PyObject *, PyObject **);
29+
_PyFrame_New_NoTrack(struct _interpreter_frame *, int);
6330

6431

6532
/* The rest of the interface is specific for frame objects */

Include/cpython/pystate.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ struct _ts {
7878
PyInterpreterState *interp;
7979

8080
/* Borrowed reference to the current frame (it can be NULL) */
81-
PyFrameObject *frame;
81+
struct _interpreter_frame *frame;
8282
int recursion_depth;
8383
int recursion_headroom; /* Allow 50 more calls to handle any errors. */
8484
int stackcheck_counter;
@@ -223,7 +223,7 @@ PyAPI_FUNC(void) PyThreadState_DeleteCurrent(void);
223223

224224
/* Frame evaluation API */
225225

226-
typedef PyObject* (*_PyFrameEvalFunction)(PyThreadState *tstate, PyFrameObject *, int);
226+
typedef PyObject* (*_PyFrameEvalFunction)(PyThreadState *tstate, struct _interpreter_frame *, int);
227227

228228
PyAPI_FUNC(_PyFrameEvalFunction) _PyInterpreterState_GetEvalFrameFunc(
229229
PyInterpreterState *interp);

Include/genobject.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ extern "C" {
1616
#define _PyGenObject_HEAD(prefix) \
1717
PyObject_HEAD \
1818
/* Note: gi_frame can be NULL if the generator is "finished" */ \
19-
PyFrameObject *prefix##_frame; \
19+
struct _interpreter_frame *prefix##_xframe; \
2020
/* The code object backing the generator */ \
21-
PyCodeObject *prefix##_code; \
21+
PyCodeObject *prefix##_code; \
2222
/* List of weak reference. */ \
2323
PyObject *prefix##_weakreflist; \
2424
/* Name of the generator. */ \

Include/internal/pycore_ceval.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ extern PyObject *_PyEval_BuiltinsFromGlobals(
4141

4242

4343
static inline PyObject*
44-
_PyEval_EvalFrame(PyThreadState *tstate, PyFrameObject *f, int throwflag)
44+
_PyEval_EvalFrame(PyThreadState *tstate, struct _interpreter_frame *frame, int throwflag)
4545
{
46-
return tstate->interp->eval_frame(tstate, f, throwflag);
46+
return tstate->interp->eval_frame(tstate, frame, throwflag);
4747
}
4848

4949
extern PyObject *
@@ -107,6 +107,9 @@ static inline void _Py_LeaveRecursiveCall_inline(void) {
107107

108108
#define Py_LeaveRecursiveCall() _Py_LeaveRecursiveCall_inline()
109109

110+
struct _interpreter_frame *_PyEval_GetFrame(void);
111+
112+
PyObject *_Py_MakeCoro(PyFrameConstructor *, struct _interpreter_frame *);
110113

111114
#ifdef __cplusplus
112115
}

Include/internal/pycore_frame.h

Lines changed: 104 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,123 @@
44
extern "C" {
55
#endif
66

7-
enum {
8-
FRAME_SPECIALS_GLOBALS_OFFSET = 0,
9-
FRAME_SPECIALS_BUILTINS_OFFSET = 1,
10-
FRAME_SPECIALS_LOCALS_OFFSET = 2,
11-
FRAME_SPECIALS_CODE_OFFSET = 3,
12-
FRAME_SPECIALS_SIZE = 4
7+
/* These values are chosen so that the inline functions below all
8+
* compare f_state to zero.
9+
*/
10+
enum _framestate {
11+
FRAME_CREATED = -2,
12+
FRAME_SUSPENDED = -1,
13+
FRAME_EXECUTING = 0,
14+
FRAME_RETURNED = 1,
15+
FRAME_UNWINDING = 2,
16+
FRAME_RAISED = 3,
17+
FRAME_CLEARED = 4
1318
};
1419

15-
static inline PyObject **
16-
_PyFrame_Specials(PyFrameObject *f) {
17-
return &f->f_valuestack[-FRAME_SPECIALS_SIZE];
20+
typedef signed char PyFrameState;
21+
22+
typedef struct _interpreter_frame {
23+
PyObject *f_globals;
24+
PyObject *f_builtins;
25+
PyObject *f_locals;
26+
PyCodeObject *f_code;
27+
PyFrameObject *frame_obj;
28+
/* Borrowed reference to a generator, or NULL */
29+
PyObject *generator;
30+
struct _interpreter_frame *previous;
31+
int f_lasti; /* Last instruction if called */
32+
int stackdepth; /* Depth of value stack */
33+
int nlocalsplus;
34+
PyFrameState f_state; /* What state the frame is in */
35+
PyObject *stack[1];
36+
} InterpreterFrame;
37+
38+
static inline int _PyFrame_IsRunnable(InterpreterFrame *f) {
39+
return f->f_state < FRAME_EXECUTING;
40+
}
41+
42+
static inline int _PyFrame_IsExecuting(InterpreterFrame *f) {
43+
return f->f_state == FRAME_EXECUTING;
1844
}
1945

20-
/* Returns a *borrowed* reference. */
21-
static inline PyObject *
22-
_PyFrame_GetGlobals(PyFrameObject *f)
46+
static inline int _PyFrameHasCompleted(InterpreterFrame *f) {
47+
return f->f_state > FRAME_EXECUTING;
48+
}
49+
50+
#define FRAME_SPECIALS_SIZE ((sizeof(InterpreterFrame)-1)/sizeof(PyObject *))
51+
52+
InterpreterFrame *
53+
_PyInterpreterFrame_HeapAlloc(PyFrameConstructor *con, PyObject *locals);
54+
55+
static inline void
56+
_PyFrame_InitializeSpecials(
57+
InterpreterFrame *frame, PyFrameConstructor *con,
58+
PyObject *locals, int nlocalsplus)
2359
{
24-
return _PyFrame_Specials(f)[FRAME_SPECIALS_GLOBALS_OFFSET];
60+
frame->f_code = (PyCodeObject *)Py_NewRef(con->fc_code);
61+
frame->f_builtins = Py_NewRef(con->fc_builtins);
62+
frame->f_globals = Py_NewRef(con->fc_globals);
63+
frame->f_locals = Py_XNewRef(locals);
64+
frame->nlocalsplus = nlocalsplus;
65+
frame->stackdepth = 0;
66+
frame->frame_obj = NULL;
67+
frame->generator = NULL;
68+
frame->f_lasti = -1;
69+
frame->f_state = FRAME_CREATED;
2570
}
2671

27-
/* Returns a *borrowed* reference. */
28-
static inline PyObject *
29-
_PyFrame_GetBuiltins(PyFrameObject *f)
72+
/* Gets the pointer to the locals array
73+
* that precedes this frame.
74+
*/
75+
static inline PyObject**
76+
_PyFrame_GetLocalsArray(InterpreterFrame *frame)
3077
{
31-
return _PyFrame_Specials(f)[FRAME_SPECIALS_BUILTINS_OFFSET];
78+
return ((PyObject **)frame) - frame->nlocalsplus;
3279
}
3380

34-
/* Returns a *borrowed* reference. */
35-
static inline PyCodeObject *
36-
_PyFrame_GetCode(PyFrameObject *f)
81+
/* For use by _PyFrame_GetFrameObject
82+
Do not call directly. */
83+
PyFrameObject *
84+
_PyFrame_MakeAndSetFrameObject(InterpreterFrame *frame);
85+
86+
/* Gets the PyFrameObject for this frame, lazily
87+
* creating it if necessary.
88+
* Returns a borrowed referennce */
89+
static inline PyFrameObject *
90+
_PyFrame_GetFrameObject(InterpreterFrame *frame)
3791
{
38-
return (PyCodeObject *)_PyFrame_Specials(f)[FRAME_SPECIALS_CODE_OFFSET];
92+
PyFrameObject *res = frame->frame_obj;
93+
if (res != NULL) {
94+
return res;
95+
}
96+
return _PyFrame_MakeAndSetFrameObject(frame);
3997
}
4098

41-
int _PyFrame_TakeLocals(PyFrameObject *f);
99+
/* Clears all references in the frame.
100+
* If take is non-zero, then the InterpreterFrame frame
101+
* may be transfered to the frame object it references
102+
* instead of being cleared. Either way
103+
* the caller no longer owns the references
104+
* in the frame.
105+
* take should be set to 1 for heap allocated
106+
* frames like the ones in generators and coroutines.
107+
*/
108+
int
109+
_PyFrame_Clear(InterpreterFrame * frame, int take);
110+
111+
int
112+
_PyFrame_Traverse(InterpreterFrame *frame, visitproc visit, void *arg);
113+
114+
int
115+
_PyFrame_FastToLocalsWithError(InterpreterFrame *frame);
116+
117+
void
118+
_PyFrame_LocalsToFast(InterpreterFrame *frame, int clear);
119+
120+
InterpreterFrame *_PyThreadState_PushFrame(
121+
PyThreadState *tstate, PyFrameConstructor *con, PyObject *locals);
122+
123+
void _PyThreadState_PopFrame(PyThreadState *tstate, InterpreterFrame *frame);
42124

43125
#ifdef __cplusplus
44126
}

Include/internal/pycore_pystate.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,6 @@ PyAPI_FUNC(int) _PyState_AddModule(
147147

148148
PyAPI_FUNC(int) _PyOS_InterruptOccurred(PyThreadState *tstate);
149149

150-
PyObject **_PyThreadState_PushLocals(PyThreadState *, int size);
151-
void _PyThreadState_PopLocals(PyThreadState *, PyObject **);
152-
153150
#ifdef __cplusplus
154151
}
155152
#endif

Lib/test/test_faulthandler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def temporary_filename():
5656
os_helper.unlink(filename)
5757

5858
class FaultHandlerTests(unittest.TestCase):
59+
5960
def get_output(self, code, filename=None, fd=None):
6061
"""
6162
Run the specified code in Python (in a new child process) and read the

Lib/test/test_sys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1275,7 +1275,7 @@ class C(object): pass
12751275
# frame
12761276
import inspect
12771277
x = inspect.currentframe()
1278-
check(x, size('4P3i4cP'))
1278+
check(x, size('3Pi3c'))
12791279
# function
12801280
def func(): pass
12811281
check(func, size('14Pi'))

Makefile.pre.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ PYTHON_OBJS= \
350350
Python/context.o \
351351
Python/dynamic_annotations.o \
352352
Python/errors.o \
353+
Python/frame.o \
353354
Python/frozenmain.o \
354355
Python/future.o \
355356
Python/getargs.o \
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
All necessary data for executing a Python function (local variables, stack,
2+
etc) is now kept in a per-thread stack. Frame objects are lazily allocated
3+
on demand. This increases performance by about 7% on the standard benchmark
4+
suite. Introspection and debugging are unaffected as frame objects are
5+
always available when needed. Patch by Mark Shannon.

Modules/_tracemalloc.c

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include "pycore_pymem.h" // _Py_tracemalloc_config
44
#include "pycore_traceback.h"
55
#include "pycore_hashtable.h"
6-
#include "frameobject.h" // PyFrame_GetBack()
6+
#include <pycore_frame.h>
77

88
#include "clinic/_tracemalloc.c.h"
99
/*[clinic input]
@@ -299,18 +299,16 @@ hashtable_compare_traceback(const void *key1, const void *key2)
299299

300300

301301
static void
302-
tracemalloc_get_frame(PyFrameObject *pyframe, frame_t *frame)
302+
tracemalloc_get_frame(InterpreterFrame *pyframe, frame_t *frame)
303303
{
304304
frame->filename = unknown_filename;
305-
int lineno = PyFrame_GetLineNumber(pyframe);
305+
int lineno = PyCode_Addr2Line(pyframe->f_code, pyframe->f_lasti*2);
306306
if (lineno < 0) {
307307
lineno = 0;
308308
}
309309
frame->lineno = (unsigned int)lineno;
310310

311-
PyCodeObject *code = PyFrame_GetCode(pyframe);
312-
PyObject *filename = code->co_filename;
313-
Py_DECREF(code);
311+
PyObject *filename = pyframe->f_code->co_filename;
314312

315313
if (filename == NULL) {
316314
#ifdef TRACE_DEBUG
@@ -395,7 +393,7 @@ traceback_get_frames(traceback_t *traceback)
395393
return;
396394
}
397395

398-
PyFrameObject *pyframe = PyThreadState_GetFrame(tstate);
396+
InterpreterFrame *pyframe = tstate->frame;
399397
for (; pyframe != NULL;) {
400398
if (traceback->nframe < _Py_tracemalloc_config.max_nframe) {
401399
tracemalloc_get_frame(pyframe, &traceback->frames[traceback->nframe]);
@@ -406,8 +404,7 @@ traceback_get_frames(traceback_t *traceback)
406404
traceback->total_nframe++;
407405
}
408406

409-
PyFrameObject *back = PyFrame_GetBack(pyframe);
410-
Py_DECREF(pyframe);
407+
InterpreterFrame *back = pyframe->previous;
411408
pyframe = back;
412409
}
413410
}

Modules/_xxsubinterpretersmodule.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "Python.h"
66
#include "frameobject.h"
7+
#include "pycore_frame.h"
78
#include "interpreteridobject.h"
89

910

@@ -1834,13 +1835,12 @@ _is_running(PyInterpreterState *interp)
18341835
}
18351836

18361837
assert(!PyErr_Occurred());
1837-
PyFrameObject *frame = PyThreadState_GetFrame(tstate);
1838+
InterpreterFrame *frame = tstate->frame;
18381839
if (frame == NULL) {
18391840
return 0;
18401841
}
18411842

18421843
int executing = _PyFrame_IsExecuting(frame);
1843-
Py_DECREF(frame);
18441844

18451845
return executing;
18461846
}

0 commit comments

Comments
 (0)