1、内存设计与实现原理
ndarray的内存结构
类ndarray
的实例本质上由一个连续的一维内存段和一个索引方案组合而成。这种将所有数据存放在一个连续的一维内存段的存储方式,实际与C语言中的多维数组存储方式一致。但Numpy索引的灵活设计,使得ndarray
对象可适应于任何跨步索引方案,以下对ndarray
对象以行作为主要存储顺序的内存设计进行说明。
Numpy在创建数组或建立数组视图时,将数组的信息记录在不同属性,如shape属性指定各维度的元素的数量,dtype属性指定元素类型及其解释方式,strides属性指定各维度的跨度,itemsize属性指定单个元素占用字节数。
![](https://i-blog.csdnimg.cn/blog_migrate/5ce83c6869d79101aadf64ccc1827350.png)
索引的解析
对于
N
N
N维的数组
a
r
r
a
y
\sf array
array,将各维度的跨度存放在
s
t
r
i
d
e
s
\sf{strides}
strides列表中,则第
k
+
1
k+1
k+1维的跨度
s
t
r
i
d
e
s
[
k
]
=
{
i
t
e
m
s
i
z
e
,
k
=
N
−
1
i
t
e
m
s
i
z
e
×
∏
j
=
k
+
1
N
−
1
s
h
a
p
e
[
j
]
,
k
=
0
,
⋯
 
,
N
−
2
{\sf strides}[k] = \begin{cases} {\sf{itemsize}}, &k=N-1\\ {\sf{itemsize}} \times \prod_{j=k+1}^{N-1}{\sf{shape}}[j], &k=0,\cdots,N-2\\ \end{cases}
strides[k]={itemsize,itemsize×∏j=k+1N−1shape[j],k=N−1k=0,⋯,N−2
令数组
a
r
r
a
y
\sf array
array的首元素地址为
&
a
r
r
a
y
\&{\sf{array}}
&array,则数组中坐标为
(
i
0
,
i
1
,
⋯
 
,
i
N
−
1
)
(i_0,i_1,\cdots,i_{N-1})
(i0,i1,⋯,iN−1)的元素的地址
&
a
r
r
a
y
[
i
0
]
[
i
1
]
⋯
[
i
N
−
1
]
=
&
a
r
r
a
y
+
∑
k
=
0
N
−
1
i
k
×
s
t
r
i
d
e
s
[
k
]
\&{\sf{array}}[i_0][i_1]\cdots[i_{N-1}]= \&{\sf{array}} + \sum_{k=0}^{N-1}i_{k}\times {\sf{strides}}[k]
&array[i0][i1]⋯[iN−1]=&array+k=0∑N−1ik×strides[k]
利用以上公式计算出给定坐标对应的地址,即 & a r r a y [ i 0 ] [ i 1 ] ⋯ [ i N − 1 ] \&{\sf{array}}[i_0][i_1]\cdots[i_{N-1}] &array[i0][i1]⋯[iN−1],然后即可得到 a r r a y [ i 0 ] [ i 1 ] ⋯ [ i N − 1 ] {\sf{array}}[i_0][i_1]\cdots[i_{N-1}] array[i0][i1]⋯[iN−1]的值。
实例
对于数据类型为int32(占4字节)
的二维整型数组
b
=
(
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
)
\bm b = \left(\begin{matrix} 0 &1 &2 &3 \\ 4 &5 &6 &7 \\ 8 &{\bm\color {red}9}&10 &11 \\ 12 &13 &14 &15 \end{matrix}\right)
b=⎝⎜⎜⎛0481215913261014371115⎠⎟⎟⎞
其形状 s h a p e = ( 4 , 4 ) {\sf shape}=(4,4) shape=(4,4), i t e m s i z e = 4 {\sf itemsize}=4 itemsize=4,因此不难计算出跨度列表 s t r i d e s = [ 16 , 4 ] {\sf strides}=[16, 4] strides=[16,4]。
若以
b
[
2
]
[
1
]
b[2][1]
b[2][1]的索引方式访问元素
9
9
9,实际底层是将引用地址解析为
&
b
[
2
]
[
1
]
=
&
b
+
2
×
s
t
r
i
d
e
s
[
0
]
+
1
×
s
t
r
i
d
e
s
[
1
]
=
&
b
+
36
\&b[2][1] = \&b + 2 \times {\sf strides}[0] + 1\times {\sf strides}[1] = \&b+36
&b[2][1]=&b+2×strides[0]+1×strides[1]=&b+36
显然,地址 & b + 36 \&b+36 &b+36存储的值为目标元素 9 9 9,即完成了索引的解析。
2、切片索引的视图原理
切片索引的解析
对于二维数组
b
b
b,使用以下起始值及步长对其进行切片
a
=
b
[
b
e
g
i
n
0
:
e
n
d
0
:
s
t
e
p
0
,
b
e
g
i
n
1
:
e
n
d
1
:
s
t
e
p
1
]
a = b[{\sf begin_0:end_0:step_0}, {\sf begin_1:end_1:step_1}]
a=b[begin0:end0:step0,begin1:end1:step1]
易知,结果数组
a
a
a的首地址
&
a
=
&
b
[
b
e
g
i
n
0
,
b
e
g
i
n
1
]
=
&
b
+
b
e
g
i
n
0
×
s
t
r
i
d
e
s
[
0
]
+
b
e
g
i
n
1
×
s
t
r
i
d
e
s
[
1
]
\&a=\&b[{\sf begin_0}, {\sf begin_1}]=\&b+{\sf begin_0}\times {\sf strides}[0] + {\sf begin_1}\times {\sf strides}[1]
&a=&b[begin0,begin1]=&b+begin0×strides[0]+begin1×strides[1]
基于各维度的切片步长,更新切片索引结果数组
a
a
a的跨度列表
s
t
r
i
d
e
s
′
=
s
t
e
p
×
s
t
r
i
d
e
s
=
[
s
t
e
p
0
,
s
t
e
p
1
]
×
s
t
r
i
d
e
s
{\sf strides'} = {\sf step} \times {\sf strides} = [{\sf step_0}, {\sf step_1}] \times {\sf strides}
strides′=step×strides=[step0,step1]×strides
此时,利用结果数组 a a a的起始地址 & a \&a &a以及新的跨度列表 s t r i d e s ′ {\sf strides'} strides′,使用索引 a [ i ] [ j ] a[i][j] a[i][j]的形式依然可以访问到目标元素。
切片索引要求每一维度具有固定的切片步长,因此我们仅需要创建一个原始数组的视图,并根据起始位置以及各维度的切片步长,修改视图的起始位置以及各维度的跨度,即可访问到结果数组中的目标元素。因此,切片索引不需要复制原始数据。由于整数数组索引步数的随机性,不能通过更改索引方案的方案访问原数组。因此,整数数组索引返回的是原始数组的副本。
实例
对于数组
b
b
b,执行切片
a
=
b
[
:
:
3
,
1
:
:
2
]
a = b[::3, 1::2]
a=b[::3,1::2],得到红色标记位置元素
b
=
(
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
)
,
a
=
(
1
3
13
15
)
\bm b = \left(\begin{matrix} 0 &{\bm\color {red}1} &2 &{\bm\color {red}3} \\ 4 &5 &6 &7 \\ 8 &9 &10 &11 \\ 12 &{\bm\color {red}13} &14 &{\bm\color {red}15} \end{matrix}\right), \quad\bm a=\left(\begin{matrix} 1 &3\\ 13 &15 \end{matrix}\right)
b=⎝⎜⎜⎛0481215913261014371115⎠⎟⎟⎞,a=(113315)
原数组 b b b的跨度列表 s t r i d e s = [ 16 , 4 ] {\sf strides}=[16, 4] strides=[16,4],结果数组 a a a的首地址 & a = & b + 4 \&a=\&b+4 &a=&b+4,结果数组 a a a的跨度列表 s t r i d e s ′ = [ 48 , 8 ] {\sf strides'}=[48, 8] strides′=[48,8]。
通过索引
a
[
1
,
1
]
a[1,1]
a[1,1]的方式访问元素15,实际底层将引用地址解析为
&
a
[
1
]
[
1
]
=
&
a
+
1
×
s
t
r
i
d
e
s
′
[
0
]
+
1
×
s
t
r
i
d
e
s
′
[
1
]
=
&
a
+
56
=
&
b
+
60
\&a[1][1] = \&a + 1 \times {\sf strides'}[0] + 1\times {\sf strides'}[1] = \&a+56 = \&b+60
&a[1][1]=&a+1×strides′[0]+1×strides′[1]=&a+56=&b+60
此外
&
b
[
3
]
[
3
]
=
&
b
+
3
×
s
t
r
i
d
e
s
[
0
]
+
3
×
s
t
r
i
d
e
s
[
1
]
=
&
b
+
60
\&b[3][3] = \&b + 3 \times {\sf strides}[0] + 3\times {\sf strides}[1] = \&b+60
&b[3][3]=&b+3×strides[0]+3×strides[1]=&b+60
显然,使用切片索引仅通过更改索引方案,即可访问到原始数据。