NumPy数组的C风格和Fortran风格

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.dotnp.linalg 等底层调用这些库,若输入数组不是期望的顺序,内部会进行复制转换,可能带来额外开销。知道数据的布局可以帮助我们预先转换以消除隐藏开销。

  • 文件 I/O 与跨语言交互 :用 np.fromfilendarray.tofile 时,存储顺序影响二进制排列。与其他语言(如 C、Fortran、Matlab)交换数据时必须对齐内存布局。

4.3 切片导致的非连续

切片不复制数据,但可能产生非连续数组(例如 arr[:, 1:3] 对 C 连续数组通常仍保持 C 连续,但 arr[::2, :] 就不连续)。对于非连续数组,矢量化运算会以更复杂的步幅访问内存,速度可能下降。可以用 np.ascontiguousarray() 强制转换为连续数组改善性能(但需付出复制代价)。


5. 高级 order 选项:'A''K'

这些选项出现在 reshaperavel 等操作中,用于控制如何遍历元素:

  • '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. 总结与最佳实践

  1. 默认是 C 顺序,这符合大多数 Python 和 C 扩展的预期。
  2. 了解数据访问模式:如果你的代码主要按列循环,并且数组很大,可以主动使用 Fortran 顺序以改善缓存效率。
  3. 与 Fortran 库交互(如 SciPy 中部分求解器、经典的 LAPACK 包装)时,最好显式传递 Fortran 连续数组,避免内部隐式复制。
  4. 谨慎切片 :大量使用步长不为 1 的切片可能导致非连续数组,必要时显式调用 np.ascontiguousarray 换取缓存友好性。
  5. 转置不会改变数据布局arr.Tarr 共享内存,只是交换了行列步幅,逻辑上变成列优先,但物理内存仍是原先的顺序。如果后续需要真正的 Fortran 连续副本,用 arr.T.copy(order='F') 或直接 np.asfortranarray(arr.T)
  6. 多维数组的创建与重塑 应明确 order,尤其是在管道中数据来自不同来源时,统一布局可以减少错误与开销。

NumPy 的 C/Fortran 顺序设计是其能成为跨语言科学计算桥梁的关键,理解内存布局不仅能写出更高效的代码,也是进行高性能数值计算的基础。

更多机器学习、Python编程语言的免费视频课程,参阅:https://space.bilibili.com/157232748

相关推荐
逝水无殇2 小时前
C# 正则表达式详解
开发语言·后端·正则表达式·c#
朱永博2 小时前
使用Onnruntime实现Padim/Patchcore推理
开发语言·c#
前端H2 小时前
Biome & Rolldown:Rust 工具链接管前端基建
开发语言·前端·rust
天天进步20152 小时前
UI-TARS 源码解析 #4:UI-TARS 与传统 RPA 的区别:为什么它不是简单的坐标点击脚本?
开发语言
运维行者_3 小时前
广域网性能监控:分布式IT架构下的链路质量保障
开发语言·网络·分布式·后端·架构·数据库架构
梅雅达编程笔记3 小时前
零基础学 Python 第7章 | 字典 dict:键值对存储
开发语言·python·beautifulsoup·numpy·pandas
灯澜忆梦3 小时前
GO---map函数
开发语言·前端·后端·golang
hehelm3 小时前
Linux网络编程—HTTP静态文件服务器
linux·服务器·开发语言·网络·c++
北冥you鱼3 小时前
Go语言与默克尔树:区块链数据完整性的基石
开发语言·golang·区块链