Skip to content

Commit 1b62b1a

Browse files
Merge branch 'python:main' into patch-4
2 parents 0a1dd00 + 14cbd0e commit 1b62b1a

File tree

9 files changed

+92
-58
lines changed

9 files changed

+92
-58
lines changed

Include/internal/pycore_function.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ _PyFunction_IsVersionValid(uint32_t version)
3030
extern uint32_t _PyFunction_GetVersionForCurrentState(PyFunctionObject *func);
3131
PyAPI_FUNC(void) _PyFunction_SetVersion(PyFunctionObject *func, uint32_t version);
3232
void _PyFunction_ClearCodeByVersion(uint32_t version);
33-
PyFunctionObject *_PyFunction_LookupByVersion(uint32_t version, PyObject **p_code);
3433

3534
extern PyObject *_Py_set_function_type_params(
3635
PyThreadState* unused, PyObject *func, PyObject *type_params);

Lib/test/test_perf_profiler.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,47 @@ def baz():
170170
self.assertNotIn(f"py::bar:{script}", child_perf_file_contents)
171171
self.assertNotIn(f"py::baz:{script}", child_perf_file_contents)
172172

173+
@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
174+
def test_trampoline_works_after_fork_with_many_code_objects(self):
175+
code = """if 1:
176+
import gc, os, sys, signal
177+
178+
# Create many code objects so trampoline_refcount > 1
179+
for i in range(50):
180+
exec(compile(f"def _dummy_{i}(): pass", f"<test{i}>", "exec"))
181+
182+
pid = os.fork()
183+
if pid == 0:
184+
# Child: create and destroy new code objects,
185+
# then collect garbage. If the old code watcher
186+
# survived the fork, the double-decrement of
187+
# trampoline_refcount will cause a SIGSEGV.
188+
for i in range(50):
189+
exec(compile(f"def _child_{i}(): pass", f"<child{i}>", "exec"))
190+
gc.collect()
191+
os._exit(0)
192+
else:
193+
_, status = os.waitpid(pid, 0)
194+
if os.WIFSIGNALED(status):
195+
print(f"FAIL: child killed by signal {os.WTERMSIG(status)}", file=sys.stderr)
196+
sys.exit(1)
197+
sys.exit(os.WEXITSTATUS(status))
198+
"""
199+
with temp_dir() as script_dir:
200+
script = make_script(script_dir, "perftest", code)
201+
env = {**os.environ, "PYTHON_JIT": "0"}
202+
with subprocess.Popen(
203+
[sys.executable, "-Xperf", script],
204+
text=True,
205+
stderr=subprocess.PIPE,
206+
stdout=subprocess.PIPE,
207+
env=env,
208+
) as process:
209+
stdout, stderr = process.communicate()
210+
211+
self.assertEqual(process.returncode, 0, stderr)
212+
self.assertEqual(stderr, "")
213+
173214
@unittest.skipIf(support.check_bolt_optimized(), "fails on BOLT instrumented binaries")
174215
def test_sys_api(self):
175216
for define_eval_hook in (False, True):

Lib/test/test_syntax.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2345,6 +2345,12 @@
23452345
Traceback (most recent call last):
23462346
SyntaxError: positional patterns follow keyword patterns
23472347
2348+
>>> match ...:
2349+
... case Foo(y=1, x=2, y=3):
2350+
... ...
2351+
Traceback (most recent call last):
2352+
SyntaxError: attribute name repeated in class pattern: y
2353+
23482354
>>> match ...:
23492355
... case C(a=b, c, d=e, f, g=h, i, j=k, ...):
23502356
... ...
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve :opcode:`MATCH_CLASS` performance by up to 52% in certain cases. Patch by Marc Mueller.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a crash in fork child process when perf support is enabled.

Objects/funcobject.c

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -373,32 +373,6 @@ _PyFunction_ClearCodeByVersion(uint32_t version)
373373
#endif
374374
}
375375

376-
PyFunctionObject *
377-
_PyFunction_LookupByVersion(uint32_t version, PyObject **p_code)
378-
{
379-
#ifdef Py_GIL_DISABLED
380-
return NULL;
381-
#else
382-
PyInterpreterState *interp = _PyInterpreterState_GET();
383-
struct _func_version_cache_item *slot = get_cache_item(interp, version);
384-
if (slot->code) {
385-
assert(PyCode_Check(slot->code));
386-
PyCodeObject *code = (PyCodeObject *)slot->code;
387-
if (code->co_version == version) {
388-
*p_code = slot->code;
389-
}
390-
}
391-
else {
392-
*p_code = NULL;
393-
}
394-
if (slot->func && slot->func->func_version == version) {
395-
assert(slot->func->func_code == slot->code);
396-
return slot->func;
397-
}
398-
return NULL;
399-
#endif
400-
}
401-
402376
uint32_t
403377
_PyFunction_GetVersionForCurrentState(PyFunctionObject *func)
404378
{

Python/ceval.c

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -509,15 +509,18 @@ match_class_attr(PyThreadState *tstate, PyObject *subject, PyObject *type,
509509
PyObject *name, PyObject *seen)
510510
{
511511
assert(PyUnicode_CheckExact(name));
512-
assert(PySet_CheckExact(seen));
513-
if (PySet_Contains(seen, name) || PySet_Add(seen, name)) {
514-
if (!_PyErr_Occurred(tstate)) {
515-
// Seen it before!
516-
_PyErr_Format(tstate, PyExc_TypeError,
517-
"%s() got multiple sub-patterns for attribute %R",
518-
((PyTypeObject*)type)->tp_name, name);
512+
// Only check for duplicates if seen is not NULL.
513+
if (seen != NULL) {
514+
assert(PySet_CheckExact(seen));
515+
if (PySet_Contains(seen, name) || PySet_Add(seen, name)) {
516+
if (!_PyErr_Occurred(tstate)) {
517+
// Seen it before!
518+
_PyErr_Format(tstate, PyExc_TypeError,
519+
"%s() got multiple sub-patterns for attribute %R",
520+
((PyTypeObject*)type)->tp_name, name);
521+
}
522+
return NULL;
519523
}
520-
return NULL;
521524
}
522525
PyObject *attr;
523526
(void)PyObject_GetOptionalAttr(subject, name, &attr);
@@ -540,14 +543,26 @@ _PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type,
540543
if (PyObject_IsInstance(subject, type) <= 0) {
541544
return NULL;
542545
}
546+
// Short circuit if there aren't any arguments:
547+
Py_ssize_t nkwargs = PyTuple_GET_SIZE(kwargs);
548+
Py_ssize_t nattrs = nargs + nkwargs;
549+
if (!nattrs) {
550+
return PyTuple_New(0);
551+
}
543552
// So far so good:
544-
PyObject *seen = PySet_New(NULL);
545-
if (seen == NULL) {
546-
return NULL;
553+
PyObject *seen = NULL;
554+
// Only check for duplicates if there is at least one positional attribute
555+
// and two or more attributes in total. Duplicate keyword attributes are
556+
// detected during the compile stage and raise a SyntaxError.
557+
if (nargs > 0 && nattrs > 1) {
558+
seen = PySet_New(NULL);
559+
if (seen == NULL) {
560+
return NULL;
561+
}
547562
}
548-
PyObject *attrs = PyList_New(0);
563+
PyObject *attrs = PyTuple_New(nattrs);
549564
if (attrs == NULL) {
550-
Py_DECREF(seen);
565+
Py_XDECREF(seen);
551566
return NULL;
552567
}
553568
// NOTE: From this point on, goto fail on failure:
@@ -588,9 +603,8 @@ _PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type,
588603
}
589604
if (match_self) {
590605
// Easy. Copy the subject itself, and move on to kwargs.
591-
if (PyList_Append(attrs, subject) < 0) {
592-
goto fail;
593-
}
606+
assert(PyTuple_GET_ITEM(attrs, 0) == NULL);
607+
PyTuple_SET_ITEM(attrs, 0, Py_NewRef(subject));
594608
}
595609
else {
596610
for (Py_ssize_t i = 0; i < nargs; i++) {
@@ -606,36 +620,29 @@ _PyEval_MatchClass(PyThreadState *tstate, PyObject *subject, PyObject *type,
606620
if (attr == NULL) {
607621
goto fail;
608622
}
609-
if (PyList_Append(attrs, attr) < 0) {
610-
Py_DECREF(attr);
611-
goto fail;
612-
}
613-
Py_DECREF(attr);
623+
assert(PyTuple_GET_ITEM(attrs, i) == NULL);
624+
PyTuple_SET_ITEM(attrs, i, attr);
614625
}
615626
}
616627
Py_CLEAR(match_args);
617628
}
618629
// Finally, the keyword subpatterns:
619-
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(kwargs); i++) {
630+
for (Py_ssize_t i = 0; i < nkwargs; i++) {
620631
PyObject *name = PyTuple_GET_ITEM(kwargs, i);
621632
PyObject *attr = match_class_attr(tstate, subject, type, name, seen);
622633
if (attr == NULL) {
623634
goto fail;
624635
}
625-
if (PyList_Append(attrs, attr) < 0) {
626-
Py_DECREF(attr);
627-
goto fail;
628-
}
629-
Py_DECREF(attr);
636+
assert(PyTuple_GET_ITEM(attrs, nargs + i) == NULL);
637+
PyTuple_SET_ITEM(attrs, nargs + i, attr);
630638
}
631-
Py_SETREF(attrs, PyList_AsTuple(attrs));
632-
Py_DECREF(seen);
639+
Py_XDECREF(seen);
633640
return attrs;
634641
fail:
635642
// We really don't care whether an error was raised or not... that's our
636643
// caller's problem. All we know is that the match failed.
637644
Py_XDECREF(match_args);
638-
Py_DECREF(seen);
645+
Py_XDECREF(seen);
639646
Py_DECREF(attrs);
640647
return NULL;
641648
}

Python/optimizer.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
#include "pycore_bitutils.h" // _Py_popcount32()
99
#include "pycore_ceval.h" // _Py_set_eval_breaker_bit
1010
#include "pycore_code.h" // _Py_GetBaseCodeUnit
11-
#include "pycore_function.h" // _PyFunction_LookupByVersion()
1211
#include "pycore_interpframe.h"
1312
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
1413
#include "pycore_opcode_metadata.h" // _PyOpcode_OpName[]

Python/perf_trampoline.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,12 @@ _PyPerfTrampoline_AfterFork_Child(void)
618618
int was_active = _PyIsPerfTrampolineActive();
619619
_PyPerfTrampoline_Fini();
620620
if (was_active) {
621+
// After fork, Fini may leave the old code watcher registered
622+
// if trampolined code objects from the parent still exist
623+
// (trampoline_refcount > 0). Clear it unconditionally before
624+
// Init registers a new one, to prevent two watchers sharing
625+
// the same globals and double-decrementing trampoline_refcount.
626+
perf_trampoline_reset_state();
621627
_PyPerfTrampoline_Init(1);
622628
}
623629
}

0 commit comments

Comments
 (0)