2.2 C序F序与连续性检测 本节摘要:C 序按行填满内存,F 序按列填满内存,两者是同一块数据的两种铺法。flags 属性的 CCONTIGUOUS 与 FCONTIGUOUS 标志用于检测连续性;ascontiguousarray 与 asfortranarray 负责在不满足时生成连续拷贝。什么时候必须转换、什么时候不必,本节给出工程判断。 两种铺法从哪来 一个 2×3 数组,六元素的数据块只有一种物理排列:1、2、3、4、5、6 依次躺在内存里。但把它解释成"先填满第一行再填第二行"(C 序,C 语言传统),还是"先填满第一列再填第二列"(F 序,Fortran 传统),得到的是两个不同的逻辑数组。
本节摘要:C 序按行填满内存,F 序按列填满内存,两者是同一块数据的两种铺法。flags 属性的 C_CONTIGUOUS 与 F_CONTIGUOUS 标志用于检测连续性;ascontiguousarray 与 asfortranarray 负责在不满足时生成连续拷贝。什么时候必须转换、什么时候不必,本节给出工程判断。
一个 2×3 数组,六元素的数据块只有一种物理排列:1、2、3、4、5、6 依次躺在内存里。但把它解释成"先填满第一行再填第二行"(C 序,C 语言传统),还是"先填满第一列再填第二列"(F 序,Fortran 传统),得到的是两个不同的逻辑数组。行列顺序之争是历史遗留:数值计算的老祖宗 Fortran 按列存,C 语言按行存,NumPy 两种都支持,默认 C 序。
import numpy as np c = np.array([[1, 2, 3], [4, 5, 6]], order="C") # 默认,按行填充 f = np.array([[1, 2, 3], [4, 5, 6]], order="F") # 按列填充 print("C 序 strides:", c.strides) # (24, 8) 行内相邻 print("F 序 strides:", f.strides) # (8, 24) 列内相邻 print("两者数值相等:", np.array_equal(c, f)) # True,逻辑值完全一样
数值一样、步长相反。逻辑层面看不出区别的数组,在内存层面是两种东西——这就是本章反复强调的:不看 strides 就不算真正认识一个数组。

数组的 flags 属性是一张"体检表",其中两项最常用:
import numpy as np a = np.arange(12).reshape(3, 4) print(a.flags["C_CONTIGUOUS"]) # True:按行连续 print(a.flags["F_CONTIGUOUS"]) # False at = a.T print(at.flags["C_CONTIGUOUS"]) # False print(at.flags["F_CONTIGUOUS"]) # True:转置后反而变成 F 序连续! # 一维数组两种连续都为 True v = np.arange(5) print(v.flags["C_CONTIGUOUS"], v.flags["F_CONTIGUOUS"]) # True True # 跨步切片:两种连续都不是 s = a[:, ::2] print(s.flags["C_CONTIGUOUS"], s.flags["F_CONTIGUOUS"]) # False False
注意转置的例子:C 序数组转置后 F_CONTIGUOUS 变为 True。连续性没有丢,只是换了一个方向。真正两头都不连续的,是跨步切片这类操作的结果。
当某个接口只接受 C 序连续数组时,用 ascontiguousarray 生成连续拷贝:
import numpy as np a = np.arange(12).reshape(3, 4) at = a.T c_view = np.ascontiguousarray(at) print(c_view.flags["C_CONTIGUOUS"]) # True print(np.shares_memory(c_view, at)) # False —— 发生了真实拷贝 print(np.array_equal(c_view, at)) # True,数值不变 # 已连续的数组传入,不会白白拷贝,原样返回 same = np.ascontiguousarray(a) print(np.shares_memory(same, a)) # True
工程判断:一次性使用就吃转置视图的零拷贝红利;反复使用(循环里多次参与计算)就花一次拷贝买回连续性。经验公式是"拷贝成本约等于一次同规模运算",后续计算超过两三次,拷贝就值回票价。
一个真实场景的排查演练。背景:某图像处理函数接收外部传来的数组,内部做上百次卷积。现象:同样的数据,从不同入口进来速度差 5 倍。排查操作:打一行 flags——
import numpy as np img = np.random.rand(1080, 1920) img_f = np.asfortranarray(img) # 模拟从 F 序接口进来的数据 print("C 序输入:", img.flags["C_CONTIGUOUS"]) # True print("F 序输入:", img_f.flags["C_CONTIGUOUS"]) # False # 统一入口:一行代码修复 def process(x): x = np.ascontiguousarray(x) # 已连续则零开销,不连续则拷贝一次 return x.sum(axis=1) # 后续上百次行方向计算全部吃满缓存 print(process(img).shape) # (1080,) print(process(img_f).shape) # (1080,)
结果:入口统一后,两种来源的耗时一致。解读:慢的不是算法,是每次调用都在非连续视图上做行方向扫描。变式:如果函数内部是列方向计算,就换成 asfortranarray,或干脆把算法改成沿连续轴计算。
下一节把视角从"怎么摆"切换到"动没动"——视图与拷贝的分界线,这是全册最重要的一节。