意外的 uint64 行为 0xFFFF'FFFF'FFFF'FFFF - 1 = 0?

Alb*_*ang 45 python numpy uint64

考虑以下展示uint64数据类型的简短 numpy 会话

import numpy as np
 
a = np.zeros(1,np.uint64)
 
a
# array([0], dtype=uint64)
 
a[0] -= 1
a
# array([18446744073709551615], dtype=uint64)
# this is 0xffff ffff ffff ffff, as expected

a[0] -= 1
a
# array([0], dtype=uint64)
# what the heck?
Run Code Online (Sandbox Code Playgroud)

我对最后的输出完全感到困惑。

我期望 0xFFFF'FFFF'FFFF'FFFE。

这里究竟发生了什么?

我的设置:

>>> sys.platform
'linux'
>>> sys.version
'3.10.5 (main, Jul 20 2022, 08:58:47) [GCC 7.5.0]'
>>> np.version.version
'1.23.1'
Run Code Online (Sandbox Code Playgroud)

use*_*ica 40

默认情况下,NumPy 将 Python int 对象转换为 与numpy.int_C 对应的有符号整数数据类型long。(这个决定是在早期 Pythonint 对应于 C 的时候做出的long。)

没有足够大的整数 dtype 可以容纳numpy.uint64dtype numpy.int_dtype 的所有值,因此numpy.uint64标量和 Python int 对象之间的运算会产生 float64 结果而不是整数结果。(uint64数组和 Python int 之间的操作可能表现不同,因为 int在此类操作中根据其值转换为数据类型,但a[0]它是标量。)

第一次减法生成值为 -1 的 float64,第二次减法生成值为 2**64 的 float64(因为 float64 没有足够的精度来精确执行减法)。这两个值都超出了 uint64 dtype 的范围,因此转换回 uint64 进行赋值会a[0]产生未定义的行为(继承自 C - NumPy 仅使用 C 强制转换)。

在您的计算机上,这恰好会产生回绕行为,因此 -1 回绕到 18446744073709551615 和 2**64 回绕到 0,但这并不能保证。您可能会在其他设置上看到不同的行为。评论中的人们确实看到了不同的行为。

  • 我很好奇,作为一个偶尔使用 numpy 的人,社区是否认为这是一个缺陷?这是非常令人惊讶的,因为大多数明确指定固定类型的语言的行为定义与 C 类似或相同。 (11认同)
  • 鉴于 C 标准允许调用 UB 的程序实际上可以执行任何操作,从 C 继承 UB 不是相当令人担忧吗?诚然,最近出现的大多数(全部?)有趣的东西,比如根本不为始终调用 UB 的执行路径生成任何代码,取决于在编译时检测到的 UB,但事实并非如此,但是仍然... (4认同)
  • @Chuu:我不知道一般社区,但我个人更喜欢混合 uint64 和有符号整数产生 uint64 输出而不是 float64 输出。(C 可以更好地处理这种特定情况,但它也有自己的问题 - 例如,两个无符号操作数上的整数算术可能会在 C 中产生 *signed* 溢出和未定义的行为,因为小于 int 的无符号类型如何提升为有符号 int。我不确定 NumPy 是否有任何保护措施来避免此问题。) (3认同)

Kel*_*ndy 12

a[0] - 11.8446744073709552e+19, 一个numpy.float64. 那不能保留所有精度,所以它的值为 18446744073709551616=2 64a当用 dtype写回时np.uint64,它会变成0.

  • 使用超出整数类型范围的浮点值进行 NumPy 浮点->整数转换是未定义行为(继承自 C),因此不能保证结果实际上是 0 - 它可以是任何值。 (10认同)