numpy浮点数:在算术运算中比内置类型慢10倍?

56 投票
8 回答
19895 浏览
提问于 2025-04-16 17:23

我在运行以下代码时,得到了非常奇怪的时间结果:

import numpy as np
s = 0
for i in range(10000000):
    s += np.float64(1) # replace with np.float32 and built-in float
  • 内置的 float: 4.9 秒
  • float64: 10.5 秒
  • float32: 45.0 秒

为什么 float64 的速度比 float 慢一倍?而 float32 又为什么比 float64 慢五倍呢?

有没有办法避免使用 np.float64 的性能损失,让 numpy 函数返回内置的 float 而不是 float64 呢?

我发现使用 numpy.float64 的速度比 Python 的 float 慢很多,而 numpy.float32 更慢(尽管我使用的是 32 位机器)。

在我的 32 位机器上使用 numpy.float32。因此,每次我使用各种 numpy 函数,比如 numpy.random.uniform 时,我都会把结果转换成 float32(这样后续的操作就会以 32 位精度进行)。

有没有办法在程序的某个地方或命令行中设置一个变量,让所有 numpy 函数返回 float32 而不是 float64 呢?

编辑 #1:

在算术计算中,numpy.float64 的速度比 float 慢10 倍。情况糟糕到即使在计算前把它转换成 float 再转换回来,程序的运行速度也能快 3 倍。为什么会这样?我能做些什么来解决这个问题吗?

我想强调的是,我的时间测量并不是由于以下原因造成的:

  • 函数调用
  • numpy 和 Python float 之间的转换
  • 对象的创建

我更新了我的代码,以便更清楚地显示问题所在。用新代码看起来我发现使用 numpy 数据类型的性能损失是十倍:

from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

for i in range(10000000):
    s = (s + 8) * s % 2399232

print(s)
print('Runtime:', datetime.now() - START_TIME)

时间结果是:

  • float64: 34.56秒
  • float32: 35.11秒
  • float: 3.53秒

为了好玩,我还尝试了:

from datetime import datetime import numpy as np

START_TIME = datetime.now()

s = np.float64(1)
for i in range(10000000):
    s = float(s)
    s = (s + 8) * s % 2399232
    s = np.float64(s)

print(s)
print('Runtime:', datetime.now() - START_TIME)

执行时间是 13.28 秒;实际上,把 float64 转换成 float 再转换回来比直接使用它快 3 倍。不过,转换还是会有影响,所以总体上比纯 Python 的 float 慢超过 3 倍。

我的机器配置是:

  • Intel Core 2 Duo T9300 (2.5GHz)
  • WinXP Professional (32-bit)
  • ActiveState Python 3.1.3.5
  • Numpy 1.5.1

编辑 #2:

谢谢大家的回答,帮助我理解如何处理这个问题。

但我仍然想知道下面代码运行时,float64float 慢 10 倍的具体原因(也许可以根据源代码来解释)。

编辑 #3:

我在 Windows 7 x64(Intel Core i7 930 @ 3.8GHz)上重新运行了代码。

再次运行的代码是:

from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

for i in range(10000000):
    s = (s + 8) * s % 2399232

print(s)
print('Runtime:', datetime.now() - START_TIME)

时间结果是:

  • float64: 16.1秒
  • float32: 16.1秒
  • float: 3.2秒

现在,无论是 np 的 float(64 位或 32 位)都比内置的 float 慢 5 倍。仍然有明显的差异。我正在努力弄清楚这背后的原因。

编辑结束

8 个回答

23

在像这样的重循环中使用Python对象,不管是float还是np.float32,总是会比较慢。NumPy在处理向量和矩阵时很快,因为它的很多操作都是在用C语言写的库中进行的,这样就能一次性处理大量数据,而不是通过Python解释器来处理。用解释器运行的代码或者使用Python对象的代码总是比较慢,使用非原生类型会让速度更慢。这是可以预料的。

如果你的应用运行得很慢,需要优化的话,可以尝试把代码转换成直接使用NumPy的向量解决方案,这样会更快;或者你也可以使用像Cython这样的工具,把循环的部分用C语言实现,这样也能提高速度。

51

CPython中的浮点数是分块分配的

比较numpy的标量分配和float类型时,主要的问题是CPython总是以N的块大小来分配floatint对象的内存。

在内部,CPython维护着一个链表,每个块都足够大,可以容纳N个float对象。当你调用float(1)时,CPython会检查当前块是否有空余空间;如果没有,就会分配一个新的块。一旦当前块有了空间,它就会初始化这个空间,并返回一个指向它的指针。

在我的机器上,每个块可以容纳41个float对象,所以第一次调用float(1)会有一些开销,但接下来的40次调用会快得多,因为内存已经分配好了。

numpy.float32比numpy.float64慢

看起来numpy在创建标量类型时有两条路径可以选择:快和慢。这取决于标量类型是否有一个可以用来进行参数转换的Python基类。

出于某种原因,numpy.float32被硬编码为走慢路径 (由_WORK0宏定义),而numpy.float64则有机会走快路径 (由_WORK1宏定义)。需要注意的是,scalartypes.c.src是一个模板,在构建时生成scalartypes.c

你可以在Cachegrind中可视化这个过程。我附上了屏幕截图,显示构造float32float64时调用的次数差异:

float64走的是快路径

float64 takes the fast path

float32走的是慢路径

float32 takes the slow path

更新 - 哪种类型走慢/快路径可能取决于操作系统是32位还是64位。在我的测试系统上,Ubuntu Lucid 64位,float64的速度是float32的10倍。

9

总结

如果一个数学表达式同时使用了 numpy 和内置数字,Python 的计算速度会变慢。避免这种类型的转换几乎可以消除我提到的性能下降。

详细信息

注意在我最初的代码中:

s = np.float64(1)
for i in range(10000000):
  s = (s + 8) * s % 2399232

在一个表达式中混合了 floatnumpy.float64 类型。也许 Python 必须把它们都转换成同一种类型?

s = np.float64(1)
for i in range(10000000):
  s = (s + np.float64(8)) * s % np.float64(2399232)

如果运行时间没有增加(而是减少了),这就说明 Python 确实在后台进行了这样的处理,这也解释了性能下降的原因。

实际上,运行时间减少了 1.5 倍!这怎么可能呢?难道 Python 最糟糕的情况就是进行这两次转换吗?

我也不太清楚。也许 Python 需要动态检查哪些东西需要转换成什么,这会耗费时间,而如果提前告诉它具体要进行哪些转换,就能加快速度。也有可能,Python 在进行数学运算时使用了完全不同的机制(根本不涉及转换),而在类型不匹配时会变得特别慢。查看 numpy 的源代码可能会有帮助,但这超出了我的能力范围。

无论如何,现在我们可以通过把转换移出循环来明显加快速度:

q = np.float64(8)
r = np.float64(2399232)
for i in range(10000000):
  s = (s + q) * s % r

正如预期的那样,运行时间大幅减少:又减少了 2.3 倍。

公平起见,我们现在需要稍微修改一下 float 版本,把常量移出循环。这会导致轻微的(10%)减速。

考虑到所有这些变化,np.float64 版本的代码现在只比等效的 float 版本慢 30%;之前那种五倍的性能下降基本消失了。

那么,为什么我们仍然看到 30% 的延迟呢?numpy.float64 数字占用的空间和 float 一样,所以这不是原因。也许用户自定义类型的算术运算符处理起来更慢。这当然不是一个大问题。

撰写回答