Numpy数组方法比numpy函数更快吗?

3 投票
2 回答
62 浏览
提问于 2025-04-11 22:57

我需要处理一个Keras模型的学习历史。这是个基础的任务,但我测量了Python内置的min()函数、numpy.min()函数和numpy ndarray.min()函数在处理列表和ndarray时的性能。结果发现,Python的内置min()函数在处理ndarray时性能差得多——Numpy快了10倍(不过在处理列表时,Numpy几乎慢了6倍,但这不是我们要讨论的重点)。

然而,ndarray.min()方法的速度几乎是numpy.min()的两倍。ndarray.min()的文档提到它和numpy.amin()的文档是相关的,而根据numpy.amin的说明,它其实是numpy.min()的别名。因此,我原本以为numpy.min()和ndarray.min()的性能应该是一样的。但为什么这两个函数的性能却不相等呢?

from timeit import default_timer
import random
a = random.sample(range(1,1000000), 10000)
b = np.array(random.sample(range(1,1000000), 10000))

def time_mgr(func):
    tms = []
    for i in range(3, 6):
        tm = default_timer()
        for j in range(10**i):
            func()
        tm = (default_timer()-tm) / 10**i * 10e6
        tms.append(tm)
    print(func.__name__, tms)

@time_mgr
def min_list():
    min(a)

@time_mgr
def np_min_list():
    np.min(a)

@time_mgr
def min_nd():
    min(b)

@time_mgr
def np_min_nd():
    np.min(b)

@time_mgr
def np_nd_min():
    b.min()

输出,时间单位为毫秒:

min_list [520.7690014503896, 515.3326001018286, 516.221239999868]
np_min_list [2977.614998817444, 3009.602500125766, 3014.1312699997798]
min_nd [2270.1649996452034, 2195.6873999442905, 2155.1631700014696]
np_min_nd [22.295000962913033, 21.675399970263243, 22.30485000181943]
np_nd_min [14.261999167501926, 12.929399963468313, 12.935079983435571]

2 个回答

2

基本上,你的观察是正确的。

以下是我的时间记录和一些笔记:

创建两个数组,一个大得多,还有一个列表:

In [254]: a = np.random.randint(0,1000,1000); b = a.tolist()
In [255]: aa = np.random.randint(0,1000,100000)

这个方法在两种情况下都快了大约7微秒——这基本上是因为函数把工作交给了这个方法的开销:

In [256]: timeit a.min()
7.15 µs ± 16 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

In [257]: timeit np.min(a)
14.7 µs ± 204 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

In [258]: timeit aa.min()
49.4 µs ± 174 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

In [259]: timeit np.min(aa)
57.4 µs ± 141 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

在列表上调用 np.min 的额外时间是将列表转换为数组所需的时间:

In [260]: timeit np.min(b)
142 µs ± 446 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

In [261]: timeit np.array(b)
120 µs ± 161 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

原生的Python函数在处理列表时表现得相当不错:

In [262]: timeit min(b)
40.7 µs ± 92 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

但是在处理数组时就慢了。额外的时间主要是遍历数组,把它当作列表来处理所花的时间:

In [263]: timeit min(a)
127 µs ± 675 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

In [264]: timeit min(list(a))
146 µs ± 1.43 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

tolist 是从数组创建列表的更快方法:

In [265]: timeit min(a.tolist())
77.1 µs ± 82 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

一般来说,当有一个numpy函数和一个方法同名时,它实际上做了两件事:

  • 如果需要的话,把参数转换为数组
  • 把实际的计算工作交给方法。

把列表转换为数组是需要时间的。是否值得花这额外的时间取决于接下来的任务。

相反,把数组当作列表来处理通常会更慢。

In [270]: timeit [i for i in b]
50 µs ± 203 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

In [271]: timeit [i for i in a]
126 µs ± 278 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

在迭代中实际创建的项目是不同的:

In [275]: type(b[0]), type(a[0])
Out[275]: (int, numpy.int32)

b[0] 是和 b 中的对象是同一个。也就是说,b 包含对 int 对象的引用。而每次调用 a[0] 时,都会创建一个新的 np.int32 对象,且有一个新的 id。这种“拆箱”是需要时间的。

总之,如果你已经有一个数组,使用这个方法是最快的。但如果为了清晰或通用性,使用函数也没什么大不了的,尤其是当数组很大的时候。把数组当作列表处理通常会更慢。如果你是从列表开始,使用原生的Python函数通常是最好的选择——如果有的话。

2

列表和对象在效率上都很糟糕:它们把数字当作对象来存储,而列表则保存了指向这些对象的指针。这种方式会阻碍所有的优化,并导致内存访问模式非常糟糕。更不用说对象是通过引用计数来管理的,并且受到全局解释器锁(GIL)的保护。在一个热循环中,只要涉及到CPython对象的代码,性能就注定会很慢,尤其是当这个循环是被解释执行的时候。这种情况在min_listmin_nd中就会出现。

另外,Numpy不能直接对列表进行操作,所以它会把列表转换成数组。这个转换通常比任何基本的Numpy数组计算要慢得多(因为在np.min(a)中,这个转换会反复进行,所以性能就很差)。

至于np.minb.min,它们是等价的,虽然在处理小数组时,前者可能会稍微慢一点,因为CPython的函数调用开销和Numpy的不同处理路径。Numpy是针对大数组进行优化的。在小数组上,你会看到很多开销,这些开销不深入Numpy的实现代码是很难理解的(因为这些开销明显依赖于具体的实现)。

在我的机器上(i5-9600KF CPU,使用CPython 3.8.1和Numpy 1.24.4),前者需要4.5微秒,而后者只需要2.5微秒,所以时间非常短。这通常就是Numpy的开销(每次调用大约1-10微秒)。当数组大小增加100倍时,我得到的时间是80微秒对比83微秒(误差在2微秒内),所以这两者在统计上没有区别。这也表明你在小数组基准测试中花费的大部分时间其实就是纯粹的开销(大约60%到80%)。

如果你想减少这些开销,可以使用像Cython或Numba这样的工具,它们能够编译(特定的)Python代码,或者干脆不使用Python,而是用C/C++/Rust这样的原生语言。

撰写回答