From 4e6d68322774d5f7c15d5067613fc851b4640d3e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 22 Mar 2023 19:07:58 +0000 Subject: [PATCH] [mypyc] Document native floats and integers (#14927) These were undocumented previously. --- mypyc/doc/float_operations.rst | 34 +++++-- mypyc/doc/int_operations.rst | 111 +++++++++++++++++++++- mypyc/doc/performance_tips_and_tricks.rst | 4 +- mypyc/doc/using_type_annotations.rst | 89 ++++++++++++++++- 4 files changed, 221 insertions(+), 17 deletions(-) diff --git a/mypyc/doc/float_operations.rst b/mypyc/doc/float_operations.rst index c1e4d284c4ba..915c184ae8e7 100644 --- a/mypyc/doc/float_operations.rst +++ b/mypyc/doc/float_operations.rst @@ -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)`` diff --git a/mypyc/doc/int_operations.rst b/mypyc/doc/int_operations.rst index 038b6e5dbc63..058fdbd511dd 100644 --- a/mypyc/doc/int_operations.rst +++ b/mypyc/doc/int_operations.rst @@ -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" + ... diff --git a/mypyc/doc/performance_tips_and_tricks.rst b/mypyc/doc/performance_tips_and_tricks.rst index 668d32827402..ae0b2950814c 100644 --- a/mypyc/doc/performance_tips_and_tricks.rst +++ b/mypyc/doc/performance_tips_and_tricks.rst @@ -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) @@ -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 `, such as indexing, diff --git a/mypyc/doc/using_type_annotations.rst b/mypyc/doc/using_type_annotations.rst index a01246ab0914..6c9277786751 100644 --- a/mypyc/doc/using_type_annotations.rst +++ b/mypyc/doc/using_type_annotations.rst @@ -30,6 +30,8 @@ mypyc, and many operations on these types have efficient implementations: * ``int`` (:ref:`native operations `) +* ``i64`` (:ref:`documentation `, :ref:`native operations `) +* ``i32`` (:ref:`documentation `, :ref:`native operations `) * ``float`` (:ref:`native operations `) * ``bool`` (:ref:`native operations `) * ``str`` (:ref:`native operations `) @@ -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 ` +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 @@ -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 @@ -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 `.