Skip to content

Commit

Permalink
[mypyc] Avoid boxing/unboxing when coercing between tuple types (#14899)
Browse files Browse the repository at this point in the history
Instead, coerce each tuple item individually. This makes some coercions
between tuple types much faster, primarily because there is less (or no)
allocation and deallocation going on.

This speeds up the raytrace benchmark by about 7% (when using native
floats).

Related to mypyc/mypyc#99.
  • Loading branch information
JukkaL authored Mar 20, 2023
1 parent 4a54894 commit 1a8ea61
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 11 deletions.
33 changes: 28 additions & 5 deletions mypyc/irbuild/ll_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
SetMem,
Truncate,
TupleGet,
TupleSet,
Unbox,
Unreachable,
Value,
Expand Down Expand Up @@ -354,11 +355,33 @@ def coerce(
return Float(float(src.value))
elif is_tagged(src_type) and is_float_rprimitive(target_type):
return self.int_to_float(src, line)
else:
# To go from one unboxed type to another, we go through a boxed
# in-between value, for simplicity.
tmp = self.box(src)
return self.unbox_or_cast(tmp, target_type, line)
elif (
isinstance(src_type, RTuple)
and isinstance(target_type, RTuple)
and len(src_type.types) == len(target_type.types)
):
# Coerce between two tuple types by coercing each item separately
values = []
for i in range(len(src_type.types)):
v = None
if isinstance(src, TupleSet):
item = src.items[i]
# We can't reuse register values, since they can be modified.
if not isinstance(item, Register):
v = item
if v is None:
v = TupleGet(src, i)
self.add(v)
values.append(v)
return self.add(
TupleSet(
[self.coerce(v, t, line) for v, t in zip(values, target_type.types)], line
)
)
# To go between any other unboxed types, we go through a boxed
# in-between value, for simplicity.
tmp = self.box(src)
return self.unbox_or_cast(tmp, target_type, line)
if (not src_type.is_unboxed and target_type.is_unboxed) or not is_subtype(
src_type, target_type
):
Expand Down
20 changes: 20 additions & 0 deletions mypyc/test-data/irbuild-float.test
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,23 @@ def init(n: int) -> None:
# narrowing assignments, generate errors here
x: float = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float")
y: float = 5 # E: Incompatible value representations in assignment (expression has type "int", variable has type "float")

[case testFloatCoerceTupleFromIntValues]
from __future__ import annotations

def f(x: int) -> None:
t: tuple[float, float, float] = (x, 2.5, -7)
[out]
def f(x):
x :: int
r0 :: tuple[int, float, int]
r1 :: int
r2 :: float
r3, t :: tuple[float, float, float]
L0:
r0 = (x, 2.5, -14)
r1 = r0[0]
r2 = CPyFloat_FromTagged(r1)
r3 = (r2, 2.5, -7.0)
t = r3
return 1
84 changes: 78 additions & 6 deletions mypyc/test-data/irbuild-i64.test
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,6 @@ def f(x: i64, y: i64) -> Tuple[i64, i64]:
return x, y

def g() -> Tuple[i64, i64]:
# TODO: Avoid boxing and unboxing
return 1, 2

def h() -> i64:
Expand All @@ -666,13 +665,11 @@ L0:
return r0
def g():
r0 :: tuple[int, int]
r1 :: object
r2 :: tuple[int64, int64]
r1 :: tuple[int64, int64]
L0:
r0 = (2, 4)
r1 = box(tuple[int, int], r0)
r2 = unbox(tuple[int64, int64], r1)
return r2
r1 = (1, 2)
return r1
def h():
r0 :: tuple[int64, int64]
r1, x, r2, y :: int64
Expand Down Expand Up @@ -2081,3 +2078,78 @@ L2:
r6 = r5.a
keep_alive x
return r6

[case testI64ConvertBetweenTuples_64bit]
from __future__ import annotations
from mypy_extensions import i64

def f(t: tuple[int, i64, int]) -> None:
tt: tuple[int, i64, i64] = t

def g(n: int) -> None:
t: tuple[i64, i64] = (1, n)
[out]
def f(t):
t :: tuple[int, int64, int]
r0 :: int
r1 :: int64
r2 :: int
r3 :: native_int
r4 :: bit
r5, r6 :: int64
r7 :: ptr
r8 :: c_ptr
r9 :: int64
r10, tt :: tuple[int, int64, int64]
L0:
r0 = t[0]
r1 = t[1]
r2 = t[2]
r3 = r2 & 1
r4 = r3 == 0
if r4 goto L1 else goto L2 :: bool
L1:
r5 = r2 >> 1
r6 = r5
goto L3
L2:
r7 = r2 ^ 1
r8 = r7
r9 = CPyLong_AsInt64(r8)
r6 = r9
keep_alive r2
L3:
r10 = (r0, r1, r6)
tt = r10
return 1
def g(n):
n :: int
r0 :: tuple[int, int]
r1 :: int
r2 :: native_int
r3 :: bit
r4, r5 :: int64
r6 :: ptr
r7 :: c_ptr
r8 :: int64
r9, t :: tuple[int64, int64]
L0:
r0 = (2, n)
r1 = r0[1]
r2 = r1 & 1
r3 = r2 == 0
if r3 goto L1 else goto L2 :: bool
L1:
r4 = r1 >> 1
r5 = r4
goto L3
L2:
r6 = r1 ^ 1
r7 = r6
r8 = CPyLong_AsInt64(r7)
r5 = r8
keep_alive r1
L3:
r9 = (1, r5)
t = r9
return 1
8 changes: 8 additions & 0 deletions mypyc/test-data/run-floats.test
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Test cases for floats (compile and run)

[case testFloatOps]
from __future__ import annotations
from typing import Any, cast
from typing_extensions import Final
from testutil import assertRaises, float_vals, FLOAT_MAGIC
Expand Down Expand Up @@ -329,6 +330,13 @@ def test_undefined_local_var() -> None:
with assertRaises(UnboundLocalError, 'local variable "y2" referenced before assignment'):
print(y2)

def test_tuples() -> None:
t1: tuple[float, float] = (1.5, 2.5)
assert t1 == tuple([1.5, 2.5])
n = int() + 5
t2: tuple[float, float, float, float] = (n, 1.5, -7, -113)
assert t2 == tuple([5.0, 1.5, -7.0, -113.0])

[case testFloatGlueMethodsAndInheritance]
from typing import Any
from typing_extensions import Final
Expand Down

0 comments on commit 1a8ea61

Please sign in to comment.