Skip to content

Commit 38eeff8

Browse files
authored
[mypyc] Add primitive for <type>.__name__ (#19683)
This seems quite common in real-world code, including in performance-critical functions. Python 3.11 added a C API function for this, which we use here. The primitive works for arbtirary objects, but only type objects have a specialized code path. Other use cases of `__name__` seem typically less performance-sensitive. This PR makes this micro-benchmark 2.0x faster on Python 3.13: ``` from typing import Iterator class FooBar: pass def foo(x: type[object], n: int) -> str: for a in range(n): s = x.__name__ return s def bench(n: int) -> None: for i in range(n): foo(FooBar, 1000) from time import time bench(50 * 1000) t0 = time() bench(50 * 1000) print(time() - t0) ```
1 parent 1f9505c commit 38eeff8

File tree

7 files changed

+134
-1
lines changed

7 files changed

+134
-1
lines changed

mypyc/irbuild/expression.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
is_int_rprimitive,
7777
is_list_rprimitive,
7878
is_none_rprimitive,
79+
is_object_rprimitive,
7980
object_rprimitive,
8081
set_rprimitive,
8182
)
@@ -98,7 +99,7 @@
9899
from mypyc.irbuild.specialize import apply_function_specialization, apply_method_specialization
99100
from mypyc.primitives.bytes_ops import bytes_slice_op
100101
from mypyc.primitives.dict_ops import dict_get_item_op, dict_new_op, exact_dict_set_item_op
101-
from mypyc.primitives.generic_ops import iter_op
102+
from mypyc.primitives.generic_ops import iter_op, name_op
102103
from mypyc.primitives.list_ops import list_append_op, list_extend_op, list_slice_op
103104
from mypyc.primitives.misc_ops import ellipsis_op, get_module_dict_op, new_slice_op, type_op
104105
from mypyc.primitives.registry import builtin_names
@@ -218,6 +219,13 @@ def transform_member_expr(builder: IRBuilder, expr: MemberExpr) -> Value:
218219
obj = builder.accept(expr.expr, can_borrow=can_borrow)
219220
rtype = builder.node_type(expr)
220221

222+
if (
223+
is_object_rprimitive(obj.type)
224+
and expr.name == "__name__"
225+
and builder.options.capi_version >= (3, 11)
226+
):
227+
return builder.primitive_op(name_op, [obj], expr.line)
228+
221229
# Special case: for named tuples transform attribute access to faster index access.
222230
typ = get_proper_type(builder.types.get(expr.expr))
223231
if isinstance(typ, TupleType) and typ.partial_fallback.type.is_named_tuple:

mypyc/lib-rt/CPy.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,10 @@ PyObject *CPy_GetANext(PyObject *aiter);
931931
void CPy_SetTypeAliasTypeComputeFunction(PyObject *alias, PyObject *compute_value);
932932
void CPyTrace_LogEvent(const char *location, const char *line, const char *op, const char *details);
933933

934+
#if CPY_3_11_FEATURES
935+
PyObject *CPy_GetName(PyObject *obj);
936+
#endif
937+
934938
#if CPY_3_14_FEATURES
935939
void CPy_SetImmortal(PyObject *obj);
936940
#endif

mypyc/lib-rt/misc_ops.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,20 @@ PyObject *CPy_GetANext(PyObject *aiter)
10451045
return NULL;
10461046
}
10471047

1048+
#if CPY_3_11_FEATURES
1049+
1050+
// Return obj.__name__ (specialized to type objects, which are the most common target).
1051+
PyObject *CPy_GetName(PyObject *obj) {
1052+
if (PyType_Check(obj)) {
1053+
return PyType_GetName((PyTypeObject *)obj);
1054+
}
1055+
_Py_IDENTIFIER(__name__);
1056+
PyObject *name = _PyUnicode_FromId(&PyId___name__); /* borrowed */
1057+
return PyObject_GetAttr(obj, name);
1058+
}
1059+
1060+
#endif
1061+
10481062
#ifdef MYPYC_LOG_TRACE
10491063

10501064
// This is only compiled in if trace logging is enabled by user

mypyc/lib-rt/mypyc_util.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ static inline CPyTagged CPyTagged_ShortFromSsize_t(Py_ssize_t x) {
140140
}
141141

142142
// Are we targeting Python 3.X or newer?
143+
#define CPY_3_11_FEATURES (PY_VERSION_HEX >= 0x030b0000)
143144
#define CPY_3_12_FEATURES (PY_VERSION_HEX >= 0x030c0000)
144145
#define CPY_3_14_FEATURES (PY_VERSION_HEX >= 0x030e0000)
145146

mypyc/primitives/generic_ops.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
ERR_NEG_INT,
2727
binary_op,
2828
custom_op,
29+
custom_primitive_op,
2930
function_op,
3031
method_op,
3132
unary_op,
@@ -382,3 +383,12 @@
382383
c_function_name="CPy_GetANext",
383384
error_kind=ERR_MAGIC,
384385
)
386+
387+
# x.__name__ (requires Python 3.11+)
388+
name_op = custom_primitive_op(
389+
name="__name__",
390+
arg_types=[object_rprimitive],
391+
return_type=object_rprimitive,
392+
c_function_name="CPy_GetName",
393+
error_kind=ERR_MAGIC,
394+
)

mypyc/test-data/irbuild-classes.test

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,3 +1468,51 @@ L1:
14681468
L2:
14691469
r2 = self == r0
14701470
return r2
1471+
1472+
[case testTypeObjectName_python3_11]
1473+
from typing import Any
1474+
1475+
class C: pass
1476+
class D(C): pass
1477+
1478+
def n1(t: type[object]) -> str:
1479+
return t.__name__
1480+
1481+
def n2(t: Any) -> str:
1482+
return t.__name__
1483+
1484+
def n3() -> str:
1485+
return C.__name__
1486+
1487+
def n4(t: type[C]) -> str:
1488+
return t.__name__
1489+
[out]
1490+
def n1(t):
1491+
t, r0 :: object
1492+
r1 :: str
1493+
L0:
1494+
r0 = CPy_GetName(t)
1495+
r1 = cast(str, r0)
1496+
return r1
1497+
def n2(t):
1498+
t, r0 :: object
1499+
r1 :: str
1500+
L0:
1501+
r0 = CPy_GetName(t)
1502+
r1 = cast(str, r0)
1503+
return r1
1504+
def n3():
1505+
r0, r1 :: object
1506+
r2 :: str
1507+
L0:
1508+
r0 = __main__.C :: type
1509+
r1 = CPy_GetName(r0)
1510+
r2 = cast(str, r1)
1511+
return r2
1512+
def n4(t):
1513+
t, r0 :: object
1514+
r1 :: str
1515+
L0:
1516+
r0 = CPy_GetName(t)
1517+
r1 = cast(str, r0)
1518+
return r1

mypyc/test-data/run-classes.test

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3115,3 +3115,51 @@ f(C(1, "yes"))
31153115
[out]
31163116
B 1
31173117
C yes
3118+
3119+
[case testTypeObjectName]
3120+
from typing import Any
3121+
import re
3122+
3123+
from dynamic import E, foo, Thing
3124+
3125+
class C: pass
3126+
class D(C): pass
3127+
3128+
def type_name(t: type[object]) -> str:
3129+
return t.__name__
3130+
3131+
def any_name(x: Any) -> str:
3132+
return x.__name__
3133+
3134+
def assert_type_name(x: Any) -> None:
3135+
assert type_name(x) == getattr(x, "__name__")
3136+
assert any_name(x) == getattr(x, "__name__")
3137+
3138+
def assert_any_name(x: Any) -> None:
3139+
assert any_name(x) == getattr(x, "__name__")
3140+
3141+
def test_type_name() -> None:
3142+
assert_type_name(C)
3143+
assert_type_name(D)
3144+
assert_type_name(int)
3145+
assert_type_name(E)
3146+
assert_type_name(re.Pattern)
3147+
3148+
def test_module_name() -> None:
3149+
assert_any_name(re)
3150+
3151+
def test_function_name() -> None:
3152+
assert_any_name(any_name)
3153+
assert_any_name(foo)
3154+
3155+
def test_obj_name() -> None:
3156+
assert_any_name(Thing())
3157+
3158+
[file dynamic.py]
3159+
class E: pass
3160+
3161+
def foo(): pass
3162+
3163+
class Thing:
3164+
def __init__(self):
3165+
self.__name__ = "xyz"

0 commit comments

Comments
 (0)