连续指多维张量的逻辑元素在底层一维 Storage 里紧密、按序排列。很多底层算子与内存操作只认连续张量;非连续张量靠跨步寻址仍能读写,但访存低效,部分 API 直接报错。
底层算子性能
这是连续性最核心的价值。PyTorch 的 CPU / CUDA 内核大量针对连续内存做向量化、SIMD 与 GPU 合并访存优化:连续张量的元素在内存中顺序排列,CPU 可以预取,GPU 能 coalesced 合并访存,带宽吃满;非连续张量(如转置得到的 b)靠 跨步寻址访问,地址跳跃,无法合并访存,缓存命中率骤降,速度明显变慢。
非连续不是不能读——跨步寻址可以正常读写——只是内存访问低效。
强制要求连续的 API
.view() 只能作用在连续张量上,非连续时直接抛错;reshape 会自动判断,不连续时先做 contiguous 拷贝:
a = torch.arange(6).reshape(2,3)
b = a.t()
b.view(6) # RuntimeError: view size is not compatible with input tensor's size and stride.view() 不拷贝数据,只修改 shape / stride,依赖连续内存;非连续的布局无法简单重解释 shape。此外 torch.frombuffer()、部分原生 C++ 扩展与自定义 CUDA kernel 同样要求输入连续。
零拷贝与 NumPy 互操作
tensor.numpy() 与张量零拷贝共享内存的前提之一是连续;非连续张量转 numpy 时内部会自动拷贝,不再共享 Storage:
a = torch.arange(6).reshape(2,3)
b = a.t()
arr = b.numpy() # 这里会拷贝,不再和a共享Storage!切片同理:连续张量的切片(a[:, :2])多能维持视图零拷贝,非连续张量的切片更容易触发隐式拷贝。
隐式拷贝的内存开销
很多函数遇到非连续张量时,会在内部静默调用 .contiguous() 拷贝一份新内存:代码里没写 .contiguous(),运行时却自动分配新 Storage、复制数据,凭空多出内存与时间开销,是最容易踩的性能坑之一。
C 连续与 Fortran 连续
PyTorch 默认 C 连续(行优先):最后一维的相邻元素在内存中相邻。.t() 得到的转置张量是 Fortran 连续(列优先),而 is_contiguous() 只判断 C 连续,所以返回 False:
b.is_contiguous() # False(C连续?否)
b.is_f_contiguous() # True(Fortran连续?是)F 连续同样是内存紧密排布,只是顺序为列优先;部分算子也能利用 F 连续做优化。
何时手动调用 .contiguous()
- 调用
.view()之前 - 传给自定义 C++ / CUDA 算子
- 转 numpy 且想保持零拷贝
- 张量运算异常慢时,排查是否非连续
连续与非连续对照
| 特性 | 连续张量 | 非连续张量(转置、切片) |
|---|---|---|
| 数据存储 | 逻辑元素在底层一维内存紧密排列 | 内存跳跃访问,stride 不规则 |
.view() | 可用 | 报错 |
| 访存速度 | 快,SIMD / GPU 合并访存 | 慢,跳跃访问 |
| 视图零拷贝 | 可用 | 可用(但很多操作会触发隐式拷贝) |
.numpy() | 零拷贝共享 | 自动拷贝 |
误区澄清
「非连续张量不能读写」是误解:跨步寻址可以正常读写(对转置后的
b直接索引赋值即可验证),代价只是访存变慢、部分 API 不支持。