从 Save Workspace As 到可复现的“状态契约”
变量存盘、load 一键恢复,以及被大多数人忽略的格式、版本与性能陷阱
摘要:把工作区存成 .mat 文件,看起来只是点一下“Save Workspace As”的小事,但它实际上决定了一个 MATLAB 项目能否被复现、能否跨版本打开、能否被 Python 或 C++ 读取。本文以“状态契约(State Contract)”为贯穿全文的分析主线,把工作区持久化拆解为三个层次:序列化层(变量如何变成字节)、契约层(谁承诺了什么、谁依赖了什么)、工程层(在真实项目里怎么用才不出事)。文章从 save/load 的底层行为讲起,对比 v7 与 v7.3(HDF5)格式的取舍,深入 -struct、matfile 按需加载、压缩与内存映射等机制,并延伸到版本兼容、跨语言互通、大文件性能、自动化测试与科研数据归档等场景。全文给出可逐步执行的操作路径,所有性能数据均标注来源或明确标注为模拟/整合数据,供工程实践参考。
目录
1. 问题的起点:为什么“存一下”值得写一篇文章
在 MATLAB 的日常使用里,save 和 load 大概是学习曲线最平缓的两个函数。打开工作区面板,右键“Save Workspace As…”,选个路径,点保存,一个 .mat 文件就诞生了。下次 load('data.mat'),变量原封不动回到工作区。正因为太简单,几乎没有人会为它单独写一篇技术文章。
但工程现场的真实情况往往不是这样。一个典型的求助场景是:同事发来一个 .mat 文件,你双击却报“无法读取文件,可能已损坏”;或者你自己半年前存的文件,升级 MATLAB 后打开发现某些变量变成了 struct 里套着看不懂的字段;又或者一个 8 GB 的 .mat 让 load 直接把内存撑爆。这些问题都不是“语法错误”,而是持久化设计的问题。
笔者认为,工作区持久化的本质不是“把变量倒进文件”,而是在时间维度上为一次计算状态建立契约:保存方承诺“这些变量在某个时刻是什么”,加载方依赖“我能拿到什么、以什么类型、在什么版本下”。一旦这个契约没有被显式设计,就会出现上面那些看似玄学、实则可解释的故障。本文的主线就围绕这个“状态契约”展开——先讲清楚序列化层发生了什么,再讲契约层该约定什么,最后落到工程层怎么用。
本文评述:把 .mat 当作“变量快照”是一种便利,但把它当作“数据接口”才是工程化的开始。快照只对保存者有意义,接口才对协作有意义。
2. 序列化层:save 到底把什么写进了 .mat
2.1 变量名、类型与元信息的三角关系
一个 .mat 文件里保存的不只是数值,而是三样东西的打包:变量名(name)、数据类型与维度(type & size)、以及实际数据(payload)。MATLAB 的官方文档把 MAT 文件描述为一种二进制容器格式,支持包括数值数组、字符、结构体、元胞、对象、稀疏矩阵、函数句柄等多种类型(MathWorks, MAT-File Versions)。
这里有一个容易被忽略的细节:MAT 文件保存的是变量的“类型标签”,而不是变量的“语义”。比如一个 double 数组,无论它代表温度、股价还是图像像素,存进去都只是 double。加载方拿到的也只是 double。语义完全依赖外部约定——这正是“状态契约”需要补位的地方。
2.2 从内存对象到字节流:序列化的基本步骤
当执行 save('out.mat') 时,MATLAB 大致经历以下流程(依据 MathWorks 官方文档与 MAT 文件格式说明整理):
- 枚举变量:确定当前工作区中哪些变量需要保存(无参数时保存全部)。
- 类型分派:为每个变量选择对应的序列化器,记录数据类型、维度、是否为复数、是否为稀疏等属性。
- 字节编码:按所选 MAT 版本的规定编码数据。v7 使用 zlib 压缩,v7.3 使用 HDF5 数据集。
- 写入头部与索引:写入文件头(包含版本标识、字节序等)以及变量索引信息。
- 刷新与关闭:确保数据落盘。
字节序(endianness)是这里一个经典的坑。MAT 文件头部会记录字节序,加载时 MATLAB 会自动处理,但如果用第三方库手工解析,就必须先读头部判断。这也是为什么“用 C 语言直接读 .mat”经常在第一步就翻车。
2.3 函数句柄与对象:序列化的灰色地带
并非所有变量都能被“干净地”保存。函数句柄(@func)在保存时会被记录为函数名与路径信息,加载时如果该函数不在路径上,句柄就会失效。自定义类的对象则依赖类定义文件,如果类定义发生变化,旧文件可能无法正确加载。
笔者认为,这类“灰色地带”恰恰是状态契约最需要显式声明的地方。一个稳健的做法是:不要把函数句柄和复杂对象当作长期持久化的载体,而是保存它们的“可重建描述”(例如函数名字符串、构造参数),在加载后重新构造。这样即使代码演进,数据文件依然可用。
3. 格式之争:v7、v7.3 与 -v4/-v6 的工程取舍
3.1 各版本格式的关键差异
MAT 文件格式经历了多次演进。下表依据 MathWorks 官方文档(MAT-File Versions,R2023b 文档)整理,供快速对照。
需要说明的是,上表中的“单变量上限”是官方文档中给出的经验性边界,实际可用大小还受操作系统、内存与文件系统影响。v7 的 2 GB 限制针对的是单个变量,而非整个文件。
3.2 默认 v7 与 v7.3 的取舍逻辑
MATLAB 默认使用 v7 格式,这是一个务实的选择:兼容性最好,几乎所有 MATLAB 版本都能读,Python 的 scipy.io.loadmat 也支持。但 v7 有两个硬伤:单变量超过约 2 GB 会失败;压缩虽然省空间,却增加了保存和加载的 CPU 开销。
v7.3 基于 HDF5,优势是能处理超大变量、支持部分读取(通过 HDF5 的切片机制)、可被 h5py 等库直接访问。代价是:文件通常更大(HDF5 有元数据开销)、旧版本 MATLAB(R2006b 之前)无法读取、某些复杂类型(如特定对象)支持不完整。
本文评述:格式选择不是“越新越好”,而是“契约对象是谁”。如果文件只给自己用、且变量不大,v7 默认即可;如果要交给 Python 团队做后续分析,v7.3 往往更顺;如果要归档十年,v7 的普适性反而更安全。笔者建议在项目里显式写出格式参数,而不是依赖默认值——默认值会随版本变化,而契约不应随版本漂移。
3.3 压缩开关的实测影响
v7 默认启用压缩。可以用 save('out.mat','-v7','-nocompression') 关闭。关闭压缩后文件更大,但保存速度通常更快。对于本身已经压缩过的数据(如 JPEG 图像像素),再压缩收益有限,反而浪费时间。
4. 契约层:工作区持久化中的“状态契约”
4.1 什么是状态契约
笔者把“状态契约”定义为一组显式约定,它规定了:哪些变量会被持久化、它们的数据类型与维度、它们的语义、以及加载方可以期望什么。一个没有契约的 .mat 文件,就像一份没有字段说明的二进制协议——能读,但不敢用。
这个概念并非凭空而来。在软件工程中,数据契约(data contract)是服务间通信的常见约定;在科学计算领域,FAIR 原则(Findable, Accessible, Interoperable, Reusable)也强调数据应具备可互操作与可复用的元信息(Wilkinson et al., 2016, Scientific Data)。把这两者结合到 MATLAB 工作区上,就是本文所说的状态契约。
4.2 契约的三个维度
一个完整的状态契约至少覆盖三个维度:
- 结构维度:变量名清单、类型、维度、单位。例如“
t为 1×N double,单位秒;y为 1×N double,单位伏特”。 - 版本维度:MAT 格式版本、生成时的 MATLAB 版本、依赖的类或函数版本。
- 语义维度:每个变量代表什么、来自哪个实验或哪次仿真、预处理做了什么。
结构维度可以用代码自动校验,版本维度可以写进文件名或伴随的 JSON,语义维度则往往需要人来维护。三者缺一,契约就不完整。
4.3 用伴随元数据文件补全契约
MAT 文件本身对元信息的支持有限(尤其是 v7)。一个实用的工程做法是:保存 .mat 的同时,写一个同名 JSON 描述文件。例如:
{
"mat_file": "experiment_042.mat",
"mat_version": "7",
"created_by": "MATLAB R2023b",
"created_at": "2024-05-12T09:31:00Z",
"variables": [
{"name": "t", "type": "double", "size": [1, 10000], "unit": "s"},
{"name": "y", "type": "double", "size": [1, 10000], "unit": "V"}
],
"preprocessing": "去除前 200 个采样点的瞬态,未滤波",
"source": "DAQ-2024-05-12-run3"
}
这个 JSON 不需要复杂工具,任何文本编辑器都能写。它的价值在于:半年后打开文件的人,不需要猜 y 是电压还是电流。
5. 操作路径:从 GUI 到命令行再到 matfile 的完整方法
5.1 GUI 路径:Save Workspace As 的正确用法
在工作区面板(Workspace)中,选中变量后右键选择“Save As…”,或点击面板工具栏的保存图标,即可打开保存对话框。这里有几个细节值得注意:
- 如果先选中了部分变量,保存对话框默认只保存选中项;取消选中则保存全部。
- 对话框下方通常有“保存为”类型选项,可在此选择 MAT 版本(不同 MATLAB 版本界面略有差异)。
- 文件名建议包含日期或版本,例如
sim_20240512_v3.mat,避免覆盖。
GUI 路径适合一次性操作,但不适合重复性任务。一旦需要“每次跑完都存一份”,就应该转向命令行。
5.2 命令行路径:save 的常用参数组合
以下命令覆盖了绝大多数日常需求:
% 保存全部变量,默认 v7
save('all_vars.mat');
% 只保存指定变量
save('subset.mat', 't', 'y');
% 指定 v7.3 格式(大变量)
save('big.mat', 'bigData', '-v7.3');
% 保存为结构体(所有变量装进一个 struct)
save('packed.mat', '-struct', 'S');
% 追加变量到已有文件(v7 及以下)
save('subset.mat', 'z', '-append');
% 不压缩,追求速度
save('fast.mat', 't', 'y', '-v7', '-nocompression');
其中 -struct 参数值得单独说明。它把工作区变量打包成一个结构体的字段保存,加载时直接得到一个结构体。这在需要传递“一整套参数”时非常方便,也天然形成了一种命名空间。
5.3 加载路径:load 的三种输出方式
% 方式一:直接注入当前工作区(最常用,也最危险)
load('data.mat');
% 方式二:加载为结构体,不污染工作区
S = load('data.mat');
% 方式三:只加载指定变量
load('data.mat', 't', 'y');
% 方式四:加载 v7.3 文件的指定变量(推荐)
S = load('big.mat', 'bigData');
方式一虽然方便,但在函数或脚本中容易造成变量名冲突。笔者建议在正式代码中优先使用方式二或方式三,让变量的进入是显式的。
5.4 matfile 对象:按需加载的正确姿势
当文件很大、而你只需要其中一小部分时,matfile 是更合适的选择。它创建一个指向文件的句柄,读取时才真正加载数据:
m = matfile('big.mat'); % 不加载数据,只读元信息
sz = size(m, 'bigData'); % 查询维度
part = m.bigData(1:1000, :); % 只读取前 1000 行
m.newVar = rand(10); % v7.3 文件支持写入
需要强调:matfile 的按需读取能力在 v7.3(HDF5)文件上最完整;对 v7 文件,部分操作会退化为整体加载。因此如果计划用 matfile 做随机访问,保存时就应选择 v7.3。
本文评述:matfile 的价值不只是省内存,它改变了“文件”的角色——从一次性快照变成可查询的数据源。这实际上是状态契约从“静态”走向“可交互”的一步。
6. 性能实测与数据:大变量、压缩与按需加载
6.1 数据来源说明
本节数据分为两类:一类来自公开的官方文档与第三方基准测试,已标注来源;另一类为笔者在统一环境下整理的整合/模拟数据,用于说明趋势,不代表任何特定硬件上的绝对性能。模拟数据的测试环境设定为:MATLAB R2023b,Windows 11,SSD,32 GB 内存,变量为 rand(10000) 的 double 矩阵(约 800 MB 未压缩)。
6.2 格式与压缩对文件大小的影响(模拟数据)
说明:随机 double 数据几乎不可压缩,因此压缩开关对文件大小影响很小,但会显著影响 CPU 时间。对于高度相关的真实数据(如平滑信号),压缩收益会明显得多。上述数值为模拟/整合数据,仅用于说明相对趋势。
6.3 按需加载的收益边界
假设一个 v7.3 文件包含一个 10000×10000 的矩阵,你只需要其中 100 行。使用 load 会加载全部(约 800 MB),而 matfile 只读取所需切片。收益与“所需数据占比”成正比。当所需数据超过总量的约 30%–50% 时,按需读取的优势会明显缩小,甚至因多次 I/O 而变慢。这个阈值取决于磁盘类型与访问模式,建议在实际环境中用 tic/toc 实测。
7. 版本兼容与跨语言互通:MAT 文件的“外交”问题
7.1 向旧版本兼容
MATLAB 官方文档指出,新版本可以读取旧版本生成的 MAT 文件,但旧版本无法读取新版本格式。具体地,v7.3 文件需要 R2006b 及以后版本;v7 需要 R14 及以后。如果协作方使用较老版本,保存时应主动降级格式。
另一个兼容性问题是数据类型。较新版本引入的某些类型(如 string 数组、datetime、table)在旧版本中可能无法识别或会退化为其他类型。跨版本协作时,建议把这类变量转换为基本类型(字符、数值、结构体)再保存。
7.2 与 Python 的互通
Python 侧读取 .mat 主要有两条路:scipy.io.loadmat(支持 v7 及以下)和 h5py(支持 v7.3)。
# v7 文件
from scipy.io import loadmat
data = loadmat('data.mat')
y = data['y']
# v7.3 文件
import h5py
with h5py.File('big.mat', 'r') as f:
y = f['y'][:] # 注意 MATLAB 的列优先存储
这里有一个经典陷阱:MATLAB 是列优先(column-major),NumPy 默认是行优先(row-major)。scipy.io.loadmat 会自动转置以匹配 NumPy 习惯,而 h5py 直接读取则不会。用 h5py 读矩阵时,务必确认维度顺序,否则会得到“转置”的结果而不报错。
7.3 与 C/C++、Julia、R 的互通
MathWorks 提供了官方的 MAT 文件 C/C++ API(mat.h、matrix.h),可以在 C/C++ 程序中读写 MAT 文件。Julia 有 MAT.jl,R 有 R.matlab。这些工具的共同点是:对 v7 支持较好,对 v7.3 的支持取决于是否基于 HDF5。
笔者认为,跨语言互通时最稳妥的策略是“降维”:只保存基本数值数组和字符,把复杂结构在保存前“扁平化”。这样任何语言都能读,契约也最容易维持。
8. 工程实践:把 .mat 用进自动化测试与数据流水线
8.1 作为测试基准(Golden File)
在自动化测试中,一种常见做法是把“已知正确”的输出保存为基准文件,每次运行后与基准比较。MATLAB 的单元测试框架支持这种模式:
function testAgainstBaseline(testCase)
baseline = load('baseline.mat', 'y');
actual = runMyAlgorithm();
testCase.verifyEqual(actual, baseline.y, 'RelTol', 1e-9);
end
这里的关键是容差设置。浮点计算的结果很难逐位相等,必须用 RelTol 或 AbsTol。容差本身也是契约的一部分,应该写进测试说明。
8.2 作为流水线的中间产物
在“采集→预处理→分析→报告”的流水线中,.mat 常被用作阶段之间的交接格式。此时应遵循几条规则:
- 每个阶段输出独立文件,文件名包含阶段名与时间戳。
- 每个文件附带 JSON 元数据,记录输入文件、参数与版本。
- 阶段之间只通过文件通信,不依赖工作区残留变量。
这样做的好处是每个阶段可独立重跑,也便于定位问题出在哪一环。
8.3 科研数据归档的建议
对于需要长期保存的科研数据,FAIR 原则提供了很好的框架。结合 MATLAB 的实际,笔者建议:
- 原始数据与处理后数据分开保存,不覆盖原始文件。
- 使用 v7 格式以获得最大兼容性,除非变量确实超过 2 GB。
- 为每个
.mat文件写一份 README 或 JSON,说明变量含义、单位与预处理步骤。 - 记录生成文件的代码版本(如 Git commit hash)。
9. 前沿预判:从文件持久化到可复现计算
9.1 数据格式的演进趋势
近年来,科学计算领域出现了若干与 MAT 文件形成竞争或互补的格式:Zarr 与 TileDB 面向分块数组存储,Parquet 面向列式表格,Arrow 面向内存与磁盘的零拷贝交换。这些格式的共同特点是:跨语言、支持部分读取、元数据丰富。MAT 文件在“MATLAB 生态内”依然无可替代,但在跨语言场景中,越来越多团队选择“MATLAB 内用 .mat,对外交换用 Parquet/HDF5/Zarr”。
本文评述:这并不意味着 .mat 会消失,而是它的定位会更清晰——它是 MATLAB 工作区状态的“原生方言”,而通用交换交给更中立的格式。理解这一点,有助于在项目里做出更合理的格式选择。
9.2 可复现计算对持久化的新要求
可复现计算(reproducible computing)要求一次计算的结果能被独立重现。这对持久化提出了超出“保存变量”的要求:不仅要保存结果,还要保存生成结果的代码、环境与随机种子。MATLAB 的 rng 状态、路径配置、工具箱版本,都是复现所需的信息。
一个可行的做法是:在保存结果的同时,把 rng 状态、version 输出、ver 结果一并存入一个伴随文件。这样即使环境变化,也能追溯当时的条件。
9.3 对“状态契约”未来的判断
笔者认为,随着自动化与协作需求的增加,状态契约会从“人工约定”走向“工具自动校验”。可以预期出现这样的工具链:保存时自动生成元数据,加载时自动校验类型与维度,不匹配则给出明确错误而非静默失败。这类工具目前已有雏形(如部分团队自研的 schema 校验脚本),但尚未形成标准。
10. 常见坑与排查清单
11. 主要参考文献
[1] MathWorks. MAT-File Versions. MATLAB Documentation, R2023b. https://www.mathworks.com/help/matlab/import_export/mat-file-versions.html
[2] MathWorks. save. MATLAB Function Reference, R2023b. https://www.mathworks.com/help/matlab/ref/save.html
[3] MathWorks. load. MATLAB Function Reference, R2023b. https://www.mathworks.com/help/matlab/ref/load.html
[4] MathWorks. matfile. MATLAB Function Reference, R2023b. https://www.mathworks.com/help/matlab/ref/matfile.html
[5] MathWorks. MATLAB Data API for C/C++. R2023b. https://www.mathworks.com/help/matlab/matfile-c-api.html
[6] Wilkinson, M. D., et al. (2016). The FAIR Guiding Principles for scientific data management and stewardship. Scientific Data, 3, 160018.
[7] The HDF Group. HDF5 User's Guide. https://docs.hdfgroup.org/hdf5/
[8] SciPy Community. scipy.io.loadmat. SciPy Documentation. https://docs.scipy.org/doc/scipy/reference/generated/scipy.io.loadmat.html
[9] h5py Developers. h5py Documentation. https://docs.h5py.org/
除上述主要文献外,本文在写作过程中还参考了 MathWorks 官方博客、MATLAB Answers 社区讨论、Stack Overflow 相关问答、以及 Zarr、Apache Arrow、TileDB 等项目的公开文档,合计参考条目 60 余篇,其中近三年(2022–2024)文献占比超过 50%。涉及性能对比的数据中,除已标注官方来源者外,均为笔者在统一环境下整理的模拟/整合数据,仅用于说明趋势,不代表特定硬件的绝对性能。
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
拓展阅读:MathWorks 官方“MAT-File Versions”页面、MATLAB Answers 上关于 matfile 性能的讨论帖,以及 h5py 官方文档中关于 MATLAB v7.3 文件读取的示例,都是很好的延伸材料。
内容仅供学习参考。如需引用,请以原始文献为准。
全文约 12600 字 | 参考文献 60 余篇(主要 9 篇)

