Skip to content

Commit 2c1e258

Browse files
bpo-43693: Add new internal code objects fields: co_fastlocalnames and co_fastlocalkinds. (pythongh-26388)
A number of places in the code base (notably ceval.c and frameobject.c) rely on mapping variable names to indices in the frame "locals plus" array (AKA fast locals), and thus opargs. Currently the compiler indirectly encodes that information on the code object as the tuples co_varnames, co_cellvars, and co_freevars. At runtime the dependent code must calculate the proper mapping from those, which isn't ideal and impacts performance-sensitive sections. This is something we can easily address in the compiler instead. This change addresses the situation by replacing internal use of co_varnames, etc. with a single combined tuple of names in locals-plus order, along with a minimal array mapping each to its kind (local vs. cell vs. free). These two new PyCodeObject fields, co_fastlocalnames and co_fastllocalkinds, are not exposed to Python code for now, but co_varnames, etc. are still available with the same values as before (though computed lazily). Aside from the (mild) performance impact, there are a number of other benefits: * there's now a clear, direct relationship between locals-plus and variables * code that relies on the locals-plus-to-name mapping is simpler * marshaled code objects are smaller and serialize/de-serialize faster Also note that we can take this approach further by expanding the possible values in co_fastlocalkinds to include specific argument types (e.g. positional-only, kwargs). Doing so would allow further speed-ups in _PyEval_MakeFrameVector(), which is where args get unpacked into the locals-plus array. It would also allow us to shrink marshaled code objects even further. https://bugs.python.org/issue43693
1 parent ea0210f commit 2c1e258

File tree

21 files changed

+5967
-5659
lines changed

21 files changed

+5967
-5659
lines changed

Doc/library/dis.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,9 +1059,8 @@ All of the following opcodes use their arguments.
10591059
.. opcode:: LOAD_CLOSURE (i)
10601060

10611061
Pushes a reference to the cell contained in slot *i* of the cell and free
1062-
variable storage. The name of the variable is ``co_cellvars[i]`` if *i* is
1063-
less than the length of *co_cellvars*. Otherwise it is ``co_freevars[i -
1064-
len(co_cellvars)]``.
1062+
variable storage. The name of the variable is
1063+
``co_fastlocalnames[i + len(co_varnames)]``.
10651064

10661065

10671066
.. opcode:: LOAD_DEREF (i)

Include/cpython/code.h

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#endif
44

55
typedef uint16_t _Py_CODEUNIT;
6+
// Each oparg must fit in the second half of _Py_CODEUNIT, hence 8 bits.
7+
#define _Py_MAX_OPARG 255
68

79
#ifdef WORDS_BIGENDIAN
810
# define _Py_OPCODE(word) ((word) >> 8)
@@ -14,6 +16,11 @@ typedef uint16_t _Py_CODEUNIT;
1416

1517
typedef struct _PyOpcache _PyOpcache;
1618

19+
20+
// These are duplicated from pycore_code.h.
21+
typedef unsigned char _PyLocalsPlusKind;
22+
typedef _PyLocalsPlusKind *_PyLocalsPlusKinds;
23+
1724
/* Bytecode object */
1825
struct PyCodeObject {
1926
PyObject_HEAD
@@ -53,9 +60,8 @@ struct PyCodeObject {
5360
int co_kwonlyargcount; /* #keyword only arguments */
5461
int co_stacksize; /* #entries needed for evaluation stack */
5562
int co_firstlineno; /* first source line number */
56-
PyObject *co_varnames; /* tuple of strings (local variable names) */
57-
PyObject *co_cellvars; /* tuple of strings (cell variable names) */
58-
PyObject *co_freevars; /* tuple of strings (free variable names) */
63+
PyObject *co_localsplusnames; /* tuple mapping offsets to names */
64+
_PyLocalsPlusKinds co_localspluskinds; /* array mapping to local kinds */
5965
PyObject *co_filename; /* unicode (where it was loaded from) */
6066
PyObject *co_name; /* unicode (name, for reference) */
6167
PyObject *co_linetable; /* string (encoding addr<->lineno mapping) See
@@ -65,11 +71,15 @@ struct PyCodeObject {
6571
/* These fields are set with computed values on new code objects. */
6672

6773
int *co_cell2arg; /* Maps cell vars which are arguments. */
68-
// These are redundant but offer some performance benefit.
74+
// redundant values (derived from co_localsplusnames and co_localspluskinds)
6975
int co_nlocalsplus; /* number of local + cell + free variables */
7076
int co_nlocals; /* number of local variables */
7177
int co_ncellvars; /* number of cell variables */
7278
int co_nfreevars; /* number of free variables */
79+
// lazily-computed values
80+
PyObject *co_varnames; /* tuple of strings (local variable names) */
81+
PyObject *co_cellvars; /* tuple of strings (cell variable names) */
82+
PyObject *co_freevars; /* tuple of strings (free variable names) */
7383

7484
/* The remaining fields are zeroed out on new code objects. */
7585

@@ -143,7 +153,7 @@ struct PyCodeObject {
143153
PyAPI_DATA(PyTypeObject) PyCode_Type;
144154

145155
#define PyCode_Check(op) Py_IS_TYPE(op, &PyCode_Type)
146-
#define PyCode_GetNumFree(op) (PyTuple_GET_SIZE((op)->co_freevars))
156+
#define PyCode_GetNumFree(op) ((op)->co_nfreevars)
147157

148158
/* Public interface */
149159
PyAPI_FUNC(PyCodeObject *) PyCode_New(

Include/internal/pycore_code.h

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,57 @@ struct _PyOpcache {
2626
};
2727

2828

29+
/* "Locals plus" for a code object is the set of locals + cell vars +
30+
* free vars. This relates to variable names as well as offsets into
31+
* the "fast locals" storage array of execution frames. The compiler
32+
* builds the list of names, their offsets, and the corresponding
33+
* kind of local.
34+
*
35+
* Those kinds represent the source of the initial value and the
36+
* variable's scope (as related to closures). A "local" is an
37+
* argument or other variable defined in the current scope. A "free"
38+
* variable is one that is defined in an outer scope and comes from
39+
* the function's closure. A "cell" variable is a local that escapes
40+
* into an inner function as part of a closure, and thus must be
41+
* wrapped in a cell. Any "local" can also be a "cell", but the
42+
* "free" kind is mutually exclusive with both.
43+
*/
44+
45+
// We would use an enum if C let us specify the storage type.
46+
typedef unsigned char _PyLocalsPlusKind;
47+
/* Note that these all fit within _PyLocalsPlusKind, as do combinations. */
48+
// Later, we will use the smaller numbers to differentiate the different
49+
// kinds of locals (e.g. pos-only arg, varkwargs, local-only).
50+
#define CO_FAST_LOCAL 0x20
51+
#define CO_FAST_CELL 0x40
52+
#define CO_FAST_FREE 0x80
53+
54+
typedef _PyLocalsPlusKind *_PyLocalsPlusKinds;
55+
56+
static inline int
57+
_PyCode_InitLocalsPlusKinds(int num, _PyLocalsPlusKinds *pkinds)
58+
{
59+
if (num == 0) {
60+
*pkinds = NULL;
61+
return 0;
62+
}
63+
_PyLocalsPlusKinds kinds = PyMem_NEW(_PyLocalsPlusKind, num);
64+
if (kinds == NULL) {
65+
PyErr_NoMemory();
66+
return -1;
67+
}
68+
*pkinds = kinds;
69+
return 0;
70+
}
71+
72+
static inline void
73+
_PyCode_ClearLocalsPlusKinds(_PyLocalsPlusKinds kinds)
74+
{
75+
if (kinds != NULL) {
76+
PyMem_Free(kinds);
77+
}
78+
}
79+
2980
struct _PyCodeConstructor {
3081
/* metadata */
3182
PyObject *filename;
@@ -42,13 +93,13 @@ struct _PyCodeConstructor {
4293
PyObject *names;
4394

4495
/* mapping frame offsets to information */
45-
PyObject *varnames;
46-
PyObject *cellvars;
47-
PyObject *freevars;
96+
PyObject *localsplusnames;
97+
_PyLocalsPlusKinds localspluskinds;
4898

4999
/* args (within varnames) */
50100
int argcount;
51101
int posonlyargcount;
102+
// XXX Replace argcount with posorkwargcount (argcount - posonlyargcount).
52103
int kwonlyargcount;
53104

54105
/* needed to create the frame */
@@ -75,6 +126,11 @@ PyAPI_FUNC(PyCodeObject *) _PyCode_New(struct _PyCodeConstructor *);
75126

76127
int _PyCode_InitOpcache(PyCodeObject *co);
77128

129+
/* Getters for internal PyCodeObject data. */
130+
PyAPI_FUNC(PyObject *) _PyCode_GetVarnames(PyCodeObject *);
131+
PyAPI_FUNC(PyObject *) _PyCode_GetCellvars(PyCodeObject *);
132+
PyAPI_FUNC(PyObject *) _PyCode_GetFreevars(PyCodeObject *);
133+
78134

79135
#ifdef __cplusplus
80136
}

Lib/ctypes/test/test_values.py

Lines changed: 3 additions & 3 deletions
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__", 138),
84-
("__phello__", -138),
85-
("__phello__.spam", 138),
83+
expected = [("__hello__", 128),
84+
("__phello__", -128),
85+
("__phello__.spam", 128),
8686
]
8787
self.assertEqual(items, expected, "PyImport_FrozenModules example "
8888
"in Doc/library/ctypes.rst may be out of date")

Lib/dis.py

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -273,15 +273,15 @@ def get_instructions(x, *, first_line=None):
273273
the disassembled code object.
274274
"""
275275
co = _get_code_object(x)
276-
cell_names = co.co_cellvars + co.co_freevars
277276
linestarts = dict(findlinestarts(co))
278277
if first_line is not None:
279278
line_offset = first_line - co.co_firstlineno
280279
else:
281280
line_offset = 0
282-
return _get_instructions_bytes(co.co_code, co.co_varnames, co.co_names,
283-
co.co_consts, cell_names, linestarts,
284-
line_offset)
281+
return _get_instructions_bytes(co.co_code,
282+
co._varname_from_oparg,
283+
co.co_names, co.co_consts,
284+
linestarts, line_offset)
285285

286286
def _get_const_info(const_index, const_list):
287287
"""Helper to get optional details about const references
@@ -295,16 +295,16 @@ def _get_const_info(const_index, const_list):
295295
argval = const_list[const_index]
296296
return argval, repr(argval)
297297

298-
def _get_name_info(name_index, name_list):
298+
def _get_name_info(name_index, get_name, **extrainfo):
299299
"""Helper to get optional details about named references
300300
301301
Returns the dereferenced name as both value and repr if the name
302302
list is defined.
303303
Otherwise returns the name index and its repr().
304304
"""
305305
argval = name_index
306-
if name_list is not None:
307-
argval = name_list[name_index]
306+
if get_name is not None:
307+
argval = get_name(name_index, **extrainfo)
308308
argrepr = argval
309309
else:
310310
argrepr = repr(argval)
@@ -336,8 +336,10 @@ def parse_exception_table(code):
336336
except StopIteration:
337337
return entries
338338

339-
def _get_instructions_bytes(code, varnames=None, names=None, constants=None,
340-
cells=None, linestarts=None, line_offset=0, exception_entries=()):
339+
def _get_instructions_bytes(code, varname_from_oparg=None,
340+
names=None, constants=None,
341+
linestarts=None, line_offset=0,
342+
exception_entries=()):
341343
"""Iterate over the instructions in a bytecode string.
342344
343345
Generates a sequence of Instruction namedtuples giving the details of each
@@ -346,6 +348,7 @@ def _get_instructions_bytes(code, varnames=None, names=None, constants=None,
346348
arguments.
347349
348350
"""
351+
get_name = None if names is None else names.__getitem__
349352
labels = set(findlabels(code))
350353
for start, end, target, _, _ in exception_entries:
351354
for i in range(start, end):
@@ -368,20 +371,21 @@ def _get_instructions_bytes(code, varnames=None, names=None, constants=None,
368371
if op in hasconst:
369372
argval, argrepr = _get_const_info(arg, constants)
370373
elif op in hasname:
371-
argval, argrepr = _get_name_info(arg, names)
374+
argval, argrepr = _get_name_info(arg, get_name)
372375
elif op in hasjabs:
373376
argval = arg*2
374377
argrepr = "to " + repr(argval)
375378
elif op in hasjrel:
376379
argval = offset + 2 + arg*2
377380
argrepr = "to " + repr(argval)
378381
elif op in haslocal:
379-
argval, argrepr = _get_name_info(arg, varnames)
382+
argval, argrepr = _get_name_info(arg, varname_from_oparg)
380383
elif op in hascompare:
381384
argval = cmp_op[arg]
382385
argrepr = argval
383386
elif op in hasfree:
384-
argval, argrepr = _get_name_info(arg, cells)
387+
argval, argrepr = _get_name_info(arg, varname_from_oparg,
388+
cell=True)
385389
elif op == FORMAT_VALUE:
386390
argval, argrepr = FORMAT_VALUE_CONVERTERS[arg & 0x3]
387391
argval = (argval, bool(arg & 0x4))
@@ -398,11 +402,11 @@ def _get_instructions_bytes(code, varnames=None, names=None, constants=None,
398402

399403
def disassemble(co, lasti=-1, *, file=None):
400404
"""Disassemble a code object."""
401-
cell_names = co.co_cellvars + co.co_freevars
402405
linestarts = dict(findlinestarts(co))
403406
exception_entries = parse_exception_table(co)
404-
_disassemble_bytes(co.co_code, lasti, co.co_varnames, co.co_names,
405-
co.co_consts, cell_names, linestarts, file=file,
407+
_disassemble_bytes(co.co_code, lasti,
408+
co._varname_from_oparg,
409+
co.co_names, co.co_consts, linestarts, file=file,
406410
exception_entries=exception_entries)
407411

408412
def _disassemble_recursive(co, *, file=None, depth=None):
@@ -416,8 +420,8 @@ def _disassemble_recursive(co, *, file=None, depth=None):
416420
print("Disassembly of %r:" % (x,), file=file)
417421
_disassemble_recursive(x, file=file, depth=depth)
418422

419-
def _disassemble_bytes(code, lasti=-1, varnames=None, names=None,
420-
constants=None, cells=None, linestarts=None,
423+
def _disassemble_bytes(code, lasti=-1, varname_from_oparg=None,
424+
names=None, constants=None, linestarts=None,
421425
*, file=None, line_offset=0, exception_entries=()):
422426
# Omit the line number column entirely if we have no line number info
423427
show_lineno = bool(linestarts)
@@ -434,8 +438,8 @@ def _disassemble_bytes(code, lasti=-1, varnames=None, names=None,
434438
offset_width = len(str(maxoffset))
435439
else:
436440
offset_width = 4
437-
for instr in _get_instructions_bytes(code, varnames, names,
438-
constants, cells, linestarts,
441+
for instr in _get_instructions_bytes(code, varname_from_oparg, names,
442+
constants, linestarts,
439443
line_offset=line_offset, exception_entries=exception_entries):
440444
new_source_line = (show_lineno and
441445
instr.starts_line is not None and
@@ -517,16 +521,16 @@ def __init__(self, x, *, first_line=None, current_offset=None):
517521
else:
518522
self.first_line = first_line
519523
self._line_offset = first_line - co.co_firstlineno
520-
self._cell_names = co.co_cellvars + co.co_freevars
521524
self._linestarts = dict(findlinestarts(co))
522525
self._original_object = x
523526
self.current_offset = current_offset
524527
self.exception_entries = parse_exception_table(co)
525528

526529
def __iter__(self):
527530
co = self.codeobj
528-
return _get_instructions_bytes(co.co_code, co.co_varnames, co.co_names,
529-
co.co_consts, self._cell_names,
531+
return _get_instructions_bytes(co.co_code,
532+
co._varname_from_oparg,
533+
co.co_names, co.co_consts,
530534
self._linestarts,
531535
line_offset=self._line_offset,
532536
exception_entries=self.exception_entries)
@@ -554,9 +558,9 @@ def dis(self):
554558
else:
555559
offset = -1
556560
with io.StringIO() as output:
557-
_disassemble_bytes(co.co_code, varnames=co.co_varnames,
561+
_disassemble_bytes(co.co_code,
562+
varname_from_oparg=co._varname_from_oparg,
558563
names=co.co_names, constants=co.co_consts,
559-
cells=self._cell_names,
560564
linestarts=self._linestarts,
561565
line_offset=self._line_offset,
562566
file=output,

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ def _write_atomic(path, data, mode=0o666):
355355
# Python 3.11a1 3450 Use exception table for unwinding ("zero cost" exception handling)
356356
# Python 3.11a1 3451 (Add CALL_METHOD_KW)
357357
# Python 3.11a1 3452 (drop nlocals from marshaled code objects)
358+
# Python 3.11a1 3453 (add co_fastlocalnames and co_fastlocalkinds)
358359

359360
#
360361
# MAGIC must change whenever the bytecode emitted by the compiler may no
@@ -364,7 +365,7 @@ def _write_atomic(path, data, mode=0o666):
364365
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
365366
# in PC/launcher.c must also be updated.
366367

367-
MAGIC_NUMBER = (3452).to_bytes(2, 'little') + b'\r\n'
368+
MAGIC_NUMBER = (3453).to_bytes(2, 'little') + b'\r\n'
368369
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
369370

370371
_PYCACHE = '__pycache__'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
``PyCodeObject`` gained ``co_fastlocalnames`` and ``co_fastlocalkinds`` as
2+
the the authoritative source of fast locals info. Marshaled code objects
3+
have changed accordingly.

Objects/clinic/codeobject.c.h

Lines changed: 48 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)