Skip to content

Commit a09aef5

Browse files
committed
gh-146381: Fold frozendict(key=const, ...) calls to LOAD_CONST
1 parent 951675c commit a09aef5

File tree

6 files changed

+90
-2
lines changed

6 files changed

+90
-2
lines changed

Include/internal/pycore_opcode_utils.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ extern "C" {
7575
#define CONSTANT_BUILTIN_ANY 4
7676
#define CONSTANT_BUILTIN_LIST 5
7777
#define CONSTANT_BUILTIN_SET 6
78-
#define NUM_COMMON_CONSTANTS 7
78+
#define CONSTANT_BUILTIN_FROZENDICT 7
79+
#define NUM_COMMON_CONSTANTS 8
7980

8081
/* Values used in the oparg for RESUME */
8182
#define RESUME_AT_FUNC_START 0

Lib/opcode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
_special_method_names = _opcode.get_special_method_names()
4242
_common_constants = [builtins.AssertionError, builtins.NotImplementedError,
4343
builtins.tuple, builtins.all, builtins.any, builtins.list,
44-
builtins.set]
44+
builtins.set, builtins.frozendict]
4545
_nb_ops = _opcode.get_nb_ops()
4646

4747
hascompare = [opmap["COMPARE_OP"]]

Lib/test/test_peepholer.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,29 @@ def g(a):
241241
self.assertTrue(g(4))
242242
self.check_lnotab(g)
243243

244+
def test_constant_folding_frozendict_call(self):
245+
# frozendict(key=const, ...) should be folded to LOAD_CONST
246+
# with a runtime guard (fallback CALL_KW path still exists)
247+
def f():
248+
return frozendict(x=1, y=2)
249+
250+
code = f.__code__
251+
self.assertInBytecode(code, 'LOAD_CONST', frozendict(x=1, y=2))
252+
self.assertInBytecode(code, 'LOAD_COMMON_CONSTANT', 7)
253+
result = f()
254+
self.assertEqual(result, frozendict(x=1, y=2))
255+
self.assertIsInstance(result, frozendict)
256+
257+
def test_constant_folding_frozendict_call_shadowed(self):
258+
# When frozendict is shadowed, the fallback path should be used
259+
def f():
260+
frozendict = dict
261+
return frozendict(x=1, y=2)
262+
263+
result = f()
264+
self.assertEqual(result, {'x': 1, 'y': 2})
265+
self.assertIsInstance(result, dict)
266+
244267
def test_constant_folding_small_int(self):
245268
tests = [
246269
('(0, )[0]', 0),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fold frozendict(key=const, ...) calls to LOAD_CONST in codegen. Patch by
2+
Donghee Na.

Python/codegen.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4128,6 +4128,66 @@ codegen_validate_keywords(compiler *c, asdl_keyword_seq *keywords)
41284128
return SUCCESS;
41294129
}
41304130

4131+
/* Try to fold frozendict(key=const, ...) into LOAD_CONST with a runtime guard.
4132+
Called after the function has been loaded onto the stack.
4133+
Return 1 if optimization was emitted, 0 if not, -1 on error. */
4134+
static int
4135+
maybe_optimize_frozendict_call(compiler *c, expr_ty e, jump_target_label end)
4136+
{
4137+
expr_ty func = e->v.Call.func;
4138+
asdl_expr_seq *args = e->v.Call.args;
4139+
asdl_keyword_seq *kwds = e->v.Call.keywords;
4140+
4141+
if (func->kind != Name_kind ||
4142+
!_PyUnicode_EqualToASCIIString(func->v.Name.id, "frozendict") ||
4143+
asdl_seq_LEN(args) != 0)
4144+
{
4145+
return 0;
4146+
}
4147+
4148+
/* All keywords must have names (no **kwargs) and constant values */
4149+
Py_ssize_t nkwds = asdl_seq_LEN(kwds);
4150+
for (Py_ssize_t i = 0; i < nkwds; i++) {
4151+
keyword_ty kw = asdl_seq_GET(kwds, i);
4152+
if (kw->arg == NULL || kw->value->kind != Constant_kind) {
4153+
return 0;
4154+
}
4155+
}
4156+
4157+
/* Build the frozendict at compile time */
4158+
PyObject *dict = PyDict_New();
4159+
if (dict == NULL) {
4160+
return -1;
4161+
}
4162+
for (Py_ssize_t i = 0; i < nkwds; i++) {
4163+
keyword_ty kw = asdl_seq_GET(kwds, i);
4164+
if (PyDict_SetItem(dict, kw->arg, kw->value->v.Constant.value) < 0) {
4165+
Py_DECREF(dict);
4166+
return -1;
4167+
}
4168+
}
4169+
PyObject *fd = PyFrozenDict_New(dict);
4170+
Py_DECREF(dict);
4171+
if (fd == NULL) {
4172+
return -1;
4173+
}
4174+
4175+
location loc = LOC(func);
4176+
NEW_JUMP_TARGET_LABEL(c, skip_optimization);
4177+
4178+
ADDOP_I(c, loc, COPY, 1);
4179+
ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_BUILTIN_FROZENDICT);
4180+
ADDOP_COMPARE(c, loc, Is);
4181+
ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, skip_optimization);
4182+
ADDOP(c, loc, POP_TOP);
4183+
ADDOP_LOAD_CONST(c, LOC(e), fd);
4184+
Py_DECREF(fd);
4185+
ADDOP_JUMP(c, loc, JUMP, end);
4186+
4187+
USE_LABEL(c, skip_optimization);
4188+
return 1;
4189+
}
4190+
41314191
static int
41324192
codegen_call(compiler *c, expr_ty e)
41334193
{
@@ -4143,6 +4203,7 @@ codegen_call(compiler *c, expr_ty e)
41434203
RETURN_IF_ERROR(check_caller(c, e->v.Call.func));
41444204
VISIT(c, expr, e->v.Call.func);
41454205
RETURN_IF_ERROR(maybe_optimize_function_call(c, e, skip_normal_call));
4206+
RETURN_IF_ERROR(maybe_optimize_frozendict_call(c, e, skip_normal_call));
41464207
location loc = LOC(e->v.Call.func);
41474208
ADDOP(c, loc, PUSH_NULL);
41484209
loc = LOC(e);

Python/pylifecycle.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ pycore_init_builtins(PyThreadState *tstate)
887887
interp->common_consts[CONSTANT_BUILTIN_ANY] = any;
888888
interp->common_consts[CONSTANT_BUILTIN_LIST] = (PyObject*)&PyList_Type;
889889
interp->common_consts[CONSTANT_BUILTIN_SET] = (PyObject*)&PySet_Type;
890+
interp->common_consts[CONSTANT_BUILTIN_FROZENDICT] = (PyObject*)&PyFrozenDict_Type;
890891

891892
for (int i=0; i < NUM_COMMON_CONSTANTS; i++) {
892893
assert(interp->common_consts[i] != NULL);

0 commit comments

Comments
 (0)