NumPy 2.x 完全指南【十二】内存布局

1. 概述

内存布局Memory Layout)是指数据或程序在计算机内存中的组织方式,它决定了数据如何存储、访问和解释,存布局的设计会直接影响性能、空间利用率和兼容性。

Java 程序运行时,JVM 会内存中分配堆(Heap)、栈(Stack)、方法区(Method Area)… 在 Hotspot VMJava 对象内存布局分为对象头(Header)、实例数据(Instance Data)…

Python 的内存机制也有很多类似之处,在 CPython 解释器中对象的内存布局通常包含以下部分:

  • 对象头(Header):用于存储对象的基本信息,如对象的引用计数、类型信息等。
  • 数据存储区:存储对象的实际数据。

Numpy 数组对象是由两部分组成的数据结构:

  • 元数据:存放在 Header 中,包括数据类型、步幅以及其他重要信息。
  • 数据缓冲区:实际数据元素的连续内存空间。

在这里插入图片描述

2. 内存布局属性

ndarray 扩展了标准的 Python 对象头,额外包含数组的维度(shape)、步幅(strides)、数据类型指针(dtype)、数据缓冲区指针(data)等元信息。这些信息主要通过布局属性进行查看,之前我们已经介绍过常用的一些属性,接着介绍剩下的。

内存布局属性定义了数组的结构、内存布局和数据类型:

属性名类型描述
ndarray.flags属性数组内存布局的信息(如连续性、读写权限等)
ndarray.shape属性数组维度组成的元组(例如 (3, 4) 表示 3 行 4 列的二维数组)
ndarray.strides属性遍历数组时,每个维度中步进的字节数组成的元组
ndarray.ndim属性数组的维度数量(如标量=0,向量=1,矩阵=2)
ndarray.data属性指向数组数据起始位置的 Python 缓冲区对象
ndarray.size属性数组中元素的总数(等于各维度大小的乘积)
ndarray.itemsize属性单个数组元素的字节长度(如 float64 类型的元素为 8 字节)
ndarray.nbytes属性数组元素消耗的总字节数(等于 size * itemsize
ndarray.base属性若数组共享其他对象的内存,则指向基对象;否则为 None

2.1 flags(标识)

ndarray.flags:返回数组内存布局信息。

flags 是一个描述数组内存布局和属性的对象,包含多个布尔标识:

属性名(大写)短标识属性访问写法类型描述
C_CONTIGUOUS'C'a.flags.c_contiguousbool数组是否按 C 语言连续顺序存储(行优先)。
F_CONTIGUOUS'F'a.flags.f_contiguousbool数组是否按 Fortran 连续顺序存储(列优先)。
OWNDATAa.flags.owndatabool数组是否拥有其数据缓冲区( True 表示数据由本数组分配)。
WRITEABLE'W'a.flags.writeablebool数据是否可修改(设为 False 后,尝试写入会抛出错误)。
ALIGNED'A'a.flags.alignedbool数据是否按硬件要求进行内存对齐(影响计算性能)。
WRITEBACKIFCOPYa.flags.writebackifcopybool数组是否为拷贝并在释放时写回原数据(用于优化操作,如.base的拷贝)。

flags 对象可以通过字典方式访问,也可以使用小写属性名称,短标识名称仅在字典访问时支持:

arr = np.array([[1, 2],
                [3, 4]],
               dtype=np.int32)
# 字典方式访问(长标识)
print(arr.flags['WRITEABLE']) # True
# 字典方式访问(短标识)
print(arr.flags['W']) # True
# 小写属性名称访问
print(arr.flags.writeable) # True

只有 WRITEBACKIFCOPYWRITEABLEALIGNED 标识可以通过以下几种方式进行修改:

  • 直接属性赋值。
  • 修改字典。
  • 对象方法:ndarray.setflags

在修改时也不能随意设置:

  • WRITEBACKIFCOPY 只能设置为 False
  • ALIGNED 只有在数据真正对齐时才可以设置为 True
  • WRITEABLE 只有在数组拥有自己的内存,或内存的最终拥有者暴露一个可写的缓冲区接口或是字符串时,才可以设置为 True
  • 数组可以同时是 C 风格和 Fortran 风格的连续数组。

示例,修改 WRITEABLE 标识:

# 直接赋值
arr.flags.writeable = False
# 字典赋值
arr.flags['WRITEABLE'] = False
# setflags()
arr.setflags(write=False)

# 字典方式访问(长标识)
print(arr.flags['WRITEABLE'])  # True

2.2 data(缓冲区对象)

ndarray.data:返回一个 memoryview 对象,指向整个数组的数据缓冲区起始地址。

注意事项:

  • 提供了一种直接访问数组原始字节数据的方式,常用于底层内存操作或与其他语言(如 C )交互。
  • 操作需谨慎,确保数据类型、内存范围合法。
  • 优先使用高阶接口(如 a[...] 索引或 a.tobytes())以保证安全。

示例 1 ,打印底层数据缓冲区的内存地址:

a = np.array([[1, 2],
              [3, 4]], dtype=np.int32)
print(a.data)  # 输出: <memory at 0x00000118E0005150>

data 返回的是一个 memoryview 对象,它是 Python 提供的一个内置对象,允许在不复制对象的情况下直接操作对象的内存缓冲区。直接打印该对象,输出的是内存地址的十六进制表示,表示某个对象(如数组的底层数据缓冲区)在计算机内存中的位置,本身无实际数据意义,仅标识数据在内存中的存储起始位置。
在这里插入图片描述

2.3 strides(步幅)

ndarray.strides:在遍历数组时每个维度的字节步长元组。

注意事项:

  • 不建议设置 arr.strides,并且未来可能会被弃用。
  • 应优先使用 numpy.lib.stride_tricks.as_strided 来以更安全的方式创建相同数据的新视图。

示例 1 ,数据类型为 int32(每个元素占 4 字节)的二维数组(默认 C 风格):

arr = np.arange(12,dtype=np.int32).reshape(3, 4)
print(arr)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]
print(arr.dtype) # int32 ,每个元素占用 4 个字节
print(arr.strides) # (16, 4)

上面二维数组在内存中是一维按顺序排列的字节序列,假设数组起始地址为 0x00,每个元素占 4 字节,内存布局如下:

内存地址(16进制)0x000x040x080x0C0x100x140x180x1C0x200x240x280x2C
元素值01234567891011
逻辑位置 (i,j)(0,0)(0,1)(0,2)(0,3)(1,0)(1,1)(1,2)(1,3)(2,0)(2,1)(2,2)(2,3)

strides 返回 (16, 4) 表示:

  • O 轴(行)步幅:跳转到下一行时在内存中要跳过多少字节,计算方式: 4(跳转到下一行时元素个数) * 4(每个元素占用字节) = 16 (步幅)。
  • 1 轴(列)步幅:跳转到下一列,计算方式: 1(跳转到下一列时经过的元素个数) * 4(每个元素占用字节) = 4 (步幅)。

有了形状(shape)、步幅(strides)、起始内存地址(data)、存储顺序(C 或者 F)后,就可以将多维结构映射到一维内存空间…

2.4 base(数据来源)

ndarray.base:用于表示当前数组的数据来源。

返回值:

  • None:当前数组有自己的数据缓冲区。
  • 另一个 ndarray 对象:当前数组是另一个数组的视图(共享数据缓冲区)。

通过此属性,可以判断一个数组是视图(view)还是独立数组(拥有自己的数据):

x = np.array([1, 2, 3, 4])
# x.base is None
print(x.base is None) # True
# owndata:数组是否拥有其数据缓冲区( `True` 表示数据由本数组分配)。
print(x.flags.owndata) # True

# 创建视图(切片)
b = x[1:]                 # x 是 a 的视图
print(b.base) # b.base 是一个数组对象,输出 [1 2 3 4]

3. 数据缓冲区

ndarray 实例的数据缓冲区由一个连续的、单一维度的计算机内存片段组成,该内存片段由数组拥有,或由其他对象拥有,存储了所有元素的原始二进制数据。

我们常说 N 维数组有多少层、行、列,是逻辑上的数据结构表示,而物理内存是由连续的存储单元构成,每个单元通过内存地址访问,无论逻辑上如何抽象,数据最终必须映射到线性地址空间,这是由硬件物理特性决定的。

所以,在物理内存中是不存在 N 维结构的,多维结构都需要映射为一维的内存块进行存储。也就是说,在内存中永远只有一维结构,所有数据必须映射为一维连续内存块中。

多维数组的逻辑索引映射到一维物理内存中有两种主要策略:

  • 行优先(C 顺序)
  • 列优先(Fortran 顺序)

3.1 行优先

行优先:同一行的元素在内存中连续存储,不同行的元素依次排列。在 NumPy 中称为 C 顺序,,因为 C 语言默认采用此方式存储多维数组。

NumPy 在创建数组时默认也使用 C 顺序存储,示例 1 ,查看数组是否是 C 顺序存储:

arr_2d = np.arange(6).reshape(2, 3)
print(arr_2d)
# [[0 1 2]
#  [3 4 5]]

# 查看是否是 C 连续:
print(arr_2d.flags.c_contiguous)  # True

可以通过多种方式设置为行优先:

  • 显式指定 order='C' (默认)。
  • ascontiguousarray 函数。

示例:

np.array([1, 2, 3], order='C')
arr_2d = np.arange(6).reshape(2, 3, order='C')

图示,二维数组内存中按行优先顺序排列为:
在这里插入图片描述

以此类推,三维或者更高维的数组,从第一个维度的第一行开始,依次将所有行映射到一维的内存结构。

3.2 列优先

列优先:同一列的元素在内存中连续存储,不同列的元素依次排列。在 NumPy 中称为 Fortran 顺序,因为 Fortran 语言默认采用此方式存储多维数组。

示例 1 ,查看数组是否是 F 顺序:

arr_2d = np.arange(6).reshape(2, 3)
print(arr_2d.flags.f_contiguous)  # False

可以通过多种方式设置为列优先:

  • 显式指定 order='F'
  • asfortranarray 函数。

示例 2 :

b = np.array([[1, 3], [2, 4]])  # 默认行优先
print(b.flags.f_contiguous)  # False
# 1.asfortranarray
b_fortran = np.asfortranarray(b)  # 转换为列优先
print(b_fortran.flags.f_contiguous)  # True
# 2. 创建时指定
c = np.array([[1, 3], [2, 4]], order="F")  
print(c.flags.f_contiguous)  # True

图示,二维数组内存中按列优先顺序排列为:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨 禹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值