Skip to content

Commit a7e785b

Browse files
committed
gh-124697: avoid unnecessary deduplication in frameobject API functions
1 parent 4f5a67a commit a7e785b

5 files changed

Lines changed: 144 additions & 70 deletions

File tree

‎Include/cpython/code.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ typedef struct {
107107
struct _PyCoMonitoringData *_co_monitoring; /* Monitoring data */ \
108108
_PyCode_DEF_UNIQUE_ID() \
109109
int _co_firsttraceable; /* index of first traceable instruction */ \
110+
/* 0 = not yet known, 1 = co_localsplusnames are unique, 2 = a name is \
111+
repeated. FrameLocalsProxy learns this from the seen map it builds \
112+
while collecting locals, and skips deduplication when this is 1. */ \
113+
char _co_localsplus_dedup; \
110114
/* Scratch space for extra data relating to the code object. \
111115
Type is a void* to keep the format private in codeobject.c to force \
112116
people to go through the proper APIs. */ \

‎Include/internal/pycore_code.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ extern PyObject* _PyCode_GetCellvars(PyCodeObject *);
273273
extern PyObject* _PyCode_GetFreevars(PyCodeObject *);
274274
extern PyObject* _PyCode_GetCode(PyCodeObject *);
275275

276+
/* co->_co_localsplus_dedup. Learned by FrameLocalsProxy from its seen map. */
277+
#define _PYCODE_LOCALSPLUS_DEDUP_UNKNOWN 0
278+
#define _PYCODE_LOCALSPLUS_DEDUP_UNIQUE 1
279+
#define _PYCODE_LOCALSPLUS_DEDUP_NEEDED 2
280+
276281
/** API for initializing the line number tables. */
277282
PyAPI_FUNC(int) _PyCode_InitAddressRange(PyCodeObject* co, PyCodeAddressRange *bounds);
278283

‎Lib/test/test_listcomps.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,23 @@ def inner():
944944
{"snaps": [1, 2], "vals": [2, 2], "consistent": [True, True]},
945945
ns={"sys": sys}, scopes=["module", "function"])
946946

947+
def test_frame_locals_dedup_learned_before_cell_is_bound(self):
948+
# Reading f_locals before the comprehension cell is bound must not
949+
# cache "names are unique": the repeated name is still on the code
950+
# object, and dict(**f_locals) inside the comprehension has to work.
951+
code = """
952+
def outer(x):
953+
def inner():
954+
list(sys._getframe().f_locals)
955+
return [dict(**sys._getframe().f_locals)['x'] for x in x]
956+
return inner()
957+
result = outer([7, 8])
958+
"""
959+
import sys
960+
self._check_in_scopes(
961+
code, {"result": [7, 8]},
962+
ns={"sys": sys}, scopes=["module", "function"])
963+
947964
def test_frame_locals_nested_comp_cell_and_enclosing_free(self):
948965
# Stress a nested inlined shape where a comp cell and enclosing free
949966
# share a name; all f_locals views must stay consistent.

‎Objects/codeobject.c‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ init_code(PyCodeObject *co, struct _PyCodeConstructor *con)
561561
/* not set */
562562
co->co_weakreflist = NULL;
563563
co->co_extra = NULL;
564+
co->_co_localsplus_dedup = _PYCODE_LOCALSPLUS_DEDUP_UNKNOWN;
564565
co->_co_cached = NULL;
565566
co->co_executors = NULL;
566567

‎Objects/frameobject.c‎

Lines changed: 117 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -94,20 +94,62 @@ framelocalsproxy_hasval(_PyInterpreterFrame *frame, PyCodeObject *co, int i)
9494
return true;
9595
}
9696

97+
/* *seen is NULL when dedup is known to be unnecessary.
98+
Otherwise it maps each localsplus name to Py_True if a value was already
99+
kept, or Py_False if the name was only seen on an empty slot. An empty
100+
slot must not count as the kept value, but it still proves the name is
101+
repeated, so it is recorded while _co_localsplus_dedup is unknown.
102+
Returns -1 if the map cannot be created. */
97103
static int
98-
framelocalsproxy_is_first_occurrence(PyObject *seen, PyObject *name)
104+
framelocalsproxy_prepare_dedup(PyCodeObject *co, PyObject **seen)
99105
{
100-
int found = PySet_Contains(seen, name);
101-
if (found < 0) {
106+
*seen = NULL;
107+
if (co->_co_localsplus_dedup == _PYCODE_LOCALSPLUS_DEDUP_UNIQUE) {
108+
return 0;
109+
}
110+
*seen = PyDict_New();
111+
if (*seen == NULL) {
102112
return -1;
103113
}
104-
if (found) {
114+
return 0;
115+
}
116+
117+
/* Returns 1 if this slot's value should be kept, 0 if not, -1 on error.
118+
An empty slot is skipped once _co_localsplus_dedup is known. A repeated
119+
name stores _PYCODE_LOCALSPLUS_DEDUP_NEEDED. UNIQUE is stored by the
120+
caller only after the walk finishes without one. */
121+
static int
122+
framelocalsproxy_visit_name(PyCodeObject *co, int i, PyObject *seen,
123+
int has_value)
124+
{
125+
if (seen == NULL) {
126+
return has_value;
127+
}
128+
if (!has_value &&
129+
co->_co_localsplus_dedup != _PYCODE_LOCALSPLUS_DEDUP_UNKNOWN) {
105130
return 0;
106131
}
107-
if (PySet_Add(seen, name) < 0) {
108-
return -1;
132+
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
133+
PyObject *prev = PyDict_GetItemWithError(seen, name);
134+
if (prev == NULL) {
135+
if (PyErr_Occurred()) {
136+
return -1;
137+
}
138+
if (PyDict_SetItem(seen, name, has_value ? Py_True : Py_False) < 0) {
139+
return -1;
140+
}
141+
return has_value;
142+
}
143+
else {
144+
co->_co_localsplus_dedup = _PYCODE_LOCALSPLUS_DEDUP_NEEDED;
145+
if (!has_value || prev == Py_True) {
146+
return 0;
147+
}
148+
if (PyDict_SetItem(seen, name, Py_True) < 0) {
149+
return -1;
150+
}
151+
return 1;
109152
}
110-
return 1;
111153
}
112154

113155
static int
@@ -397,27 +439,30 @@ framelocalsproxy_keys(PyObject *self, PyObject *Py_UNUSED(ignored))
397439
return NULL;
398440
}
399441
// An inlined comprehension cell can share a name with a free var.
400-
PyObject *seen = PySet_New(NULL);
401-
if (seen == NULL) {
442+
// Assume dedup may be needed until a walk of seen shows otherwise.
443+
PyObject *seen = NULL;
444+
if (framelocalsproxy_prepare_dedup(co, &seen) < 0) {
402445
Py_DECREF(names);
403446
return NULL;
404447
}
405448

406449
for (int i = 0; i < co->co_nlocalsplus; i++) {
407-
if (framelocalsproxy_hasval(frame->f_frame, co, i)) {
450+
int has = framelocalsproxy_hasval(frame->f_frame, co, i);
451+
int emit = framelocalsproxy_visit_name(co, i, seen, has);
452+
if (emit < 0) {
453+
goto error;
454+
}
455+
if (emit) {
408456
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
409-
int first = framelocalsproxy_is_first_occurrence(seen, name);
410-
if (first < 0) {
457+
if (PyList_Append(names, name) < 0) {
411458
goto error;
412459
}
413-
if (first) {
414-
if (PyList_Append(names, name) < 0) {
415-
goto error;
416-
}
417-
}
418460
}
419461
}
420-
Py_DECREF(seen);
462+
if (co->_co_localsplus_dedup == _PYCODE_LOCALSPLUS_DEDUP_UNKNOWN) {
463+
co->_co_localsplus_dedup = _PYCODE_LOCALSPLUS_DEDUP_UNIQUE;
464+
}
465+
Py_XDECREF(seen);
421466

422467
// Iterate through the extra locals
423468
if (frame->f_extra_locals) {
@@ -438,7 +483,7 @@ framelocalsproxy_keys(PyObject *self, PyObject *Py_UNUSED(ignored))
438483
return names;
439484

440485
error:
441-
Py_DECREF(seen);
486+
Py_XDECREF(seen);
442487
Py_DECREF(names);
443488
return NULL;
444489
}
@@ -622,30 +667,30 @@ framelocalsproxy_values(PyObject *self, PyObject *Py_UNUSED(ignored))
622667
if (values == NULL) {
623668
return NULL;
624669
}
625-
PyObject *seen = PySet_New(NULL);
626-
if (seen == NULL) {
670+
PyObject *seen = NULL;
671+
if (framelocalsproxy_prepare_dedup(co, &seen) < 0) {
627672
Py_DECREF(values);
628673
return NULL;
629674
}
630675

631676
for (int i = 0; i < co->co_nlocalsplus; i++) {
632677
PyObject *value = framelocalsproxy_getval(frame->f_frame, co, i);
633-
if (value) {
634-
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
635-
int first = framelocalsproxy_is_first_occurrence(seen, name);
636-
if (first == 1) {
637-
if (PyList_Append(values, value) < 0) {
638-
Py_DECREF(value);
639-
goto error;
640-
}
641-
}
678+
int has = value != NULL;
679+
int emit = framelocalsproxy_visit_name(co, i, seen, has);
680+
if (emit < 0) {
681+
Py_XDECREF(value);
682+
goto error;
683+
}
684+
if (emit && PyList_Append(values, value) < 0) {
642685
Py_DECREF(value);
643-
if (first < 0) {
644-
goto error;
645-
}
686+
goto error;
646687
}
688+
Py_XDECREF(value);
689+
}
690+
if (co->_co_localsplus_dedup == _PYCODE_LOCALSPLUS_DEDUP_UNKNOWN) {
691+
co->_co_localsplus_dedup = _PYCODE_LOCALSPLUS_DEDUP_UNIQUE;
647692
}
648-
Py_DECREF(seen);
693+
Py_XDECREF(seen);
649694

650695
// Iterate through the extra locals
651696
if (frame->f_extra_locals) {
@@ -663,7 +708,7 @@ framelocalsproxy_values(PyObject *self, PyObject *Py_UNUSED(ignored))
663708
return values;
664709

665710
error:
666-
Py_DECREF(seen);
711+
Py_XDECREF(seen);
667712
Py_DECREF(values);
668713
return NULL;
669714
}
@@ -677,36 +722,37 @@ framelocalsproxy_items(PyObject *self, PyObject *Py_UNUSED(ignored))
677722
if (items == NULL) {
678723
return NULL;
679724
}
680-
PyObject *seen = PySet_New(NULL);
681-
if (seen == NULL) {
725+
PyObject *seen = NULL;
726+
if (framelocalsproxy_prepare_dedup(co, &seen) < 0) {
682727
Py_DECREF(items);
683728
return NULL;
684729
}
685730

686731
for (int i = 0; i < co->co_nlocalsplus; i++) {
687-
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
688732
PyObject *value = framelocalsproxy_getval(frame->f_frame, co, i);
689-
690-
if (value) {
691-
int first = framelocalsproxy_is_first_occurrence(seen, name);
692-
if (first == 1) {
693-
PyObject *pair = _PyTuple_FromPairSteal(Py_NewRef(name), value);
694-
if (pair == NULL) {
695-
goto error;
696-
}
697-
if (_PyList_AppendTakeRef((PyListObject *)items, pair) < 0) {
698-
goto error;
699-
}
700-
}
701-
else {
702-
Py_DECREF(value);
703-
if (first < 0) {
704-
goto error;
705-
}
706-
}
733+
int has = value != NULL;
734+
int emit = framelocalsproxy_visit_name(co, i, seen, has);
735+
if (emit < 0) {
736+
Py_XDECREF(value);
737+
goto error;
707738
}
739+
if (!emit) {
740+
Py_XDECREF(value);
741+
continue;
742+
}
743+
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
744+
PyObject *pair = _PyTuple_FromPairSteal(Py_NewRef(name), value);
745+
if (pair == NULL) {
746+
goto error;
747+
}
748+
if (_PyList_AppendTakeRef((PyListObject *)items, pair) < 0) {
749+
goto error;
750+
}
751+
}
752+
if (co->_co_localsplus_dedup == _PYCODE_LOCALSPLUS_DEDUP_UNKNOWN) {
753+
co->_co_localsplus_dedup = _PYCODE_LOCALSPLUS_DEDUP_UNIQUE;
708754
}
709-
Py_DECREF(seen);
755+
Py_XDECREF(seen);
710756
seen = NULL;
711757

712758
// Iterate through the extra locals
@@ -746,24 +792,25 @@ framelocalsproxy_length(PyObject *self)
746792
size += PyDict_Size(frame->f_extra_locals);
747793
}
748794

749-
PyObject *seen = PySet_New(NULL);
750-
if (seen == NULL) {
795+
PyObject *seen = NULL;
796+
if (framelocalsproxy_prepare_dedup(co, &seen) < 0) {
751797
return -1;
752798
}
753799
for (int i = 0; i < co->co_nlocalsplus; i++) {
754-
if (framelocalsproxy_hasval(frame->f_frame, co, i)) {
755-
PyObject *name = PyTuple_GET_ITEM(co->co_localsplusnames, i);
756-
int first = framelocalsproxy_is_first_occurrence(seen, name);
757-
if (first < 0) {
758-
Py_DECREF(seen);
759-
return -1;
760-
}
761-
else if (first) {
762-
size++;
763-
}
800+
int has = framelocalsproxy_hasval(frame->f_frame, co, i);
801+
int emit = framelocalsproxy_visit_name(co, i, seen, has);
802+
if (emit < 0) {
803+
Py_XDECREF(seen);
804+
return -1;
764805
}
806+
if (emit) {
807+
size++;
808+
}
809+
}
810+
if (co->_co_localsplus_dedup == _PYCODE_LOCALSPLUS_DEDUP_UNKNOWN) {
811+
co->_co_localsplus_dedup = _PYCODE_LOCALSPLUS_DEDUP_UNIQUE;
765812
}
766-
Py_DECREF(seen);
813+
Py_XDECREF(seen);
767814
return size;
768815
}
769816

0 commit comments

Comments
 (0)