Skip to content

Commit 17a8ebd

Browse files
author
Release Manager
committed
gh-37369: Faster scalar multiplication for python types There is an annoying problem at the moment. If you compute the scalar multiplication of a point on an elliptic curve over a finite field, a fast call to Pari is made when the scalar is a Sage type, but when the scalar is a Python `int` then the multiplication for `_acted_upon_` is skipped by the coercion system and `__mul__` is called instead which is a much slower Sage implementation using double and add. As an example of this see the following run times. ```py ┌────────────────────────────────────────────────────────────────────┐ │ SageMath version 10.2, Release Date: 2023-12-03 │ │ Using Python 3.11.4. Type "help()" for help. │ └────────────────────────────────────────────────────────────────────┘ sage: F = GF(2**127 - 1) sage: E = EllipticCurve(F, [0,6,0,1,0]) sage: P = E.random_point() sage: k = randint(0, 2**127) sage: sage: %timeit int(k) * P 3.77 ms ± 98.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) sage: %timeit P * int(k) 3.74 ms ± 12.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) sage: %timeit ZZ(k) * P 235 µs ± 2.62 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) sage: %timeit P * ZZ(k) 234 µs ± 4.81 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) ``` This is particularly annoying if you write Sagemath code in a `.py` file and write `2**100 * P`, which will be parsed such that `2**100` is an `int` and the very slow method is used. For this 127-bit characteristic example above, the slowdown is more than a factor of 10. This PR writes small methods for `__mul__` and `__rmul__` which wrap `_acted_upon_` and allow the speed up for Python `int` values. This results in the following times for the same example: ```py ┌────────────────────────────────────────────────────────────────────┐ │ SageMath version 10.3.beta8, Release Date: 2024-02-13 │ │ Using Python 3.11.4. Type "help()" for help. │ └────────────────────────────────────────────────────────────────────┘ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ Warning: this is a prerelease version, and it may be unstable. ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ sage: F = GF(2**127 - 1) sage: E = EllipticCurve(F, [0,6,0,1,0]) sage: P = E.random_point() sage: k = randint(0, 2**127) sage: sage: %timeit int(k) * P 222 µs ± 1.6 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) sage: %timeit P * int(k) 225 µs ± 4.54 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) sage: %timeit ZZ(k) * P 219 µs ± 1.88 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) sage: %timeit P * ZZ(k) 230 µs ± 15.6 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) ``` URL: #37369 Reported by: Giacomo Pope Reviewer(s): John Cremona, nbruin
2 parents 2217e9e + 137cb27 commit 17a8ebd

File tree

1 file changed

+46
-0
lines changed

1 file changed

+46
-0
lines changed

src/sage/structure/parent.pyx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2635,6 +2635,25 @@ cdef class Parent(sage.structure.category_object.CategoryObject):
26352635
26362636
sage: print(coercion_model.get_action(E, ZZ, operator.pow)) # needs sage.schemes
26372637
None
2638+
2639+
::
2640+
2641+
With Pull Request #37369, registered multiplication actions by
2642+
`ZZ` are also discovered and used when a Python ``int`` is multiplied.
2643+
Previously, it was only discovering the generic Integer Multiplication
2644+
Action that all additive groups have. As a result, optimised
2645+
implementations, such as the use of Pari for scalar multiplication of points
2646+
on elliptic curves over Finite Fields, was not used if an ``int``
2647+
multiplied a point, resulting in a 10x slowdown for large characteristic::
2648+
2649+
sage: E = EllipticCurve(GF(17),[1,1])
2650+
sage: coercion_model.discover_action(ZZ, E, operator.mul)
2651+
Left action by Integer Ring on Elliptic Curve defined by y^2 = x^3 + x + 1 over Finite Field of size 17
2652+
sage: coercion_model.discover_action(int, E, operator.mul)
2653+
Left action by Integer Ring on Elliptic Curve defined by y^2 = x^3 + x + 1 over Finite Field of size 17
2654+
with precomposition on left by Native morphism:
2655+
From: Set of Python objects of class 'int'
2656+
To: Integer Ring
26382657
"""
26392658
# G acts on S, G -> G', R -> S => G' acts on R (?)
26402659
# NO! ZZ[x,y] acts on Matrices(ZZ[x]) but ZZ[y] does not.
@@ -2678,6 +2697,33 @@ cdef class Parent(sage.structure.category_object.CategoryObject):
26782697
return action
26792698

26802699
if parent_is_integers(S) and not self.has_coerce_map_from(S):
2700+
# Try the above again, but first coerce integer-like type to Integer
2701+
# with a connecting coercion
2702+
global _Integer
2703+
if _Integer is None:
2704+
from sage.rings.integer import Integer as _Integer
2705+
ZZ_el = _Integer(S_el)
2706+
ZZ = ZZ_el.parent()
2707+
2708+
# Now we check if there's an element action from Integers
2709+
action = detect_element_action(self, ZZ, self_on_left, self_el, ZZ_el)
2710+
2711+
# When this is not None, we can do the Precomposed action
2712+
if action is not None:
2713+
# Compute the coercion from whatever S is to the Integer class
2714+
# This should always work because parent_is_integers(S) is True
2715+
# but it fails when S is gmpy2.mpz.
2716+
# TODO: should we also patch _internal_coerce_map_from so that
2717+
# there's a map from gmpy2.mpz to ZZ?
2718+
connecting = ZZ._internal_coerce_map_from(S)
2719+
if connecting is not None:
2720+
if self_on_left:
2721+
action = PrecomposedAction(action, None, connecting)
2722+
else:
2723+
action = PrecomposedAction(action, connecting, None)
2724+
return action
2725+
2726+
# Otherwise, we do the most basic IntegerMulAction
26812727
from sage.structure.coerce_actions import IntegerMulAction
26822728
try:
26832729
return IntegerMulAction(S, self, not self_on_left, self_el)

0 commit comments

Comments
 (0)