Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mypyc] Document native floats and integers #14927

Merged
merged 2 commits into from
Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions mypyc/doc/float_operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,40 @@ These ``float`` operations have fast, optimized implementations. Other
floating point operations use generic implementations that are often
slower.

.. note::

At the moment, only a few float operations are optimized. This will
improve in future mypyc releases.

Construction
------------

* Float literal
* ``float(string)``
* ``float(x: int)``
* ``float(x: i64)``
* ``float(x: i32)``
* ``float(x: str)``
* ``float(x: float)`` (no-op)

Operators
---------

* Arithmetic (``+``, ``-``, ``*``, ``/``, ``//``, ``%``)
* Comparisons (``==``, ``!=``, ``<``, etc.)
* Augmented assignment (``x += y``, etc.)

Functions
---------

* ``int(f)``
* ``i32(f)`` (convert to ``i32``)
* ``i64(f)`` (convert to ``i64``)
* ``abs(f)``
* ``math.sin(f)``
* ``math.cos(f)``
* ``math.tan(f)``
* ``math.sqrt(f)``
* ``math.exp(f)``
* ``math.log(f)``
* ``math.floor(f)``
* ``math.ceil(f)``
* ``math.fabs(f)``
* ``math.pow(x, y)``
* ``math.copysign(x, y)``
* ``math.isinf(f)``
* ``math.isnan(f)``
111 changes: 106 additions & 5 deletions mypyc/doc/int_operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,133 @@
Native integer operations
=========================

Operations on ``int`` values that are listed here have fast, optimized
Mypyc supports these integer types:

* ``int`` (arbitrary-precision integer)
* ``i64`` (64-bit signed integer)
* ``i32`` (32-bit signed integer)

``i64`` and ``i32`` are *native integer types* and must be imported
from the ``mypy_extensions`` module. ``int`` corresponds to the Python
``int`` type, but uses a more efficient runtime representation (tagged
pointer). Native integer types are value types. All integer types have
optimized primitive operations, but the native integer types are more
efficient than ``int``, since they don't require range or bounds
checks.

Operations on integers that are listed here have fast, optimized
implementations. Other integer operations use generic implementations
that are often slower. Some operations involving integers and other
types are documented elsewhere, such as list indexing.
that are generally slower. Some operations involving integers and other
types, such as list indexing, are documented elsewhere.

Construction
------------

``int`` type:

* Integer literal
* ``int(x: float)``
* ``int(x: i64)``
* ``int(x: i32)``
* ``int(x: str)``
* ``int(x: str, base: int)``
* ``int(x: int)`` (no-op)

``i64`` type:

* ``i64(x: int)``
* ``i64(x: float)``
* ``i64(x: i32)``
* ``i64(x: str)``
* ``i64(x: str, base: int)``
* ``i64(x: i64)`` (no-op)

``i32`` type:

* ``i32(x: int)``
* ``i32(x: float)``
* ``i32(x: i64)`` (truncate)
* ``i32(x: str)``
* ``i32(x: str, base: int)``
* ``i32(x: i32)`` (no-op)

Conversions from ``int`` to a native integer type raise
``OverflowError`` if the value is too large or small. Conversions from
a wider native integer type to a narrower one truncate the value and never
fail. More generally, operations between native integer types don't
check for overflow.

Implicit conversions
--------------------

``int`` values can be implicitly converted to a native integer type,
for convenience. This means that these are equivalent::

def implicit() -> None:
# Implicit conversion of 0 (int) to i64
x: i64 = 0

def explicit() -> None:
# Explicit conversion of 0 (int) to i64
x = i64(0)

Similarly, a native integer value can be implicitly converted to an
arbitrary-precision integer. These two functions are equivalent::

def implicit(x: i64) -> int:
# Implicit conversion from i64 to int
return x

def explicit(x: i64) -> int:
# Explicit conversion from i64 to int
return int(x)

Operators
---------

* Arithmetic (``+``, ``-``, ``*``, ``//``, ``%``)
* Arithmetic (``+``, ``-``, ``*``, ``//``, ``/``, ``%``)
* Bitwise operations (``&``, ``|``, ``^``, ``<<``, ``>>``, ``~``)
* Comparisons (``==``, ``!=``, ``<``, etc.)
* Augmented assignment (``x += y``, etc.)

If one of the above native integer operations overflows or underflows,
the behavior is undefined. Native integer types should only be used if
all possible values are small enough for the type. For this reason,
the arbitrary-precision ``int`` type is recommended unless the
performance of integer operations is critical.

It's a compile-time error to mix different native integer types in a
binary operation such as addition. An explicit conversion is required::

def add(x: i64, y: i32) -> None:
a = x + y # Error (i64 + i32)
b = x + i64(y) # OK

You can freely mix a native integer value and an arbitrary-precision
``int`` value in an operation. The native integer type is "sticky"
and the ``int`` operand is coerced to the native integer type::

def example(x: i64, y: int) -> None:
a = x * y
# Type of "a" is "i64"
...
b = 1 - x
# Similarly, type of "b" is "i64"

Statements
----------

For loop over range:
For loop over a range is compiled efficiently, if the ``range(...)`` object
is constructed in the for statement (after ``in``):

* ``for x in range(end)``
* ``for x in range(start, end)``
* ``for x in range(start, end, step)``

If one of the arguments to ``range`` in a for loop is a native integer
type, the type of the loop variable is inferred to have this native
integer type, instead of ``int``::

for x in range(i64(n)):
# Type of "x" is "i64"
...
4 changes: 2 additions & 2 deletions mypyc/doc/performance_tips_and_tricks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ These things also tend to be relatively slow:

* Using generator functions

* Using floating point numbers (they are relatively unoptimized)

* Using callable values (i.e. not leveraging early binding to call
functions or methods)

Expand Down Expand Up @@ -160,6 +158,8 @@ Here are examples of features that are fast, in no particular order

* Many integer operations

* Many ``float`` operations

* Booleans

* :ref:`Native list operations <list-ops>`, such as indexing,
Expand Down
89 changes: 85 additions & 4 deletions mypyc/doc/using_type_annotations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ mypyc, and many operations on these types have efficient
implementations:

* ``int`` (:ref:`native operations <int-ops>`)
* ``i64`` (:ref:`documentation <native-ints>`, :ref:`native operations <int-ops>`)
* ``i32`` (:ref:`documentation <native-ints>`, :ref:`native operations <int-ops>`)
* ``float`` (:ref:`native operations <float-ops>`)
* ``bool`` (:ref:`native operations <bool-ops>`)
* ``str`` (:ref:`native operations <str-ops>`)
Expand Down Expand Up @@ -271,7 +273,8 @@ Value and heap types
In CPython, memory for all objects is dynamically allocated on the
heap. All Python types are thus *heap types*. In compiled code, some
types are *value types* -- no object is (necessarily) allocated on the
heap. ``bool``, ``None`` and fixed-length tuples are value types.
heap. ``bool``, ``float``, ``None``, :ref:`native integer types <native-ints>`
and fixed-length tuples are value types.

``int`` is a hybrid. For typical integer values, it is a value
type. Large enough integer values, those that require more than 63
Expand All @@ -287,9 +290,9 @@ Value types have a few differences from heap types:
* Similarly, mypyc transparently changes from a heap-based
representation to a value representation (unboxing).

* Object identity of integers and tuples is not preserved. You should
use ``==`` instead of ``is`` if you are comparing two integers or
fixed-length tuples.
* Object identity of integers, floating point values and tuples is not
preserved. You should use ``==`` instead of ``is`` if you are comparing
two integers, floats or fixed-length tuples.

* When an instance of a subclass of a value type is converted to the
base type, it is implicitly converted to an instance of the target
Expand All @@ -312,3 +315,81 @@ Example::
x = a[0]
# True is converted to 1 on assignment
x = True

Since integers and floating point values have a different runtime
representations and neither can represent all the values of the other
type, type narrowing of floating point values through assignment is
disallowed in compiled code. For consistency, mypyc rejects assigning
an integer value to a float variable even in variable initialization.
An explicit conversion is required.

Examples::

def narrowing(n: int) -> None:
# Error: Incompatible value representations in assignment
# (expression has type "int", variable has type "float")
x: float = 0

y: float = 0.0 # Ok

if f():
y = n # Error
if f():
y = float(n) # Ok

.. _native-ints:

Native integer types
--------------------

You can use the native integer types ``i64`` (64-bit signed integer)
and ``i32`` (32-bit signed integer) if you know that integer values
will always fit within fixed bounds. These types are faster than the
arbitrary-precision ``int`` type, since they don't require overflow
checks on operations. ``i32`` may also use less memory than ``int``
values. The types are imported from the ``mypy_extensions`` module
(installed via ``pip install mypy_extensions``).

Example::

from mypy_extensions import i64

def sum_list(l: list[i64]) -> i64:
s: i64 = 0
for n in l:
s += n
return s

# Implicit conversions from int to i64
print(sum_list([1, 3, 5]))

.. note::

Since there are no overflow checks when performing native integer
arithmetic, the above function could result in an overflow or other
undefined behavior if the sum might not fit within 64 bits.

The behavior when running as interpreted Python program will be
different if there are overflows. Declaring native integer types
have no effect unless code is compiled. Native integer types are
effectively equivalent to ``int`` when interpreted.

Native integer types have these additional properties:

* Values can be implicitly converted between ``int`` and a native
integer type (both ways).

* Conversions between different native integer types must be explicit.
A conversion to a narrower native integer type truncates the value
without a runtime overflow check.

* If a binary operation (such as ``+``) or an augmented assignment
(such as ``+=``) mixes native integer and ``int`` values, the
``int`` operand is implicitly coerced to the native integer type
(native integer types are "sticky").

* You can't mix different native integer types in binary
operations. Instead, convert between types explicitly.

For more information about native integer types, refer to
:ref:`native integer operations <int-ops>`.