一句话定义:视图(View)只是原始数据上的一个“取景框”,它只记录从哪个位置开始看、按什么方式看,并不搬动内容;拷贝(Copy)则是把内容完整复制一份,存放到新的内存里。
在 NumPy、PyTorch 这类库里,一个张量(tensor)其实是两样东西:一块实实在在的内存,加上少量元数据,包括数据从哪个位置开始(offset)、每沿某一维前进一步跨过多少个元素(stride,步长)、形状(shape)是什么、元素是什么类型(dtype)。真正占空间的只有内存那一半。
打个比方:图书馆里的一本书是原始数据,目录卡片上写着“第 3 章从第 88 页开始,每页 40 行”。reshape(重排形状)、transpose(转置)、切片(slicing)这些操作,改动的只是卡片,书本身没有动。所以它们快得离谱:耗时几乎与数据量无关。一个几十 GB 的张量做一次转置,可能比你眨一次眼还快,因为它一个字节都没有搬动。而 copy 相当于把整本书复印一份,耗时随数据量线性增长。
理解这一切的关键是 stride。转置之所以是 O(1),是因为它只交换了两个维度的 stride;但代价是数据在内存里不再连续,之后真要逐元素计算时,CPU 就得跳着地址读,同样的运算反而变慢。同一个操作,既能“快得离谱”,也能“慢得可疑”,原因就在这里。reshape 能不能算视图,取决于新形状能否用一组新 stride 描述同一块内存:能,就是视图;不能(比如转置后强行拉平),就只能拷贝。PyTorch 的 .view() 在无法返回视图时会直接报错,.reshape() 则会静默地拷贝一份;前者提醒你,后者可能让你不知不觉中多付了拷贝开销。
| 视图 View | 拷贝 Copy | |
|---|---|---|
| 内存 | 复用原内存 | 新分配一块内存 |
| 耗时 | 基本与数据量无关 | 与元素个数成正比 |
| 修改影响 | 会改动原数据 | 互不影响 |
| 典型操作 | 切片、transpose、部分 reshape、expand | .copy()、.clone()、.contiguous()、布尔掩码索引 |
表格里最后一行值得留意:用布尔数组做索引(fancy indexing)必然返回拷贝,因为它选出的元素在内存里不再规则排列。
对实际工作的意义有三点。其一,切片是视图,改切片就等于改原数据,这是最常见的一类 bug;排查时改一个值,看原数组变不变即可。其二,视图不释放内存:从一个大数组里切出一小片存起来,整个底层 buffer 仍被它引用着,内存或显存就会莫名其妙地爆掉。其三,想彻底断开联系,就显式拷贝一份。
判断是否共享底层存储,NumPy 可以看 np.shares_memory,PyTorch 可以比对 .data_ptr(),具体行为以官方文档为准。经验法则是:只改变“看待方式”的操作是 O(1);一旦涉及元素顺序真正重排、类型转换或规则之外的索引,大概率要付出拷贝的代价。
