Numerics¶
PPy gives the same answers as CPython for overflow, floor division, and the sign of the remainder, cases where several other native compilers differ.
Run it¶
Overflow falls back¶
@ppy.pure
@ppy.opt(3)
def may_overflow(n: int) -> int:
result: int = 1
for i in range(1, n + 1):
result *= i
return result
Each result *= i is an overflow-checking multiply. may_overflow(20) runs
twenty of them natively. may_overflow(30) sets the flag on the
twenty-first, the function returns to its Python body, and CPython finishes
with arbitrary precision: the 33-digit number Python prints.
Under ppy run and ppy build the guards are on by default. --unsafe on
either produces a wrap-semantics artifact.
Floor, not truncation¶
C rounds toward zero. Python rounds toward negative infinity and gives the
remainder the divisor's sign. The IR marks the operation
rounding = "floor", and the LLVM backend emits the sign-corrected
sequence, or a single arithmetic shift when the divisor is a power of two.
The shift is one reason the collatz kernel keeps up with its C twin.
floor_semantics(-7, 2) is -4 and modulo_semantics(7, -2) is -1 on
every path.
Compared with Numba, Codon, Mojo, C, and Rust¶
The same three functions, written for five other compilers, in
compare/: semantics_numba.py,
semantics_codon.py, semantics.mojo,
semantics.c, and semantics.rs.
There is nothing to time here. The question is what each prints for
may_overflow(30), floor_semantics(-7, 2), and modulo_semantics(7, -2).
The answers Python gives are 265252859812191058636308480000000, -4,
and -1.
may_overflow(30) |
-7 // 2 |
7 % -2 |
what the code says | |
|---|---|---|---|---|
| Python, PPy on every path | 265252859812191058636308480000000 | -4 | -1 | int is an integer |
Numba @njit |
-8764578968847253504 | -4 | -1 | 64-bit wrap; Python's floor and sign |
Mojo 1.0 Int |
-8764578968847253504 | -4 | -1 | 64-bit wrap; Python's floor and sign |
| Codon | -8764578968847253504 | -3 | 1 | 64-bit wrap; C's truncation and sign |
C long long (gcc, -O0 and -O3) |
-8764578968847253504 | -3 | 1 | signed overflow is undefined; truncation |
Rust i64, release |
-8764578968847253504 | -3 | 1 | wraps; truncation |
Rust i64, debug |
panics: attempt to multiply with overflow | -3 | 1 | checked, then aborts |
- Numba and Mojo keep Python's division and remainder and wrap the multiply silently.
- Codon, C, and Rust in release keep C's rounding and wrap.
- Rust in debug is the only other one that notices the overflow, and its answer is to stop.
PPy notices and continues in Python: the same function, the same source,
the number Python prints. --unsafe on ppy run or ppy build buys the
wrap-semantics row, and says so.
Numba 0.67.0 on CPython 3.12.13, Codon 0.19.6, Mojo 1.0.0, gcc 13.3, rustc 1.95.0.
What it prints¶
python numerics.ppy, ppy numerics.ppy, ppy run numerics.ppy
Read on: Arbitrary precision ยท The IR
numerics.ppy is hand-written; there is no .py source and no conversion step.
11_numerics/numerics.ppy¶
import ppy
@ppy.pure
@ppy.opt(3)
def machine_range(a: int, b: int) -> int:
return a * b
@ppy.pure
@ppy.opt(3)
def may_overflow(n: int) -> int:
result: int = 1
for i in range(1, n + 1):
result *= i
return result
@ppy.pure
@ppy.opt(3)
def floor_semantics(a: int, b: int) -> int:
return a // b
@ppy.pure
@ppy.opt(3)
def modulo_semantics(a: int, b: int) -> int:
return a % b
def main() -> None:
print(machine_range(1000, 1000))
print(may_overflow(20))
print(may_overflow(30))
print(floor_semantics(-7, 2), floor_semantics(7, 2))
print(modulo_semantics(-7, 2), modulo_semantics(7, -2))
main()
Counterpart programs¶
The programs the comparison above measured, each written the way its tool expects. The PPy one is first.
semantics.c (C)
/* The same three questions put to C: signed overflow is undefined, division truncates. */
#include <stdio.h>
static long long may_overflow(long long n) {
long long result = 1;
for (long long i = 1; i <= n; i++) result *= i;
return result;
}
static long long floor_semantics(long long a, long long b) { return a / b; }
static long long modulo_semantics(long long a, long long b) { return a % b; }
int main(void) {
printf("%lld\n", may_overflow(20));
printf("%lld\n", may_overflow(30));
printf("%lld %lld\n", floor_semantics(-7, 2), floor_semantics(7, 2));
printf("%lld %lld\n", modulo_semantics(-7, 2), modulo_semantics(7, -2));
return 0;
}
semantics.mojo (Mojo)
"""The same three questions put to Mojo 1.0: `Int` is a machine word."""
def may_overflow(n: Int) -> Int:
var result = 1
for i in range(1, n + 1):
result *= i
return result
def floor_semantics(a: Int, b: Int) -> Int:
return a // b
def modulo_semantics(a: Int, b: Int) -> Int:
return a % b
def main():
print(may_overflow(20))
print(may_overflow(30))
print(floor_semantics(-7, 2), floor_semantics(7, 2))
print(modulo_semantics(-7, 2), modulo_semantics(7, -2))
semantics.rs (Rust)
//! The same three questions put to Rust: `i64` panics on overflow in debug, wraps in release.
fn may_overflow(n: i64) -> i64 {
let mut result: i64 = 1;
for i in 1..=n {
result *= i;
}
result
}
fn floor_semantics(a: i64, b: i64) -> i64 {
a / b
}
fn modulo_semantics(a: i64, b: i64) -> i64 {
a % b
}
fn main() {
println!("{}", may_overflow(20));
println!("{}", may_overflow(30));
println!("{} {}", floor_semantics(-7, 2), floor_semantics(7, 2));
println!("{} {}", modulo_semantics(-7, 2), modulo_semantics(7, -2));
}
semantics_codon.py (Python)
"""The same three questions put to Codon: Python syntax, a 64-bit `int`."""
def may_overflow(n: int) -> int:
result = 1
for i in range(1, n + 1):
result *= i
return result
def floor_semantics(a: int, b: int) -> int:
return a // b
def modulo_semantics(a: int, b: int) -> int:
return a % b
print(may_overflow(20))
print(may_overflow(30))
print(floor_semantics(-7, 2), floor_semantics(7, 2))
print(modulo_semantics(-7, 2), modulo_semantics(7, -2))
semantics_numba.py (Python)
"""The three questions of `numerics.ppy` put to Numba: `@njit` on a machine integer."""
from numba import njit
@njit
def may_overflow(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
@njit
def floor_semantics(a, b):
return a // b
@njit
def modulo_semantics(a, b):
return a % b
print(may_overflow(20))
print(may_overflow(30))
print(floor_semantics(-7, 2), floor_semantics(7, 2))
print(modulo_semantics(-7, 2), modulo_semantics(7, -2))
Source: examples/11_numerics.