Skip to content

Commit 23965ab

Browse files
authored
[mypyc] Speed up unary "not" (#19774)
Specialize "not" for common primitive types, optional types and native instance types. Also specialize variable-length tuple in a boolean context (while working on "not" I noticed that this wasn't specialized). This appears to speed up self check by 0.7%, but this is only barely above the noise floor.
1 parent acacc9e commit 23965ab

File tree

4 files changed

+342
-5
lines changed

4 files changed

+342
-5
lines changed

mypyc/irbuild/ll_builder.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@
9494
c_pyssize_t_rprimitive,
9595
c_size_t_rprimitive,
9696
check_native_int_range,
97-
dict_rprimitive,
9897
float_rprimitive,
9998
int_rprimitive,
10099
is_bool_or_bit_rprimitive,
@@ -110,13 +109,13 @@
110109
is_list_rprimitive,
111110
is_none_rprimitive,
112111
is_object_rprimitive,
112+
is_optional_type,
113113
is_set_rprimitive,
114114
is_short_int_rprimitive,
115115
is_str_rprimitive,
116116
is_tagged,
117117
is_tuple_rprimitive,
118118
is_uint8_rprimitive,
119-
list_rprimitive,
120119
none_rprimitive,
121120
object_pointer_rprimitive,
122121
object_rprimitive,
@@ -1684,6 +1683,44 @@ def unary_not(self, value: Value, line: int, *, likely_bool: bool = False) -> Va
16841683
if is_bool_or_bit_rprimitive(typ):
16851684
mask = Integer(1, typ, line)
16861685
return self.int_op(typ, value, mask, IntOp.XOR, line)
1686+
if is_tagged(typ) or is_fixed_width_rtype(typ):
1687+
return self.binary_op(value, Integer(0), "==", line)
1688+
if (
1689+
is_str_rprimitive(typ)
1690+
or is_list_rprimitive(typ)
1691+
or is_tuple_rprimitive(typ)
1692+
or is_dict_rprimitive(typ)
1693+
or isinstance(typ, RInstance)
1694+
):
1695+
bool_val = self.bool_value(value)
1696+
return self.unary_not(bool_val, line)
1697+
if is_optional_type(typ):
1698+
value_typ = optional_value_type(typ)
1699+
assert value_typ
1700+
if (
1701+
is_str_rprimitive(value_typ)
1702+
or is_list_rprimitive(value_typ)
1703+
or is_tuple_rprimitive(value_typ)
1704+
or is_dict_rprimitive(value_typ)
1705+
or isinstance(value_typ, RInstance)
1706+
):
1707+
# 'X | None' type: Check for None first and then specialize for X.
1708+
res = Register(bit_rprimitive)
1709+
cmp = self.add(ComparisonOp(value, self.none_object(), ComparisonOp.EQ, line))
1710+
none, not_none, out = BasicBlock(), BasicBlock(), BasicBlock()
1711+
self.add(Branch(cmp, none, not_none, Branch.BOOL))
1712+
self.activate_block(none)
1713+
self.add(Assign(res, self.true()))
1714+
self.goto(out)
1715+
self.activate_block(not_none)
1716+
val = self.unary_not(
1717+
self.unbox_or_cast(value, value_typ, line, can_borrow=True, unchecked=True),
1718+
line,
1719+
)
1720+
self.add(Assign(res, val))
1721+
self.goto(out)
1722+
self.activate_block(out)
1723+
return res
16871724
if likely_bool and is_object_rprimitive(typ):
16881725
# First quickly check if it's a bool, and otherwise fall back to generic op.
16891726
res = Register(bit_rprimitive)
@@ -1882,10 +1919,12 @@ def bool_value(self, value: Value) -> Value:
18821919
elif is_fixed_width_rtype(value.type):
18831920
zero = Integer(0, value.type)
18841921
result = self.add(ComparisonOp(value, zero, ComparisonOp.NEQ))
1885-
elif is_same_type(value.type, str_rprimitive):
1922+
elif is_str_rprimitive(value.type):
18861923
result = self.call_c(str_check_if_true, [value], value.line)
1887-
elif is_same_type(value.type, list_rprimitive) or is_same_type(
1888-
value.type, dict_rprimitive
1924+
elif (
1925+
is_list_rprimitive(value.type)
1926+
or is_dict_rprimitive(value.type)
1927+
or is_tuple_rprimitive(value.type)
18891928
):
18901929
length = self.builtin_len(value, value.line)
18911930
zero = Integer(0)

mypyc/test-data/irbuild-bool.test

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,181 @@ L0:
473473
r0 = x == y
474474
r1 = r0 ^ 1
475475
return r1
476+
477+
[case testUnaryNotWithPrimitiveTypes]
478+
def not_obj(x: object) -> bool:
479+
return not x
480+
481+
def not_int(x: int) -> bool:
482+
return not x
483+
484+
def not_str(x: str) -> bool:
485+
return not x
486+
487+
def not_list(x: list[int]) -> bool:
488+
return not x
489+
490+
def not_tuple(x: tuple[int, ...]) -> bool:
491+
return not x
492+
493+
def not_dict(x: dict[str, int]) -> bool:
494+
return not x
495+
[out]
496+
def not_obj(x):
497+
x :: object
498+
r0 :: i32
499+
r1 :: bit
500+
r2 :: bool
501+
L0:
502+
r0 = PyObject_Not(x)
503+
r1 = r0 >= 0 :: signed
504+
r2 = truncate r0: i32 to builtins.bool
505+
return r2
506+
def not_int(x):
507+
x :: int
508+
r0 :: bit
509+
L0:
510+
r0 = int_eq x, 0
511+
return r0
512+
def not_str(x):
513+
x :: str
514+
r0, r1 :: bit
515+
L0:
516+
r0 = CPyStr_IsTrue(x)
517+
r1 = r0 ^ 1
518+
return r1
519+
def not_list(x):
520+
x :: list
521+
r0 :: native_int
522+
r1 :: short_int
523+
r2, r3 :: bit
524+
L0:
525+
r0 = var_object_size x
526+
r1 = r0 << 1
527+
r2 = int_ne r1, 0
528+
r3 = r2 ^ 1
529+
return r3
530+
def not_tuple(x):
531+
x :: tuple
532+
r0 :: native_int
533+
r1 :: short_int
534+
r2, r3 :: bit
535+
L0:
536+
r0 = var_object_size x
537+
r1 = r0 << 1
538+
r2 = int_ne r1, 0
539+
r3 = r2 ^ 1
540+
return r3
541+
def not_dict(x):
542+
x :: dict
543+
r0 :: native_int
544+
r1 :: short_int
545+
r2, r3 :: bit
546+
L0:
547+
r0 = PyDict_Size(x)
548+
r1 = r0 << 1
549+
r2 = int_ne r1, 0
550+
r3 = r2 ^ 1
551+
return r3
552+
553+
[case testUnaryNotWithNativeClass]
554+
from __future__ import annotations
555+
556+
class C:
557+
def __bool__(self) -> bool:
558+
return True
559+
560+
def not_c(x: C) -> bool:
561+
return not x
562+
563+
def not_c_opt(x: C | None) -> bool:
564+
return not x
565+
[out]
566+
def C.__bool__(self):
567+
self :: __main__.C
568+
L0:
569+
return 1
570+
def not_c(x):
571+
x :: __main__.C
572+
r0, r1 :: bool
573+
L0:
574+
r0 = x.__bool__()
575+
r1 = r0 ^ 1
576+
return r1
577+
def not_c_opt(x):
578+
x :: union[__main__.C, None]
579+
r0 :: object
580+
r1, r2 :: bit
581+
r3 :: __main__.C
582+
r4, r5 :: bool
583+
L0:
584+
r0 = load_address _Py_NoneStruct
585+
r1 = x == r0
586+
if r1 goto L1 else goto L2 :: bool
587+
L1:
588+
r2 = 1
589+
goto L3
590+
L2:
591+
r3 = unchecked borrow cast(__main__.C, x)
592+
r4 = r3.__bool__()
593+
r5 = r4 ^ 1
594+
r2 = r5
595+
L3:
596+
keep_alive x
597+
return r2
598+
599+
[case testUnaryNotWithOptionalPrimitiveTypes]
600+
from __future__ import annotations
601+
602+
def not_str(x: str | None) -> bool:
603+
return not x
604+
605+
def not_list(x: list[int] | None) -> bool:
606+
return not x
607+
[out]
608+
def not_str(x):
609+
x :: union[str, None]
610+
r0 :: object
611+
r1, r2 :: bit
612+
r3 :: str
613+
r4, r5 :: bit
614+
L0:
615+
r0 = load_address _Py_NoneStruct
616+
r1 = x == r0
617+
if r1 goto L1 else goto L2 :: bool
618+
L1:
619+
r2 = 1
620+
goto L3
621+
L2:
622+
r3 = unchecked borrow cast(str, x)
623+
r4 = CPyStr_IsTrue(r3)
624+
r5 = r4 ^ 1
625+
r2 = r5
626+
L3:
627+
keep_alive x
628+
return r2
629+
def not_list(x):
630+
x :: union[list, None]
631+
r0 :: object
632+
r1, r2 :: bit
633+
r3 :: list
634+
r4 :: native_int
635+
r5 :: short_int
636+
r6, r7 :: bit
637+
L0:
638+
r0 = load_address _Py_NoneStruct
639+
r1 = x == r0
640+
if r1 goto L1 else goto L2 :: bool
641+
L1:
642+
r2 = 1
643+
goto L3
644+
L2:
645+
r3 = unchecked borrow cast(list, x)
646+
r4 = var_object_size r3
647+
r5 = r4 << 1
648+
r6 = int_ne r5, 0
649+
r7 = r6 ^ 1
650+
r2 = r7
651+
L3:
652+
keep_alive x
653+
return r2

mypyc/test-data/irbuild-i64.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,8 @@ def f(x: i64) -> i64:
947947
elif not x:
948948
return 6
949949
return 3
950+
def unary_not(x: i64) -> bool:
951+
return not x
950952
[out]
951953
def f(x):
952954
x :: i64
@@ -964,6 +966,12 @@ L3:
964966
L4:
965967
L5:
966968
return 3
969+
def unary_not(x):
970+
x :: i64
971+
r0 :: bit
972+
L0:
973+
r0 = x == 0
974+
return r0
967975

968976
[case testI64AssignMixed_64bit]
969977
from mypy_extensions import i64

0 commit comments

Comments
 (0)