`_C_INFIX_OPS` mapped both Python `/` and `//` onto C `/`, so the JIT kernel
for an integer UDT field emitted bare signed division and remainder. Measured
at the parent commit, for a record of int32 and int64 fields:
floordiv: z->q_a = ((x->q_a) / (y->q_a) - (((x->q_a) % (y->q_a) != 0) && ...))
truediv: z->q_a = (x->q_a) / (y->q_a)
That is two separate defects.
First, a trap. Bare signed `/` and `%` are undefined for a zero divisor and
for `INT_MIN / -1`. On x86-64 `idiv` raises #DE, which is SIGFPE and process
death rather than an exception. AArch64's `sdiv` returns 0 and does not trap,
so this cannot be exhibited on the arm64 machine it was written on. It is a
real defect in the generated C either way.
Second, a wrong answer, and this one is not architecture-specific. C `/` on
two integers is integer division, but Python's `/` divides in floating point,
so the two paths disagreed for the same program:
binary.truediv on an int64 UDT, 10**18 / 3
with a C compiler: 333333333333333333
without one: 333333333333333312 (numpy's answer)
Float `//` was wrong on its own account: the kernel computed `floor(a / b)`,
which is not floor division. `1.0 // 0.1` is 9.0 but `floor(1.0 / 0.1)` is
10.0, and `inf // 2.0` is NaN but `floor(inf / 2.0)` is inf. Both paths now
use the remainder-based algorithm numpy and CPython share.
Three of the values here are choices rather than discoveries, and each
follows numpy rather than the alternative:
- Integer `x / 0` gives 0, which is what `np.floor_divide` does.
`np.true_divide` gives an infinity whose cast back to an integer is
undefined in numpy too.
- `INT_MIN // -1` wraps to `INT_MIN`, as numpy does. Numba returns 0 for it,
deliberately, to dodge the same trap.
- Complex `/` by zero gives numpy's infinities. Numba raises
`ZeroDivisionError` unconditionally, outside the error model's control, so
the cfunc previously left the element unwritten.
Two range escapes are left, and both move the paths together rather than
apart. A 64-bit field can leave the range through the `(double)` conversion
itself, since `(2**63 - 1) / 1` rounds up to `2**63`; both paths then land on
the same hardware conversion rather than on defined behaviour, so they agree
with each other but need not agree across machines (measured saturating to
`INT64_MAX` on arm64). Operands of mixed signedness escape through a negative
divisor the `INT_MIN` guard does not see; the `_expr_binary` docstring details
why that also cannot split the paths.
_C_INFIX_OPS mapped both Python / and // onto C /, so the JIT kernel
for an integer UDT field emitted bare signed division and remainder. Measured
at the parent commit, for a record of int32 and int64 fields:
That is two separate defects.
First, a trap. Bare signed / and % are undefined for a zero divisor and
for INT_MIN / -1. On x86-64 idiv raises #DE, which is SIGFPE and process
death rather than an exception. AArch64's sdiv returns 0 and does not trap,
so this cannot be exhibited on the arm64 machine it was written on. It is a
real defect in the generated C either way.
Second, a wrong answer, and this one is not architecture-specific. C / on
two integers is integer division, but Python's / divides in floating point,
so the two paths disagreed for the same program:
Float // was wrong on its own account: the kernel computed floor(a / b),
which is not floor division. 1.0 // 0.1 is 9.0 but floor(1.0 / 0.1) is
10.0, and inf // 2.0 is NaN but floor(inf / 2.0) is inf. Both paths now
use the remainder-based algorithm numpy and CPython share.
Three of the values here are choices rather than discoveries, and each
follows numpy rather than the alternative:
np.true_divide gives an infinity whose cast back to an integer is
undefined in numpy too.
deliberately, to dodge the same trap.
ZeroDivisionError unconditionally, outside the error model's control, so
the cfunc previously left the element unwritten.
Two range escapes are left, and both move the paths together rather than
apart. A 64-bit field can leave the range through the (double) conversion
itself, since (2**63 - 1) / 1 rounds up to 2**63; both paths then land on
the same hardware conversion rather than on defined behaviour, so they agree
with each other but need not agree across machines (measured saturating to
INT64_MAX on arm64). Operands of mixed signedness escape through a negative
divisor the INT_MIN guard does not see; the _expr_binary docstring details
why that also cannot split the paths.
Stack created with GitHub Stacks CLI • Give Feedback 💬