Numpy多维数组的内存设计与实现原理

本文深入解析了Numpy库中ndarray对象的内存布局原理,包括一维连续内存段存储方式、索引解析机制及切片索引的视图原理。通过具体实例展示了如何计算元素地址和切片索引的跨度列表。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、内存设计与实现原理

ndarray的内存结构

ndarray的实例本质上由一个连续的一维内存段一个索引方案组合而成。这种将所有数据存放在一个连续的一维内存段的存储方式,实际与C语言中的多维数组存储方式一致。但Numpy索引的灵活设计,使得ndarray对象可适应于任何跨步索引方案,以下对ndarray对象以行作为主要存储顺序的内存设计进行说明。

Numpy在创建数组或建立数组视图时,将数组的信息记录在不同属性,如shape属性指定各维度的元素的数量,dtype属性指定元素类型及其解释方式,strides属性指定各维度的跨度,itemsize属性指定单个元素占用字节数。

图1 ndarray对象在内存中的存储与索引

索引的解析

对于NNN维的数组array\sf arrayarray,将各维度的跨度存放在strides\sf{strides}strides列表中,则第k+1k+1k+1维的跨度
strides[k]={itemsize,k=N−1itemsize×∏j=k+1N−1shape[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+1N1shape[j],k=N1k=0,,N2

令数组array\sf arrayarray的首元素地址为&array\&{\sf{array}}&array,则数组中坐标为(i0,i1,⋯ ,iN−1)(i_0,i_1,\cdots,i_{N-1})(i0,i1,,iN1)的元素的地址
&array[i0][i1]⋯[iN−1]=&array+∑k=0N−1ik×strides[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][iN1]=&array+k=0N1ik×strides[k]

利用以上公式计算出给定坐标对应的地址,即&array[i0][i1]⋯[iN−1]\&{\sf{array}}[i_0][i_1]\cdots[i_{N-1}]&array[i0][i1][iN1],然后即可得到array[i0][i1]⋯[iN−1]{\sf{array}}[i_0][i_1]\cdots[i_{N-1}]array[i0][i1][iN1]的值。

实例

对于数据类型为int32(占4字节)的二维整型数组
b=(0123456789101112131415)\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

其形状shape=(4,4){\sf shape}=(4,4)shape=(4,4)itemsize=4{\sf itemsize}=4itemsize=4,因此不难计算出跨度列表strides=[16,4]{\sf strides}=[16, 4]strides=[16,4]

若以b[2][1]b[2][1]b[2][1]的索引方式访问元素999,实际底层是将引用地址解析为
&b[2][1]=&b+2×strides[0]+1×strides[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存储的值为目标元素999,即完成了索引的解析。

2、切片索引的视图原理

切片索引的解析

对于二维数组bbb,使用以下起始值及步长对其进行切片
a=b[begin0:end0:step0,begin1:end1:step1]a = b[{\sf begin_0:end_0:step_0}, {\sf begin_1:end_1:step_1}]a=b[begin0:end0:step0,begin1:end1:step1]

易知,结果数组aaa的首地址
&a=&b[begin0,begin1]=&b+begin0×strides[0]+begin1×strides[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]

基于各维度的切片步长,更新切片索引结果数组aaa的跨度列表
strides′=step×strides=[step0,step1]×strides{\sf strides'} = {\sf step} \times {\sf strides} = [{\sf step_0}, {\sf step_1}] \times {\sf strides}strides=step×strides=[step0,step1]×strides

此时,利用结果数组aaa的起始地址&a\&a&a以及新的跨度列表strides′{\sf strides'}strides,使用索引a[i][j]a[i][j]a[i][j]的形式依然可以访问到目标元素。

切片索引要求每一维度具有固定的切片步长,因此我们仅需要创建一个原始数组的视图,并根据起始位置以及各维度的切片步长,修改视图的起始位置以及各维度的跨度,即可访问到结果数组中的目标元素。因此,切片索引不需要复制原始数据。由于整数数组索引步数的随机性,不能通过更改索引方案的方案访问原数组。因此,整数数组索引返回的是原始数组的副本。

实例

对于数组bbb,执行切片a=b[::3,1::2]a = b[::3, 1::2]a=b[::3,1::2],得到红色标记位置元素
b=(0123456789101112131415),a=(131315)\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)

原数组bbb的跨度列表strides=[16,4]{\sf strides}=[16, 4]strides=[16,4],结果数组aaa的首地址&a=&b+4\&a=\&b+4&a=&b+4,结果数组aaa的跨度列表strides′=[48,8]{\sf strides'}=[48, 8]strides=[48,8]

通过索引a[1,1]a[1,1]a[1,1]的方式访问元素15,实际底层将引用地址解析为
&a[1][1]=&a+1×strides′[0]+1×strides′[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×strides[0]+3×strides[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

显然,使用切片索引仅通过更改索引方案,即可访问到原始数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值