Skip to content

Commit 409fa8e

Browse files
gh-150027: Avoid copying during construction of frozenset objects (GH-150028)
1 parent 29415c0 commit 409fa8e

11 files changed

Lines changed: 116 additions & 22 deletions

File tree

Include/internal/pycore_intrinsics.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
#define INTRINSIC_TYPEVARTUPLE 9
1919
#define INTRINSIC_SUBSCRIPT_GENERIC 10
2020
#define INTRINSIC_TYPEALIAS 11
21+
#define INTRINSIC_BUILD_FROZENSET 12
2122

22-
#define MAX_INTRINSIC_1 11
23+
#define MAX_INTRINSIC_1 12
2324

2425

2526
/* Binary Functions: */

Include/internal/pycore_opcode_utils.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ extern "C" {
8080
#define CONSTANT_TRUE 9
8181
#define CONSTANT_FALSE 10
8282
#define CONSTANT_MINUS_ONE 11
83-
#define NUM_COMMON_CONSTANTS 12
83+
#define CONSTANT_BUILTIN_FROZENSET 12
84+
#define NUM_COMMON_CONSTANTS 13
8485

8586
/* Values used in the oparg for RESUME */
8687
#define RESUME_AT_FUNC_START 0

Include/internal/pycore_setobject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ extern void _PySet_ClearInternal(PySetObject *so);
3535

3636
PyAPI_FUNC(int) _PySet_AddTakeRef(PySetObject *so, PyObject *key);
3737

38+
PyObject *
39+
_PySet_Freeze(PyObject *set);
40+
3841
#ifdef __cplusplus
3942
}
4043
#endif

Lib/opcode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
builtins.set,
4545
# Append-only — must match CONSTANT_* in
4646
# Include/internal/pycore_opcode_utils.h.
47-
None, "", True, False, -1]
47+
None, "", True, False, -1, builtins.frozenset]
4848
_nb_ops = _opcode.get_nb_ops()
4949

5050
hascompare = [opmap["COMPARE_OP"]]

Lib/test/test_builtin.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,10 @@ def f_list():
268268
def f_set():
269269
return set(2*x for x in [1,2,3])
270270

271-
funcs = [f_all, f_any, f_tuple, f_list, f_set]
271+
def f_frozenset():
272+
return frozenset(2*x for x in [1,2,3])
273+
274+
funcs = [f_all, f_any, f_tuple, f_list, f_set, f_frozenset]
272275

273276
for f in funcs:
274277
# check that generator code object is not duplicated
@@ -278,35 +281,37 @@ def f_set():
278281

279282
# check the overriding the builtins works
280283

281-
global all, any, tuple, list, set
282-
saved = all, any, tuple, list, set
284+
global all, any, tuple, list, set, frozenset
285+
saved = all, any, tuple, list, set, frozenset
283286
try:
284287
all = lambda x : "all"
285288
any = lambda x : "any"
286289
tuple = lambda x : "tuple"
287290
list = lambda x : "list"
288291
set = lambda x : "set"
292+
frozenset = lambda x : "frozenset"
289293

290294
overridden_outputs = [f() for f in funcs]
291295
finally:
292-
all, any, tuple, list, set = saved
296+
all, any, tuple, list, set, frozenset = saved
293297

294-
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set'])
298+
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set', 'frozenset'])
295299
# Now repeat, overriding the builtins module as well
296-
saved = all, any, tuple, list, set
300+
saved = all, any, tuple, list, set, frozenset
297301
try:
298302
builtins.all = all = lambda x : "all"
299303
builtins.any = any = lambda x : "any"
300304
builtins.tuple = tuple = lambda x : "tuple"
301305
builtins.list = list = lambda x : "list"
302306
builtins.set = set = lambda x : "set"
307+
builtins.frozenset = frozenset = lambda x : "frozenset"
303308

304309
overridden_outputs = [f() for f in funcs]
305310
finally:
306-
all, any, tuple, list, set = saved
307-
builtins.all, builtins.any, builtins.tuple, builtins.list, builtins.set = saved
311+
all, any, tuple, list, set, frozenset = saved
312+
builtins.all, builtins.any, builtins.tuple, builtins.list, builtins.set, builtins.frozenset = saved
308313

309-
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set'])
314+
self.assertEqual(overridden_outputs, ['all', 'any', 'tuple', 'list', 'set', 'frozenset'])
310315

311316
def test_builtin_call_async_genexpr_no_crash(self):
312317
async def f_all():

Lib/test/test_compiler_codegen.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,34 @@ def test_syntax_error__return_not_in_function(self):
161161
self.assertIsNone(cm.exception.text)
162162
self.assertEqual(cm.exception.offset, 1)
163163
self.assertEqual(cm.exception.end_offset, 10)
164+
165+
def test_frozenset_optimization(self):
166+
l1 = self.Label()
167+
snippet = "frozenset({1, 2, 3})"
168+
expected = [
169+
('RESUME', 0),
170+
('ANNOTATIONS_PLACEHOLDER', None),
171+
('LOAD_NAME', 0),
172+
('COPY', 1),
173+
('LOAD_COMMON_CONSTANT', 12),
174+
('IS_OP', 0),
175+
('POP_JUMP_IF_FALSE', l1),
176+
('POP_TOP', None),
177+
('LOAD_CONST', 1),
178+
('LOAD_CONST', 2),
179+
('LOAD_CONST', 3),
180+
('BUILD_SET', 3),
181+
('CALL_INTRINSIC_1', 12),
182+
('JUMP', 0),
183+
l1,
184+
('PUSH_NULL', None),
185+
('LOAD_CONST', 1),
186+
('LOAD_CONST', 2),
187+
('LOAD_CONST', 3),
188+
('BUILD_SET', 3),
189+
('CALL', 1),
190+
('POP_TOP', None),
191+
('LOAD_CONST', 0),
192+
('RETURN_VALUE', None)
193+
]
194+
self.codegen_test(snippet, expected)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve performance of :class:`frozenset` objects by avoiding copies during
2+
construction.

Objects/setobject.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,16 @@ set_swap_bodies(PySetObject *a, PySetObject *b)
15451545
FT_ATOMIC_STORE_PTR_RELEASE(b->table, b_table);
15461546
}
15471547

1548+
PyObject *
1549+
_PySet_Freeze(PyObject *set)
1550+
{
1551+
assert(set != NULL);
1552+
assert(PySet_CheckExact(set));
1553+
assert(_PyObject_IsUniquelyReferenced(set));
1554+
set->ob_type = &PyFrozenSet_Type;
1555+
return Py_NewRef(set);
1556+
}
1557+
15481558
/*[clinic input]
15491559
@critical_section
15501560
set.copy

Python/codegen.c

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3953,22 +3953,45 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end)
39533953

39543954
if (! (func->kind == Name_kind &&
39553955
asdl_seq_LEN(args) == 1 &&
3956-
asdl_seq_LEN(kwds) == 0 &&
3957-
asdl_seq_GET(args, 0)->kind == GeneratorExp_kind))
3956+
asdl_seq_LEN(kwds) == 0))
39583957
{
39593958
return 0;
39603959
}
39613960

3962-
expr_ty generator_exp = asdl_seq_GET(args, 0);
3963-
PySTEntryObject *generator_entry = _PySymtable_Lookup(SYMTABLE(c), (void *)generator_exp);
3961+
location loc = LOC(func);
3962+
3963+
expr_ty arg_expr = asdl_seq_GET(args, 0);
3964+
3965+
if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "frozenset")
3966+
&& (arg_expr->kind == Set_kind || arg_expr->kind == SetComp_kind)) {
3967+
NEW_JUMP_TARGET_LABEL(c, skip_optimization);
3968+
3969+
ADDOP_I(c, loc, COPY, 1);
3970+
ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_BUILTIN_FROZENSET);
3971+
ADDOP_COMPARE(c, loc, Is);
3972+
ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, skip_optimization);
3973+
ADDOP(c, loc, POP_TOP);
3974+
3975+
VISIT(c, expr, arg_expr);
3976+
ADDOP_I(c, loc, CALL_INTRINSIC_1, INTRINSIC_BUILD_FROZENSET);
3977+
3978+
ADDOP_JUMP(c, loc, JUMP, end);
3979+
3980+
USE_LABEL(c, skip_optimization);
3981+
return 1;
3982+
}
3983+
3984+
if (arg_expr->kind != GeneratorExp_kind) {
3985+
return 0;
3986+
}
3987+
3988+
PySTEntryObject *generator_entry = _PySymtable_Lookup(SYMTABLE(c), (void *)arg_expr);
39643989
if (generator_entry->ste_coroutine) {
39653990
Py_DECREF(generator_entry);
39663991
return 0;
39673992
}
39683993
Py_DECREF(generator_entry);
39693994

3970-
location loc = LOC(func);
3971-
39723995
int optimized = 0;
39733996
NEW_JUMP_TARGET_LABEL(c, skip_optimization);
39743997

@@ -3994,6 +4017,9 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end)
39944017
else if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "set")) {
39954018
const_oparg = CONSTANT_BUILTIN_SET;
39964019
}
4020+
else if (_PyUnicode_EqualToASCIIString(func->v.Name.id, "frozenset")) {
4021+
const_oparg = CONSTANT_BUILTIN_FROZENSET;
4022+
}
39974023
if (const_oparg != -1) {
39984024
ADDOP_I(c, loc, COPY, 1); // the function
39994025
ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, const_oparg);
@@ -4003,10 +4029,10 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end)
40034029

40044030
if (const_oparg == CONSTANT_BUILTIN_TUPLE || const_oparg == CONSTANT_BUILTIN_LIST) {
40054031
ADDOP_I(c, loc, BUILD_LIST, 0);
4006-
} else if (const_oparg == CONSTANT_BUILTIN_SET) {
4032+
} else if (const_oparg == CONSTANT_BUILTIN_SET || const_oparg == CONSTANT_BUILTIN_FROZENSET) {
40074033
ADDOP_I(c, loc, BUILD_SET, 0);
40084034
}
4009-
VISIT(c, expr, generator_exp);
4035+
VISIT(c, expr, arg_expr);
40104036

40114037
NEW_JUMP_TARGET_LABEL(c, loop);
40124038
NEW_JUMP_TARGET_LABEL(c, cleanup);
@@ -4017,7 +4043,7 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end)
40174043
if (const_oparg == CONSTANT_BUILTIN_TUPLE || const_oparg == CONSTANT_BUILTIN_LIST) {
40184044
ADDOP_I(c, loc, LIST_APPEND, 3);
40194045
ADDOP_JUMP(c, loc, JUMP, loop);
4020-
} else if (const_oparg == CONSTANT_BUILTIN_SET) {
4046+
} else if (const_oparg == CONSTANT_BUILTIN_SET || const_oparg == CONSTANT_BUILTIN_FROZENSET) {
40214047
ADDOP_I(c, loc, SET_ADD, 3);
40224048
ADDOP_JUMP(c, loc, JUMP, loop);
40234049
}
@@ -4029,7 +4055,8 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end)
40294055
ADDOP(c, NO_LOCATION, POP_ITER);
40304056
if (const_oparg != CONSTANT_BUILTIN_TUPLE &&
40314057
const_oparg != CONSTANT_BUILTIN_LIST &&
4032-
const_oparg != CONSTANT_BUILTIN_SET) {
4058+
const_oparg != CONSTANT_BUILTIN_SET &&
4059+
const_oparg != CONSTANT_BUILTIN_FROZENSET) {
40334060
ADDOP_LOAD_CONST(c, loc, initial_res == Py_True ? Py_False : Py_True);
40344061
}
40354062
ADDOP_JUMP(c, loc, JUMP, end);
@@ -4044,6 +4071,9 @@ maybe_optimize_function_call(compiler *c, expr_ty e, jump_target_label end)
40444071
} else if (const_oparg == CONSTANT_BUILTIN_SET) {
40454072
// result is already a set
40464073
}
4074+
else if (const_oparg == CONSTANT_BUILTIN_FROZENSET) {
4075+
ADDOP_I(c, loc, CALL_INTRINSIC_1, INTRINSIC_BUILD_FROZENSET);
4076+
}
40474077
else {
40484078
ADDOP_LOAD_CONST(c, loc, initial_res);
40494079
}

Python/intrinsics.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "pycore_intrinsics.h" // INTRINSIC_PRINT
1010
#include "pycore_list.h" // _PyList_AsTupleAndClear()
1111
#include "pycore_object.h" // _PyObject_IsUniquelyReferenced()
12+
#include "pycore_setobject.h" // _PySet_Freeze()
1213
#include "pycore_pyerrors.h" // _PyErr_SetString()
1314
#include "pycore_runtime.h" // _Py_ID()
1415
#include "pycore_typevarobject.h" // _Py_make_typevar()
@@ -207,6 +208,14 @@ make_typevar(PyThreadState* Py_UNUSED(ignored), PyObject *v)
207208
return _Py_make_typevar(v, NULL, NULL);
208209
}
209210

211+
static PyObject *
212+
make_frozenset(PyThreadState* Py_UNUSED(ignored), PyObject *set)
213+
{
214+
assert(PySet_CheckExact(set));
215+
assert(_PyObject_IsUniquelyReferenced(set));
216+
return _PySet_Freeze(set);
217+
}
218+
210219

211220
#define INTRINSIC_FUNC_ENTRY(N, F) \
212221
[N] = {F, #N},
@@ -225,6 +234,7 @@ _PyIntrinsics_UnaryFunctions[] = {
225234
INTRINSIC_FUNC_ENTRY(INTRINSIC_TYPEVARTUPLE, _Py_make_typevartuple)
226235
INTRINSIC_FUNC_ENTRY(INTRINSIC_SUBSCRIPT_GENERIC, _Py_subscript_generic)
227236
INTRINSIC_FUNC_ENTRY(INTRINSIC_TYPEALIAS, _Py_make_typealias)
237+
INTRINSIC_FUNC_ENTRY(INTRINSIC_BUILD_FROZENSET, make_frozenset)
228238
};
229239

230240

0 commit comments

Comments
 (0)