diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 7032b61d7654b8..7737b1884f25f1 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -454,6 +454,9 @@ typedef struct { #define _PyFrozenDictObject_CAST(op) \ (assert(PyFrozenDict_Check(op)), _Py_CAST(PyFrozenDictObject*, (op))) +PyObject * +_PyDict_Freeze(PyObject *dict); + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_intrinsics.h b/Include/internal/pycore_intrinsics.h index 59a7b16073f886..55c337dca44111 100644 --- a/Include/internal/pycore_intrinsics.h +++ b/Include/internal/pycore_intrinsics.h @@ -19,8 +19,9 @@ #define INTRINSIC_SUBSCRIPT_GENERIC 10 #define INTRINSIC_TYPEALIAS 11 #define INTRINSIC_BUILD_FROZENSET 12 +#define INTRINSIC_BUILD_FROZENDICT 13 -#define MAX_INTRINSIC_1 12 +#define MAX_INTRINSIC_1 13 /* Binary Functions: */ diff --git a/Include/internal/pycore_opcode_utils.h b/Include/internal/pycore_opcode_utils.h index 7067b48ec22cb3..d952fd633ad87a 100644 --- a/Include/internal/pycore_opcode_utils.h +++ b/Include/internal/pycore_opcode_utils.h @@ -82,7 +82,8 @@ extern "C" { #define CONSTANT_MINUS_ONE 11 #define CONSTANT_BUILTIN_FROZENSET 12 #define CONSTANT_EMPTY_TUPLE 13 -#define NUM_COMMON_CONSTANTS 14 +#define CONSTANT_BUILTIN_FROZENDICT 14 +#define NUM_COMMON_CONSTANTS 15 /* Values used in the oparg for RESUME */ #define RESUME_AT_FUNC_START 0 diff --git a/Lib/opcode.py b/Lib/opcode.py index 750d83b2c87af5..252a0dcb1d1f1c 100644 --- a/Lib/opcode.py +++ b/Lib/opcode.py @@ -44,7 +44,7 @@ builtins.set, # Append-only — must match CONSTANT_* in # Include/internal/pycore_opcode_utils.h. - None, "", True, False, -1, builtins.frozenset, ()] + None, "", True, False, -1, builtins.frozenset, (), builtins.frozendict] _nb_ops = _opcode.get_nb_ops() hascompare = [opmap["COMPARE_OP"]] diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 1d2c105ac047e1..28172acdf5db93 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -266,7 +266,10 @@ def f_set(): def f_frozenset(): return frozenset(2*x for x in [1,2,3]) - funcs = [f_all, f_any, f_tuple, f_list, f_set, f_frozenset] + def f_frozendict(): + return frozendict(2*x for x in [1,2]) + + funcs = [f_all, f_any, f_tuple, f_list, f_set, f_frozenset, f_frozendict] for f in funcs: # check that generator code object is not duplicated @@ -276,8 +279,8 @@ def f_frozenset(): # check the overriding the builtins works - global all, any, tuple, list, set, frozenset - saved = all, any, tuple, list, set, frozenset + global all, any, tuple, list, set, frozenset, frozendict + saved = all, any, tuple, list, set, frozenset, frozendict try: all = lambda x : "all" any = lambda x : "any" @@ -285,14 +288,15 @@ def f_frozenset(): list = lambda x : "list" set = lambda x : "set" frozenset = lambda x : "frozenset" + frozendict = lambda x: "frozendict" overridden_outputs = [f() for f in funcs] finally: - all, any, tuple, list, set, frozenset = saved + all, any, tuple, list, set, frozenset, frozendict = saved - self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set', 'frozenset']) + self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set', 'frozenset', 'frozendict']) # Now repeat, overriding the builtins module as well - saved = all, any, tuple, list, set, frozenset + saved = all, any, tuple, list, set, frozenset, frozendict try: builtins.all = all = lambda x : "all" builtins.any = any = lambda x : "any" @@ -300,13 +304,14 @@ def f_frozenset(): builtins.list = list = lambda x : "list" builtins.set = set = lambda x : "set" builtins.frozenset = frozenset = lambda x : "frozenset" + builtins.frozendict = frozendict = lambda x: "frozendict" overridden_outputs = [f() for f in funcs] finally: - all, any, tuple, list, set, frozenset = saved - builtins.all, builtins.any, builtins.tuple, builtins.list, builtins.set, builtins.frozenset = saved + all, any, tuple, list, set, frozenset, frozendict = saved + builtins.all, builtins.any, builtins.tuple, builtins.list, builtins.set, builtins.frozenset, builtins.frozendict = saved - self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set', 'frozenset']) + self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set', 'frozenset', 'frozendict']) def test_builtin_call_async_genexpr_no_crash(self): async def f_all(): diff --git a/Lib/test/test_compiler_codegen.py b/Lib/test/test_compiler_codegen.py index 87509245f6a866..725f10e68538db 100644 --- a/Lib/test/test_compiler_codegen.py +++ b/Lib/test/test_compiler_codegen.py @@ -217,6 +217,35 @@ def test_frozenset_optimization(self): ] self.codegen_test(snippet, expected) + def test_frozendict_optimization(self): + l1 = self.Label() + snippet = "frozendict({1: 2})" + expected = [ + ('RESUME', 0), + ('ANNOTATIONS_PLACEHOLDER', None), + ('LOAD_NAME', 0), + ('COPY', 1), + ('LOAD_COMMON_CONSTANT', 12), + ('IS_OP', 0), + ('POP_JUMP_IF_FALSE', l1), + ('POP_TOP', None), + ('LOAD_CONST', 1), + ('LOAD_CONST', 2), + ('BUILD_MAP', 2), + ('CALL_INTRINSIC_1', 12), + ('JUMP', 0), + l1, + ('PUSH_NULL', None), + ('LOAD_CONST', 1), + ('LOAD_CONST', 2), + ('BUILD_MAP', 3), + ('CALL', 1), + ('POP_TOP', None), + ('LOAD_CONST', 0), + ('RETURN_VALUE', None) + ] + self.codegen_test(snippet, expected) + def test_comp_without_target_optimization(self): snippet = "[i for i in range(10)]" expected = [ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-06-07-06.gh-issue-150027.Yz-gfp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-06-07-06.gh-issue-150027.Yz-gfp.rst new file mode 100644 index 00000000000000..6df117e30ef8ac --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-06-07-06.gh-issue-150027.Yz-gfp.rst @@ -0,0 +1,2 @@ +Improve performance of :class:`frozendict` objects by avoiding copies during +construction. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 246ffe6c18e9b5..e847137e5e4143 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -8595,3 +8595,14 @@ PyTypeObject PyFrozenDict_Type = { .tp_vectorcall = frozendict_vectorcall, .tp_version_tag = _Py_TYPE_VERSION_FROZENDICT, }; + + +PyObject * +_PyDict_Freeze(PyObject *dict) +{ + assert(dict != NULL); + assert(PyDict_CheckExact(dict)); + assert(_PyObject_IsUniquelyReferenced(dict)); + dict->ob_type = &PyFrozenDict_Type; + return Py_NewRef(dict); +} diff --git a/Programs/test_frozenmain.h b/Programs/test_frozenmain.h index c82b791439a3fa..41a61cbbf5aeb1 100644 --- a/Programs/test_frozenmain.h +++ b/Programs/test_frozenmain.h @@ -13,10 +13,10 @@ unsigned char M_test_frozenmain[] = { 89,2,31,0,78,4,89,6,12,0,78,5,89,5,89,6, 42,26,0,0,0,0,0,0,0,0,0,0,12,0,48,4, 50,1,0,0,0,0,0,0,29,0,71,26,0,0,9,0, - 28,0,77,7,33,0,41,7,233,0,0,0,0,122,18,70, + 28,0,77,7,33,0,41,7,233,0,0,0,0,218,18,70, 114,111,122,101,110,32,72,101,108,108,111,32,87,111,114,108, - 100,122,8,115,121,115,46,97,114,103,118,218,6,99,111,110, - 102,105,103,122,7,99,111,110,102,105,103,32,122,2,58,32, + 100,218,8,115,121,115,46,97,114,103,118,218,6,99,111,110, + 102,105,103,218,7,99,111,110,102,105,103,32,218,2,58,32, 41,5,218,12,112,114,111,103,114,97,109,95,110,97,109,101, 218,10,101,120,101,99,117,116,97,98,108,101,218,15,117,115, 101,95,101,110,118,105,114,111,110,109,101,110,116,218,17,99, @@ -25,15 +25,15 @@ unsigned char M_test_frozenmain[] = { 41,7,218,3,115,121,115,218,17,95,116,101,115,116,105,110, 116,101,114,110,97,108,99,97,112,105,218,5,112,114,105,110, 116,218,4,97,114,103,118,218,11,103,101,116,95,99,111,110, - 102,105,103,115,114,3,0,0,0,218,3,107,101,121,169,0, + 102,105,103,115,114,5,0,0,0,218,3,107,101,121,169,0, 243,0,0,0,0,218,18,116,101,115,116,95,102,114,111,122, 101,110,109,97,105,110,46,112,121,218,8,60,109,111,100,117, - 108,101,62,114,18,0,0,0,1,0,0,0,115,94,0,0, + 108,101,62,114,22,0,0,0,1,0,0,0,115,94,0,0, 0,241,3,1,1,1,243,8,0,1,11,219,0,24,225,0, 5,208,6,26,212,0,27,217,0,5,128,106,144,35,151,40, 145,40,212,0,27,216,9,26,215,9,38,210,9,38,211,9, 40,168,24,213,9,50,128,6,244,2,6,12,2,128,67,241, 14,0,5,10,136,71,144,67,144,53,152,2,152,54,160,35, 157,59,152,45,208,10,40,214,4,41,243,15,6,12,2,114, - 16,0,0,0, + 20,0,0,0, }; diff --git a/Python/codegen.c b/Python/codegen.c index f2c2b21d106fbd..692784fa3e2d2b 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4006,6 +4006,25 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end) return 1; } + if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "frozendict") + && (arg_expr->kind == Dict_kind || arg_expr->kind == DictComp_kind)) { + NEW_JUMP_TARGET_LABEL(c, skip_optimization); + + ADDOP_I(c, loc, COPY, 1); + ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_BUILTIN_FROZENDICT); + ADDOP_COMPARE(c, loc, Is); + ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, skip_optimization); + ADDOP(c, loc, POP_TOP); + + VISIT(c, expr, arg_expr); + ADDOP_I(c, loc, CALL_INTRINSIC_1, INTRINSIC_BUILD_FROZENDICT); + + ADDOP_JUMP(c, loc, JUMP, end); + + USE_LABEL(c, skip_optimization); + return 1; + } + if (arg_expr->kind != GeneratorExp_kind) { return 0; } diff --git a/Python/intrinsics.c b/Python/intrinsics.c index f081f33cc83b88..079751b03326d5 100644 --- a/Python/intrinsics.c +++ b/Python/intrinsics.c @@ -3,6 +3,7 @@ #include "Python.h" #include "pycore_compile.h" // _PyCompile_GetUnaryIntrinsicName +#include "pycore_dict.h" // _PyDict_Freeze() #include "pycore_function.h" // _Py_set_function_type_params() #include "pycore_genobject.h" // _PyAsyncGenValueWrapperNew #include "pycore_interpframe.h" // _PyFrame_GetLocals() @@ -216,6 +217,14 @@ make_frozenset(PyThreadState* Py_UNUSED(ignored), PyObject *set) return _PySet_Freeze(set); } +static PyObject * +make_frozendict(PyThreadState* Py_UNUSED(ignored), PyObject *dict) +{ + assert(PyDict_CheckExact(dict)); + assert(_PyObject_IsUniquelyReferenced(dict)); + return _PyDict_Freeze(dict); +} + #define INTRINSIC_FUNC_ENTRY(N, F) \ [N] = {F, #N}, @@ -235,6 +244,7 @@ _PyIntrinsics_UnaryFunctions[] = { INTRINSIC_FUNC_ENTRY(INTRINSIC_SUBSCRIPT_GENERIC, _Py_subscript_generic) INTRINSIC_FUNC_ENTRY(INTRINSIC_TYPEALIAS, _Py_make_typealias) INTRINSIC_FUNC_ENTRY(INTRINSIC_BUILD_FROZENSET, make_frozenset) + INTRINSIC_FUNC_ENTRY(INTRINSIC_BUILD_FROZENDICT, make_frozendict) }; diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 01ca459b2eb2b8..283856bf45dd70 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -898,6 +898,7 @@ pycore_init_builtins(PyThreadState *tstate) common_objs[CONSTANT_BUILTIN_FROZENSET] = (PyObject *)&PyFrozenSet_Type; common_objs[CONSTANT_EMPTY_TUPLE] = Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_TUPLE); + common_objs[CONSTANT_BUILTIN_FROZENDICT] = (PyObject *)&PyFrozenDict_Type; for (int i = 0; i < NUM_COMMON_CONSTANTS; i++) { assert(common_objs[i] != NULL); _Py_SetImmortal(common_objs[i]);