NumPy 作为 Python 科学计算的基础库,其核心是 ndarray 对象。数组在内存中的存储顺序是其最重要的底层特性之一,NumPy 原生支持两种存储顺序:C 风格(行优先) 和 Fortran 风格(列优先)。这个设计并非偶然,而是源于科学计算的历史与性能需求。下面将从来源、内存布局、在 NumPy 中的表现形式、对性能的影响以及实际应用等方面详细讲解。
1. 来源:C 与 Fortran 的历史背景
在数值计算领域,C 语言和 Fortran 语言长期占据统治地位,它们对多维数组采用了截然不同的内存排列约定:
-
C 语言(行优先,Row-major)
在 C 中,二维数组定义为
a[rows][cols],内存中最后一维(列)变化最快。即先存储第一行的所有列元素,然后第二行,依此类推。例如,矩阵
[[1,2],[3,4]]在内存中的线性顺序是1,2,3,4。C 的数组本质上是"数组的数组",这种布局天然与下标运算一致。
-
Fortran 语言(列优先,Column-major)
在 Fortran 中,二维数组定义为
a(rows,cols),内存中第一维(行)变化最快。先存储第一列的所有行元素,再存储第二列......同样的矩阵内存排列为
1,3,2,4。这种习惯源于早期 Fortran 设计,并且很多线性代数库(如经典 BLAS/LAPACK,虽然后来也有 C 接口)内部遵循列优先存储,因为矩阵列向量在内存中连续更有利于向量化操作。
NumPy 支持这两种存储顺序,一方面是为了兼容不同语言生态的库 (如与 C 扩展或 Fortran 库无缝交换数据),另一方面是为了让用户针对算法特点优化内存访问模式,充分利用 CPU 缓存。
2. 内存布局特点
2.1 行优先(C order)与列优先(Fortran order)
存储顺序影响多维数组的线性展开方式:
- C 顺序(
order='C'):最右边的轴变化最快,从轴 (axis=0) 到轴 (axis=N-1) 遍历时,最后一个轴的索引先递增。对二维来说就是先行后列。 - Fortran 顺序(
order='F'):最左边的轴变化最快,第一个轴的索引先递增。对二维来说就是先列后行。
NumPy 用 strides(步幅)元组描述这一特性:从当前元素移动到下一元素需要在内存中跳过的字节数。
python
import numpy as np
a = np.array([[1,2],[3,4]], order='C')
b = np.array([[1,2],[3,4]], order='F')
print(a.strides) # 假设 int64,输出 (16, 8) -> 行步幅16字节,列步幅8字节
print(b.strides) # 输出 (8, 16) -> 行步幅8字节,列步幅16字节
C 顺序下,列索引变化最快,因此列方向步幅最小;Fortran 顺序下行索引变化最快,行方向步幅最小。
2.2 连续性的概念
- C 连续(C_CONTIGUOUS) :数组在内存中按 C 顺序平坦存储,且
strides符合 C 布局规律。.flags['C_CONTIGUOUS']为 True。 - Fortran 连续(F_CONTIGUOUS) :按 Fortran 顺序平坦存储,
.flags['F_CONTIGUOUS']为 True。
一个数组可以同时是 C 连续和 F 连续的(例如一维数组,或者单行/单列的二维数组)。视图、切片等操作可能破坏连续性。
3. 在 NumPy 中的使用方式
3.1 创建时指定顺序
绝大多数数组创建函数都接受 order 参数:
python
np.array([[1,2],[3,4]], order='C') # 默认 'C'
np.zeros((3,4), order='F')
np.ones((2,3,4), order='C')
np.empty((5,5), order='F')
order 的可取值:'C'、'F'、'A' 等。'A' 表示若输入是 Fortran 连续则保持 Fortran,否则用 C(不常用作显式创建,多在方法中出现)。
3.2 查看存储顺序
除了检查 strides,还可以用:
python
arr.flags['C_CONTIGUOUS']
arr.flags['F_CONTIGUOUS']
一个直接的规律:arr.flags.c_contiguous 等属性亦可用。
3.3 转换存储顺序
-
生成新连续数组(复制数据)
np.ascontiguousarray(arr)→ 保证 C 连续np.asfortranarray(arr)→ 保证 Fortran 连续arr.copy(order='F')→ 复制并转为指定顺序这些操作都会重新排列内存中的数据。
-
转置与顺序变化
arr.T返回视图,并不移动数据,只是交换strides。若原数组 C 连续,则转置后变为 Fortran 连续(内存仍按原先的行优先排列,但逻辑上按列优先遍历)。这不会复制数据,但会导致内存访问模式改变。 -
reshape 与 order
arr.reshape(new_shape, order='C')会尽可能返回视图,若要求的内存布局与原数组连续顺序冲突,则会复制。指定order='F'会像 Fortran 那样读取元素并用 Fortran 布局构造新数组。 -
展平操作
arr.flatten(order='C')总是返回复制。arr.ravel(order='C')返回视图(若可能),否则复制。
order控制展平时遍历元素的顺序:'C'按行优先读取,'F'按列优先,'A'根据原数组在内存中的实际顺序展平(如果 Fortran 连续则按列优先,否则按行优先),'K'按元素在内存中出现的顺序展平(与 strides 的物理顺序一致)。
4. 对性能的影响
4.1 缓存局部性
现代 CPU 依赖多级缓存,按内存连续方向遍历数组可大幅提升缓存命中率。对于大型数组的逐元素运算:
- C 连续数组,沿最后一个轴(列)循环的代码最快,因为在内存中地址连续递增。
- F 连续数组,沿第一个轴(行)循环的代码最快。
因此,若算法主要对列向量操作(例如线性代数中的逐列处理),F 顺序有优势;若对行操作多,C 顺序更佳。
4.2 与外部库的交互
-
Python C 扩展 通常期望 C 连续数组,使用
PyArray_ContiguousFromObject等。 -
经典 Fortran 库(LAPACK、BLAS) 大多要求 Fortran 连续数组。NumPy 的
np.dot、np.linalg等底层调用这些库,若输入数组不是期望的顺序,内部会进行复制转换,可能带来额外开销。知道数据的布局可以帮助我们预先转换以消除隐藏开销。 -
文件 I/O 与跨语言交互 :用
np.fromfile或ndarray.tofile时,存储顺序影响二进制排列。与其他语言(如 C、Fortran、Matlab)交换数据时必须对齐内存布局。
4.3 切片导致的非连续
切片不复制数据,但可能产生非连续数组(例如 arr[:, 1:3] 对 C 连续数组通常仍保持 C 连续,但 arr[::2, :] 就不连续)。对于非连续数组,矢量化运算会以更复杂的步幅访问内存,速度可能下降。可以用 np.ascontiguousarray() 强制转换为连续数组改善性能(但需付出复制代价)。
5. 高级 order 选项:'A' 和 'K'
这些选项出现在 reshape、ravel 等操作中,用于控制如何遍历元素:
'A'(Any/Preserve):保留原始数组的内存布局。如果原数组是 Fortran 连续,则按照 Fortran 顺序重塑/展平;否则默认按 C 顺序。这在不知道原始布局时很有用。'K'(Keep order) :按照元素在内存中出现的物理顺序(由strides决定,不一定整齐的行列顺序)。对于具有复杂步幅的非连续数组,'K'能原样展平内存视图。
例:
python
a = np.array([[1,2],[3,4]], order='F')
a.ravel(order='A') # 返回 array([1,3,2,4]),因为原数组F连续
a.ravel(order='K') # 返回 array([1,3,2,4]),内存中实际存储顺序
a.ravel(order='C') # 返回 array([1,2,3,4]),始终按C顺序读取
6. 总结与最佳实践
- 默认是 C 顺序,这符合大多数 Python 和 C 扩展的预期。
- 了解数据访问模式:如果你的代码主要按列循环,并且数组很大,可以主动使用 Fortran 顺序以改善缓存效率。
- 与 Fortran 库交互(如 SciPy 中部分求解器、经典的 LAPACK 包装)时,最好显式传递 Fortran 连续数组,避免内部隐式复制。
- 谨慎切片 :大量使用步长不为 1 的切片可能导致非连续数组,必要时显式调用
np.ascontiguousarray换取缓存友好性。 - 转置不会改变数据布局 :
arr.T与arr共享内存,只是交换了行列步幅,逻辑上变成列优先,但物理内存仍是原先的顺序。如果后续需要真正的 Fortran 连续副本,用arr.T.copy(order='F')或直接np.asfortranarray(arr.T)。 - 多维数组的创建与重塑 应明确
order,尤其是在管道中数据来自不同来源时,统一布局可以减少错误与开销。
NumPy 的 C/Fortran 顺序设计是其能成为跨语言科学计算桥梁的关键,理解内存布局不仅能写出更高效的代码,也是进行高性能数值计算的基础。
更多机器学习、Python编程语言的免费视频课程,参阅:https://space.bilibili.com/157232748