numpy浮点数:在算术运算中比内置类型慢10倍?
我在运行以下代码时,得到了非常奇怪的时间结果:
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:
谢谢大家的回答,帮助我理解如何处理这个问题。
但我仍然想知道下面代码运行时,float64
比 float
慢 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 个回答
在像这样的重循环中使用Python对象,不管是float
还是np.float32
,总是会比较慢。NumPy在处理向量和矩阵时很快,因为它的很多操作都是在用C语言写的库中进行的,这样就能一次性处理大量数据,而不是通过Python解释器来处理。用解释器运行的代码或者使用Python对象的代码总是比较慢,使用非原生类型会让速度更慢。这是可以预料的。
如果你的应用运行得很慢,需要优化的话,可以尝试把代码转换成直接使用NumPy的向量解决方案,这样会更快;或者你也可以使用像Cython这样的工具,把循环的部分用C语言实现,这样也能提高速度。
CPython中的浮点数是分块分配的
比较numpy的标量分配和float
类型时,主要的问题是CPython总是以N的块大小来分配float
和int
对象的内存。
在内部,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中可视化这个过程。我附上了屏幕截图,显示构造float32
和float64
时调用的次数差异:
float64
走的是快路径
float32
走的是慢路径
更新 - 哪种类型走慢/快路径可能取决于操作系统是32位还是64位。在我的测试系统上,Ubuntu Lucid 64位,float64
的速度是float32
的10倍。
总结
如果一个数学表达式同时使用了 numpy
和内置数字,Python 的计算速度会变慢。避免这种类型的转换几乎可以消除我提到的性能下降。
详细信息
注意在我最初的代码中:
s = np.float64(1)
for i in range(10000000):
s = (s + 8) * s % 2399232
在一个表达式中混合了 float
和 numpy.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
一样,所以这不是原因。也许用户自定义类型的算术运算符处理起来更慢。这当然不是一个大问题。