Skip to content

Commit d3ef642

Browse files
authored
[mypyc] Replace integer floor division by a power of two with a shift (#12870)
In a microbenchmark right shift was a bit faster.
1 parent 0054046 commit d3ef642

File tree

3 files changed

+69
-0
lines changed

3 files changed

+69
-0
lines changed

mypyc/irbuild/expression.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,8 @@ def transform_op_expr(builder: IRBuilder, expr: OpExpr) -> Value:
408408
# Special case some int ops to allow borrowing operands.
409409
if (is_int_rprimitive(builder.node_type(expr.left))
410410
and is_int_rprimitive(builder.node_type(expr.right))):
411+
if expr.op == '//':
412+
expr = try_optimize_int_floor_divide(expr)
411413
if expr.op in int_borrow_friendly_op:
412414
borrow_left = is_borrow_friendly_expr(builder, expr.right)
413415
left = builder.accept(expr.left, can_borrow=borrow_left)
@@ -419,6 +421,17 @@ def transform_op_expr(builder: IRBuilder, expr: OpExpr) -> Value:
419421
)
420422

421423

424+
def try_optimize_int_floor_divide(expr: OpExpr) -> OpExpr:
425+
"""Replace // with a power of two with a right shift, if possible."""
426+
if not isinstance(expr.right, IntExpr):
427+
return expr
428+
divisor = expr.right.value
429+
shift = divisor.bit_length() - 1
430+
if 0 < shift < 28 and divisor == (1 << shift):
431+
return OpExpr('>>', expr.left, IntExpr(shift))
432+
return expr
433+
434+
422435
def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
423436
index = expr.index
424437
base_type = builder.node_type(expr.base)

mypyc/test-data/irbuild-int.test

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,41 @@ L5:
117117
r8 = x
118118
L6:
119119
return r8
120+
121+
[case testIntFloorDivideByPowerOfTwo]
122+
def divby1(x: int) -> int:
123+
return x // 1
124+
def divby2(x: int) -> int:
125+
return x // 2
126+
def divby3(x: int) -> int:
127+
return x // 3
128+
def divby4(x: int) -> int:
129+
return x // 4
130+
def divby8(x: int) -> int:
131+
return x // 8
132+
[out]
133+
def divby1(x):
134+
x, r0 :: int
135+
L0:
136+
r0 = CPyTagged_FloorDivide(x, 2)
137+
return r0
138+
def divby2(x):
139+
x, r0 :: int
140+
L0:
141+
r0 = CPyTagged_Rshift(x, 2)
142+
return r0
143+
def divby3(x):
144+
x, r0 :: int
145+
L0:
146+
r0 = CPyTagged_FloorDivide(x, 6)
147+
return r0
148+
def divby4(x):
149+
x, r0 :: int
150+
L0:
151+
r0 = CPyTagged_Rshift(x, 4)
152+
return r0
153+
def divby8(x):
154+
x, r0 :: int
155+
L0:
156+
r0 = CPyTagged_Rshift(x, 6)
157+
return r0

mypyc/test-data/run-integers.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ assert test_isinstance_int_and_not_bool(True) == False
172172
assert test_isinstance_int_and_not_bool(1) == True
173173

174174
[case testIntOps]
175+
from typing import Any
176+
175177
def check_and(x: int, y: int) -> None:
176178
# eval() can be trusted to calculate expected result
177179
expected = eval('{} & {}'.format(x, y))
@@ -428,6 +430,22 @@ def test_constant_fold() -> None:
428430
n64 = -(1 << 64) + int()
429431
assert n64 == -(1 << 64)
430432

433+
def div_by_2(x: int) -> int:
434+
return x // 2
435+
436+
def div_by_3(x: int) -> int:
437+
return x // 3
438+
439+
def div_by_4(x: int) -> int:
440+
return x // 4
441+
442+
def test_floor_divide_by_literal() -> None:
443+
for i in range(-100, 100):
444+
i_boxed: Any = i
445+
assert div_by_2(i) == i_boxed // int('2')
446+
assert div_by_3(i) == i_boxed // int('3')
447+
assert div_by_4(i) == i_boxed // int('4')
448+
431449
[case testIntMinMax]
432450
def test_int_min_max() -> None:
433451
x: int = 200

0 commit comments

Comments
 (0)