1. 概述
内存布局(Memory Layout
)是指数据或程序在计算机内存中的组织方式,它决定了数据如何存储、访问和解释,存布局的设计会直接影响性能、空间利用率和兼容性。
Java
程序运行时,JVM
会内存中分配堆(Heap
)、栈(Stack
)、方法区(Method Area
)… 在 Hotspot VM
中 Java
对象内存布局分为对象头(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_contiguous | bool | 数组是否按 C 语言连续顺序存储(行优先)。 |
F_CONTIGUOUS | 'F' | a.flags.f_contiguous | bool | 数组是否按 Fortran 连续顺序存储(列优先)。 |
OWNDATA | 无 | a.flags.owndata | bool | 数组是否拥有其数据缓冲区( True 表示数据由本数组分配)。 |
WRITEABLE | 'W' | a.flags.writeable | bool | 数据是否可修改(设为 False 后,尝试写入会抛出错误)。 |
ALIGNED | 'A' | a.flags.aligned | bool | 数据是否按硬件要求进行内存对齐(影响计算性能)。 |
WRITEBACKIFCOPY | 无 | a.flags.writebackifcopy | bool | 数组是否为拷贝并在释放时写回原数据(用于优化操作,如.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
只有 WRITEBACKIFCOPY
、WRITEABLE
和 ALIGNED
标识可以通过以下几种方式进行修改:
- 直接属性赋值。
- 修改字典。
- 对象方法:
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进制) | 0x00 | 0x04 | 0x08 | 0x0C | 0x10 | 0x14 | 0x18 | 0x1C | 0x20 | 0x24 | 0x28 | 0x2C |
---|---|---|---|---|---|---|---|---|---|---|---|---|
元素值 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
逻辑位置 (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
图示,二维数组内存中按列优先顺序排列为: