Skip to content

Commit f83ec9f

Browse files
authored
[mypyc] Speed up implicit __ne__ method (#19759)
Avoid the use of `PyObject_Not` if `__eq__` returns a boolean (which is common). Also skip incref on the bool/bit result on 3.12+, since the object is immortal. This speeds up a microbenchmark that repeatedly does `!=` operations by 20%.
1 parent 6a97dc6 commit f83ec9f

File tree

7 files changed

+155
-32
lines changed

7 files changed

+155
-32
lines changed

mypyc/codegen/emitfunc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
RStruct,
9090
RTuple,
9191
RType,
92-
is_bool_rprimitive,
92+
is_bool_or_bit_rprimitive,
9393
is_int32_rprimitive,
9494
is_int64_rprimitive,
9595
is_int_rprimitive,
@@ -633,7 +633,7 @@ def emit_method_call(self, dest: str, op_obj: Value, name: str, op_args: list[Va
633633
def visit_inc_ref(self, op: IncRef) -> None:
634634
if (
635635
isinstance(op.src, Box)
636-
and (is_none_rprimitive(op.src.src.type) or is_bool_rprimitive(op.src.src.type))
636+
and (is_none_rprimitive(op.src.src.type) or is_bool_or_bit_rprimitive(op.src.src.type))
637637
and HAVE_IMMORTAL
638638
):
639639
# On Python 3.12+, None/True/False are immortal, and we can skip inc ref

mypyc/irbuild/classdef.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,9 @@ def gen_glue_ne_method(builder: IRBuilder, cls: ClassIR, line: int) -> None:
845845
)
846846
builder.activate_block(regular_block)
847847
rettype = bool_rprimitive if return_bool and strict_typing else object_rprimitive
848-
retval = builder.coerce(builder.unary_op(eqval, "not", line), rettype, line)
848+
retval = builder.coerce(
849+
builder.builder.unary_not(eqval, line, likely_bool=True), rettype, line
850+
)
849851
builder.add(Return(retval))
850852
builder.activate_block(not_implemented_block)
851853
builder.add(Return(not_implemented))

mypyc/irbuild/ll_builder.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
KeepAlive,
5757
LoadAddress,
5858
LoadErrorValue,
59+
LoadGlobal,
5960
LoadLiteral,
6061
LoadMem,
6162
LoadStatic,
@@ -108,6 +109,7 @@
108109
is_int_rprimitive,
109110
is_list_rprimitive,
110111
is_none_rprimitive,
112+
is_object_rprimitive,
111113
is_set_rprimitive,
112114
is_short_int_rprimitive,
113115
is_str_rprimitive,
@@ -1318,6 +1320,14 @@ def none_object(self) -> Value:
13181320
"""Load Python None value (type: object_rprimitive)."""
13191321
return self.add(LoadAddress(none_object_op.type, none_object_op.src, line=-1))
13201322

1323+
def true_object(self) -> Value:
1324+
"""Load Python True object (type: object_rprimitive)."""
1325+
return self.add(LoadGlobal(object_rprimitive, "Py_True"))
1326+
1327+
def false_object(self) -> Value:
1328+
"""Load Python False object (type: object_rprimitive)."""
1329+
return self.add(LoadGlobal(object_rprimitive, "Py_False"))
1330+
13211331
def load_int(self, value: int) -> Value:
13221332
"""Load a tagged (Python) integer literal value."""
13231333
if value > MAX_LITERAL_SHORT_INT or value < MIN_LITERAL_SHORT_INT:
@@ -1663,12 +1673,39 @@ def _non_specialized_unary_op(self, value: Value, op: str, line: int) -> Value:
16631673
assert target, "Unsupported unary operation: %s" % op
16641674
return target
16651675

1666-
def unary_not(self, value: Value, line: int) -> Value:
1667-
"""Perform unary 'not'."""
1676+
def unary_not(self, value: Value, line: int, *, likely_bool: bool = False) -> Value:
1677+
"""Perform unary 'not'.
1678+
1679+
Args:
1680+
likely_bool: The operand is likely a bool value, even if the type is something
1681+
more general, so specialize for bool values
1682+
"""
16681683
typ = value.type
16691684
if is_bool_or_bit_rprimitive(typ):
16701685
mask = Integer(1, typ, line)
16711686
return self.int_op(typ, value, mask, IntOp.XOR, line)
1687+
if likely_bool and is_object_rprimitive(typ):
1688+
# First quickly check if it's a bool, and otherwise fall back to generic op.
1689+
res = Register(bit_rprimitive)
1690+
false, not_false, true, other = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock()
1691+
out = BasicBlock()
1692+
cmp = self.add(ComparisonOp(value, self.true_object(), ComparisonOp.EQ, line))
1693+
self.add(Branch(cmp, false, not_false, Branch.BOOL))
1694+
self.activate_block(false)
1695+
self.add(Assign(res, self.false()))
1696+
self.goto(out)
1697+
self.activate_block(not_false)
1698+
cmp = self.add(ComparisonOp(value, self.false_object(), ComparisonOp.EQ, line))
1699+
self.add(Branch(cmp, true, other, Branch.BOOL))
1700+
self.activate_block(true)
1701+
self.add(Assign(res, self.true()))
1702+
self.goto(out)
1703+
self.activate_block(other)
1704+
val = self._non_specialized_unary_op(value, "not", line)
1705+
self.add(Assign(res, val))
1706+
self.goto(out)
1707+
self.activate_block(out)
1708+
return res
16721709
return self._non_specialized_unary_op(value, "not", line)
16731710

16741711
def unary_minus(self, value: Value, line: int) -> Value:

mypyc/test-data/irbuild-basic.test

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2347,22 +2347,42 @@ def A.__ne__(__mypyc_self__, rhs):
23472347
__mypyc_self__ :: __main__.A
23482348
rhs, r0, r1 :: object
23492349
r2 :: bit
2350-
r3 :: i32
2351-
r4 :: bit
2352-
r5 :: bool
2350+
r3 :: object
2351+
r4, r5 :: bit
23532352
r6 :: object
2353+
r7 :: bit
2354+
r8 :: i32
2355+
r9 :: bit
2356+
r10 :: bool
2357+
r11 :: object
23542358
L0:
23552359
r0 = __mypyc_self__.__eq__(rhs)
23562360
r1 = load_address _Py_NotImplementedStruct
23572361
r2 = r0 == r1
2358-
if r2 goto L2 else goto L1 :: bool
2362+
if r2 goto L7 else goto L1 :: bool
23592363
L1:
2360-
r3 = PyObject_Not(r0)
2361-
r4 = r3 >= 0 :: signed
2362-
r5 = truncate r3: i32 to builtins.bool
2363-
r6 = box(bool, r5)
2364-
return r6
2364+
r3 = load_global Py_True :: static
2365+
r4 = r0 == r3
2366+
if r4 goto L2 else goto L3 :: bool
23652367
L2:
2368+
r5 = 0
2369+
goto L6
2370+
L3:
2371+
r6 = load_global Py_False :: static
2372+
r7 = r0 == r6
2373+
if r7 goto L4 else goto L5 :: bool
2374+
L4:
2375+
r5 = 1
2376+
goto L6
2377+
L5:
2378+
r8 = PyObject_Not(r0)
2379+
r9 = r8 >= 0 :: signed
2380+
r10 = truncate r8: i32 to builtins.bool
2381+
r5 = r10
2382+
L6:
2383+
r11 = box(bit, r5)
2384+
return r11
2385+
L7:
23662386
return r1
23672387

23682388
[case testDecorators_toplevel]

mypyc/test-data/irbuild-classes.test

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -854,22 +854,42 @@ def Base.__ne__(__mypyc_self__, rhs):
854854
__mypyc_self__ :: __main__.Base
855855
rhs, r0, r1 :: object
856856
r2 :: bit
857-
r3 :: i32
858-
r4 :: bit
859-
r5 :: bool
857+
r3 :: object
858+
r4, r5 :: bit
860859
r6 :: object
860+
r7 :: bit
861+
r8 :: i32
862+
r9 :: bit
863+
r10 :: bool
864+
r11 :: object
861865
L0:
862866
r0 = __mypyc_self__.__eq__(rhs)
863867
r1 = load_address _Py_NotImplementedStruct
864868
r2 = r0 == r1
865-
if r2 goto L2 else goto L1 :: bool
869+
if r2 goto L7 else goto L1 :: bool
866870
L1:
867-
r3 = PyObject_Not(r0)
868-
r4 = r3 >= 0 :: signed
869-
r5 = truncate r3: i32 to builtins.bool
870-
r6 = box(bool, r5)
871-
return r6
871+
r3 = load_global Py_True :: static
872+
r4 = r0 == r3
873+
if r4 goto L2 else goto L3 :: bool
872874
L2:
875+
r5 = 0
876+
goto L6
877+
L3:
878+
r6 = load_global Py_False :: static
879+
r7 = r0 == r6
880+
if r7 goto L4 else goto L5 :: bool
881+
L4:
882+
r5 = 1
883+
goto L6
884+
L5:
885+
r8 = PyObject_Not(r0)
886+
r9 = r8 >= 0 :: signed
887+
r10 = truncate r8: i32 to builtins.bool
888+
r5 = r10
889+
L6:
890+
r11 = box(bit, r5)
891+
return r11
892+
L7:
873893
return r1
874894
def Derived.__eq__(self, other):
875895
self :: __main__.Derived
@@ -979,22 +999,42 @@ def Derived.__ne__(__mypyc_self__, rhs):
979999
__mypyc_self__ :: __main__.Derived
9801000
rhs, r0, r1 :: object
9811001
r2 :: bit
982-
r3 :: i32
983-
r4 :: bit
984-
r5 :: bool
1002+
r3 :: object
1003+
r4, r5 :: bit
9851004
r6 :: object
1005+
r7 :: bit
1006+
r8 :: i32
1007+
r9 :: bit
1008+
r10 :: bool
1009+
r11 :: object
9861010
L0:
9871011
r0 = __mypyc_self__.__eq__(rhs)
9881012
r1 = load_address _Py_NotImplementedStruct
9891013
r2 = r0 == r1
990-
if r2 goto L2 else goto L1 :: bool
1014+
if r2 goto L7 else goto L1 :: bool
9911015
L1:
992-
r3 = PyObject_Not(r0)
993-
r4 = r3 >= 0 :: signed
994-
r5 = truncate r3: i32 to builtins.bool
995-
r6 = box(bool, r5)
996-
return r6
1016+
r3 = load_global Py_True :: static
1017+
r4 = r0 == r3
1018+
if r4 goto L2 else goto L3 :: bool
9971019
L2:
1020+
r5 = 0
1021+
goto L6
1022+
L3:
1023+
r6 = load_global Py_False :: static
1024+
r7 = r0 == r6
1025+
if r7 goto L4 else goto L5 :: bool
1026+
L4:
1027+
r5 = 1
1028+
goto L6
1029+
L5:
1030+
r8 = PyObject_Not(r0)
1031+
r9 = r8 >= 0 :: signed
1032+
r10 = truncate r8: i32 to builtins.bool
1033+
r5 = r10
1034+
L6:
1035+
r11 = box(bit, r5)
1036+
return r11
1037+
L7:
9981038
return r1
9991039

10001040
[case testDefaultVars]

mypyc/test-data/run-dunders-special.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ class UsesNotImplemented:
88

99
def test_not_implemented() -> None:
1010
assert UsesNotImplemented() != object()
11+
x = UsesNotImplemented() == object()
12+
assert not x

mypyc/test-data/run-dunders.test

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,3 +965,25 @@ def test_final() -> None:
965965
assert b + 3 == 9
966966
assert (a < A(5)) is False
967967
assert (b < A(5)) is True
968+
969+
[case testDundersEq]
970+
class Eq:
971+
def __init__(self, x: int) -> None:
972+
self.x = x
973+
974+
def __eq__(self, other: object) -> bool:
975+
if not isinstance(other, Eq):
976+
return NotImplemented
977+
return self.x == other.x
978+
979+
def eq(x: Eq, y: Eq) -> bool:
980+
return x == y
981+
982+
def ne(x: Eq, y: Eq) -> bool:
983+
return x != y
984+
985+
def test_equality_with_implicit_ne() -> None:
986+
assert eq(Eq(1), Eq(1))
987+
assert not eq(Eq(1), Eq(2))
988+
assert ne(Eq(1), Eq(2))
989+
assert not ne(Eq(1), Eq(1))

0 commit comments

Comments
 (0)