为什么numpy.zeros和numpy.zeros的性能不同?

2024-04-20 02:44:31 发布

您现在位置:Python中文网/ 问答频道 /正文

我终于在我的代码中发现了一个性能瓶颈,但是我不知道原因是什么。为了解决这个问题,我将所有的numpy.zeros_like调用改为使用numpy.zeros。但是为什么zeros_like要慢得多呢?

例如(注意zeros调用上的e-05):

>>> timeit.timeit('np.zeros((12488, 7588, 3), np.uint8)', 'import numpy as np', number = 10)
5.2928924560546875e-05
>>> timeit.timeit('np.zeros_like(x)', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10)
1.4402990341186523

但奇怪的是,用zeros创建的数组的写入速度明显慢于用zeros_like创建的数组:

>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros((12488, 7588, 3), np.uint8)', number = 10)
0.4310588836669922
>>> timeit.timeit('x[100:-100, 100:-100] = 1', 'import numpy as np; x = np.zeros_like(np.zeros((12488, 7588, 3), np.uint8))', number = 10)
0.33325695991516113

我的猜测是zeros使用了一些CPU技巧,而不是实际地写入内存来分配它。这是写给你的时候就在飞行中完成的。但这仍然不能解释数组创建时间的巨大差异。

我正在用当前的numpy版本运行Mac OS X Yosemite:

>>> numpy.__version__
'1.9.1'

Tags: 代码importnumpynumberasnpzeros原因
2条回答

现代操作系统实际上是在分配内存,也就是说,只有当一个进程首次使用时,内存才被分配给它。zeros从操作系统获取内存,以便操作系统在首次使用时将其归零。zeros_like另一方面,它自己用零填充分配的内存。这两种方法都需要大约相同的工作量——只不过对于zeros_like调零是预先完成的,而zeros最终是在飞行中完成的。

从技术上讲,C语言的区别在于调用callocmalloc+memset

我在Ipython中的计时是(使用更简单的timeit接口):

In [57]: timeit np.zeros_like(x)
1 loops, best of 3: 420 ms per loop

In [58]: timeit np.zeros((12488, 7588, 3), np.uint8)
100000 loops, best of 3: 15.1 µs per loop

当我使用IPython(np.zeros_like??)查看代码时,我看到:

res = empty_like(a, dtype=dtype, order=order, subok=subok)
multiarray.copyto(res, 0, casting='unsafe')

np.zeros是纯黑盒编译的代码。

empty的计时为:

In [63]: timeit np.empty_like(x)
100000 loops, best of 3: 13.6 µs per loop

In [64]: timeit np.empty((12488, 7588, 3), np.uint8)
100000 loops, best of 3: 14.9 µs per loop

所以zeros_like中的额外时间是copy

在我的测试中,赋值时间(x[]=1)的差异可以忽略不计。

我猜zerosonesempty都是早期编译的产物。empty_like是为了方便起见添加的,只需从输入中绘制形状和类型信息。zeros_like编写时更注重易于编程维护(重用empty_like),而不是速度。

np.onesnp.full也使用np.empty ... copyto序列,并显示类似的计时。


https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/array_assign_scalar.c 似乎是将标量(例如0)复制到数组的文件。我看不到memset的用法。

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/alloc.c调用了malloccalloc

https://github.com/numpy/numpy/blob/master/numpy/core/src/multiarray/ctors.c-用于zerosempty的源。两者都调用PyArray_NewFromDescr_int,但一个最终使用npy_alloc_cache_zero,另一个使用npy_alloc_cache

npy_alloc_cachealloc.c调用allocnpy_alloc_cache_zero调用npy_alloc_cache,然后是memsetalloc.c中的代码与线程选项进一步混淆。

更多关于callocvmalloc+memset差异的信息,请访问: Why malloc+memset is slower than calloc?

但是,对于缓存和垃圾收集,我想知道calloc/memset区别是否适用。


这个带有memory_profile包的简单测试支持这样一种说法,即zerosempty动态分配内存,而zeros_like预先分配所有内容:

N = (1000, 1000) 
M = (slice(None, 500, None), slice(500, None, None))

Line #    Mem usage    Increment   Line Contents
================================================
     2   17.699 MiB    0.000 MiB   @profile
     3                             def test1(N, M):
     4   17.699 MiB    0.000 MiB       print(N, M)
     5   17.699 MiB    0.000 MiB       x = np.zeros(N)   # no memory jump
     6   17.699 MiB    0.000 MiB       y = np.empty(N)
     7   25.230 MiB    7.531 MiB       z = np.zeros_like(x) # initial jump
     8   29.098 MiB    3.867 MiB       x[M] = 1     # jump on usage
     9   32.965 MiB    3.867 MiB       y[M] = 1
    10   32.965 MiB    0.000 MiB       z[M] = 1
    11   32.965 MiB    0.000 MiB       return x,y,z

相关问题 更多 >