1 数组的两种内存布局方式
行优先与列优先
首先我们回顾一下,矩阵数据在内存中的两种布局方式:
- 行优先(row-major):以行为优先单位,在内存中逐行存储/读取;对于多维,意味着当线性扫描内存时,第一个维度的变化最慢。
- 列优先(column-major):以列为优先单位,在内存中逐列存储/读取;对于多维,意味着当线性扫描内存时,最后一个维度的变化最慢。
以下面的[2, 2, 2]
张量为例:
a = [[[1, 2],[3, 4]],[[5, 6],[7, 8]]]
在内存中的数据排布:
行优先:1, 2 | 3, 4 || 5, 6 | 7, 8a[0,0] a[0,1] a[1,0] a[1,1]
列优先:1, 5 | 3, 7 || 2, 6 | 4, 8a[0,0] a[1,0] a[0,1] a[1,1]
谁更好?
选择行优先还是列优先,主要取决于我们访问数组的模式。由于每次从内存中获取数据时,CPU都会自动将该数据及其相邻的内存加载到缓存中,希望利用引用的局部性。因此,如果访问数组时是逐列访问的,我们就希望同一列的数据在内存中靠得更近,便于一次性加载到CPU缓存中从而避免反复加载,亦即更加的“Cache-friendly”,此时列优先显然是最好的选择。而对于逐行访问的情况,则应该选择行优先。
C和大多数DeepLearning库用的都是行优先,而Fortran和matlab等一些用于科学计算的语言,使用的是列优先。不要问为什么,这是历史的偶然选择而已。如果要强行解释,可以说Fortran是考虑到线性代数中的向量默认为列向量,所以用列优先与数学符号更匹配,虽然用列优先并不会加速矩阵运算(比如矩阵乘法中第一个矩阵是逐行访问,第二个是逐列访问,不可兼得),但是更能显现出科学家与众不同的装逼特性 :-) 。
2 numpy 中的行优先和列优先
numpy支持这两种内存布局方式,默认采用行优先。可以在新建array
,或者进行reshape
等操作时,通过指定order
参数来决定数据的内存布局方式。
array() 新建
函数原型:
array(object, dtype=None, copy=True, order='K', subok=False, ndmin=0)
参数:
dtype
: 存储单元格式,有np.float32、np.bool、np.int32等。copy
: 是否在内存中新建array。subok
: (不用管)If True, then sub-classes will be passed-through, otherwise the returned array will be forced to be a base-class array (default).ndmin
: 返回的数组应该具有至少ndmin
个维数,不足时补充若干个大小为1的维度。
np.array([[1,2,3],[4,5,6]]).shape
Out[52]: (2, 3)
np.array([[1,2,3],[4,5,6]], ndmin=4).shape
Out[53]: (1, 1, 2, 3)
order
: 新建的array在内存中的布局方式(该参数在copy==True
时才有意义),从 {‘K’, ‘A’, ‘C’, ‘F’} 中选择;
举个例子:
s = [[1,2,3], ['a','b','c']] # python序列采用行优先布局
# 内存中 s :1, 2, 3, 'a', 'b', 'c'a = np.array(s, order='C')
# a.reshape(-1) :'1', '2', '3', 'a', 'b', 'c'b = np.array(s, order='F')
# b.reshape(-1) :'1', 'a', '2', 'b', '3', 'c'
reshape() 重整维度
函数原型:
reshape(array, newshape, order='C')
array.reshape(newshape, order='C')
参数:
newshape
: 一个描述各维度大小的序列,也可以是单个int。order
: 从 {‘A’, ‘C’, ‘F’} 中选择。
b = reshape(a, newshape, order)
相当于:
b = np.array(a, order) # 在内存中新建一个 b ,以 order 布局方式存储从 a 中读取的数据
b.shape = newshape # 设定index指针的计算方式
3 “lazy”的 transpose() 转置
注意,numpy中的转置transpose()
是非常“lazy”的,亦即不对内存中的数据进行重排,仅仅改变读取方式。
举个例子:
''' a.shape = [1,2,3] '''
transpose_scheme = [2,1,0] # 维度0与2交换位置
b = np.transpose(a, axes=transpose_scheme)
'''
此时 b.shape 虽然变成了 [3,2,1]
但是 b 与 a 在内存的排布是一样的
'''
transpose()
等效于:在读取/写入函数函数外,包了一个能改变维度顺序的函数装饰器。
def change_axis_order(transpose_scheme):def get_func(func):@wraps(func)def wrapper(self, axes):transposed_axes = [axes[i] for i in transpose_scheme]return func(self, transposed_axes)return wrapperreturn get_func'''
b = np.transpose(a, axes=transpose_scheme)
相当于:
'''
b = a.copy()
b.__getitem__ = change_axis_order(transpose_scheme)(b.__getitem__)
b.__setitem__ = change_axis_order(transpose_scheme)(b.__setitem__)
之所以采用这种“lazy”的方式,是因为重新在内存中排列数据的非常耗时的。
如果一定要在内存中重新排列数据,可以采用以下方法:
b = np.zeros_like(a)
b[:] = np.array(a, axes=transpose_scheme)