这是对我之前提问this answer Fastest approach to read thousands of images into one big numpy array的跟进。
在chapter 2.3 "Memory allocation of the ndarray"中,Travis Oliphant写了以下关于如何在内存中访问C有序numpy数组的索引。
...按顺序移动计算机内存,最后一个索引首先递增,然后是倒数第二个索引,依此类推。
这可以通过对两个第一个或最后两个索引的二维数组的访问时间进行基准测试来确认(出于我的目的,这是一个加载500个大小为512x512像素的图像的模拟):
import numpy as np
N = 512
n = 500
a = np.random.randint(0,255,(N,N))
def last_and_second_last():
'''Store along the two last indexes'''
imgs = np.empty((n,N,N), dtype='uint16')
for num in range(n):
imgs[num,:,:] = a
return imgs
def second_and_third_last():
'''Store along the two first indexes'''
imgs = np.empty((N,N,n), dtype='uint16')
for num in range(n):
imgs[:,:,num] = a
return imgs
基准
In [2]: %timeit last_and_second_last()
136 ms ± 2.18 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [3]: %timeit second_and_third_last()
1.56 s ± 10.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
到目前为止一切顺利。但是,当我沿最后一个和第三个维度加载数组时,这几乎和将它们加载到最后两个维度一样快。
def last_and_third_last():
'''Store along the last and first indexes'''
imgs = np.empty((N,n,N), dtype='uint16')
for num in range(n):
imgs[:,num,:] = a
return imgs
基准
In [4]: %timeit last_and_third_last()
149 ms ± 227 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
last_and_third_last()
相比,为什么last_and_second_last()
与second_and third_last()
的距离更接近?答案 0 :(得分:4)
Numpy的数组是基于c和c ++构建的,所以当我们将其推向它的绝对限制时,我们可以考虑像缓存行这样的东西。在last_and_second_last():
和last_and_third_last():
中,您沿着最后一个轴读取多个字节,因此一次使用整个缓存行(实际上16个,因为您的最后一个轴是1024个字节长)。在second_and_third_last()
中,必须复制整个高速缓存行以读取(或写入)最后一个轴中的单个值。现代的c编译器(和其他人:fortran等)将采用嵌套循环,以错误的顺序访问内存,并默默地重新排序它们以优化缓存使用,但python不能这样做。
示例:强>
arr = np.arange(64).reshape([4,4,4])
arr[i,j,:]
,可以立即在缓存中加载所有这些内容(例如:[0,1,2,3]
)arr[i,:,k]
arr[i,0,:]
并从4 [k]
arr[i,1,:]
并从4 [k]
答案 1 :(得分:4)
我将尝试说明索引,而不会详细介绍处理器缓存等。
让我们制作一个具有独特元素值的小3d数组:
In [473]: X = np.mgrid[100:300:100,10:30:10,1:4:1].sum(axis=0)
In [474]: X
Out[474]:
array([[[111, 112, 113],
[121, 122, 123]],
[[211, 212, 213],
[221, 222, 223]]])
In [475]: X.shape
Out[475]: (2, 2, 3)
ravel
将其视为1d数组,并向我们展示如何在内存中布置值。 (顺便说一句,这是默认的C
排序)
In [476]: X.ravel()
Out[476]: array([111, 112, 113, 121, 122, 123, 211, 212, 213, 221, 222, 223])
当我在第一维上编制索引时,我得到2 * 3的值,这是上面列表中的一个连续的块:
In [477]: X[0,:,:].ravel()
Out[477]: array([111, 112, 113, 121, 122, 123])
索引而不是最后一个给出从阵列中选择的4个值 - 我已添加..
以突出显示
In [478]: X[:,:,0].ravel()
Out[478]: array([111,.. 121,.. 211,.. 221])
中间的索引为我提供了2个连续的子块,即2行X
。
In [479]: X[:,0,:].ravel()
Out[479]: array([111, 112, 113,.. 211, 212, 213])
使用strides
和shape
计算numpy
可以同时访问X
中的任何一个元素。在X[:,:,i]
案例中,它必须做什么。 4个值是分散的'穿过数据库。
但是如果它可以访问连续的块,例如在X[i,:,:]
中,它可以将更多的动作委托给低级编译和处理器代码。 X[:,i,:]
[n,:,:]
这些区块并不大,但可能仍然足够大,可以产生很大的差异。
在您的测试用例中,[:,n,:]
在512 * 512个元素块上迭代500次。
[:,:,n]
必须将该访问划分为512个块,每块512个。
uint16
必须进行500 x 512 x 512次个人访问。
我想知道与float16
合作是否夸大了效果。在另一个问题中,我们只是表明使用numpy
的计算要慢得多(最多10倍),因为处理器(和编译器)被调整为使用32位和64位数。如果处理器被调整为移动64位数字块,那么移动一个隔离的16位数字可能需要大量额外处理。这就像从文档逐字进行复制粘贴,当逐行复制时,每个副本需要更少的击键次数。
确切的细节隐藏在处理器,操作系统和编译器以及imgs
代码中,但希望这可以让您了解为什么中间案例更接近最优情况而不是最坏情况。
在测试时 - 将a.dtype
设置为setTimeout
可以减慢所有情况下的速度。所以' uint16'不会造成任何特殊问题。
Why does `numpy.einsum` work faster with `float32` than `float16` or `uint16`?
答案 2 :(得分:2)
这里的关键点不是它是最后一个轴,而是它是ax
的轴imgs.strides[ax] == imgs.dtype.itemsize
- 也就是说,内存沿着该轴是连续的。默认行为是将此应用于最后一个轴,但不要假设 - 您将看到与imgs.T
相反的行为(因为这会创建一个视图,反转strides
数组)
当NumPy检测到轴是连续的时,它在整个维度上使用memcpy
,编译器会显着优化。在其他情况下,NumPy一次只能记忆一个元素
答案 3 :(得分:0)
对我而言,一个关键是要理解C顺序numpy数组中的行相互附加以在内存中形成连续的块/缓冲区,类似于@hpaulj在[None, 4, None, 1, None, None, 0, None, 2, None, 3, None, None, None,None]
中显示的布局。阅读更多关于数组如何在C中工作的内容进一步有助于理解,尤其是这三种资源:
由于检索在内存中连续布局的元素非常有效,因此访问数组的昂贵部分将成为查找,即当跳过部分连续内存块以继续在另一个内存位置读取时。 Aaron的回答概述了为什么这是一项昂贵的操作的原因。
正如@hpaulj指出的那样,.ravel()
方法执行的查找次数最少,[n,:,:]
方法到目前为止最多,这解释了为什么这种方法显着滞后其他两个背后:
[:,:,n]