简介:一份面向C++开发者的TDMS文件读写实践资料,系统讲解了基于TDMS官方库的完整读写流程,包括DDC_CreateFile创建文件、DDC_AddChannelGroup/DDC_AddChannel添加通道组与通道、DDC_SetDataValues写入数据、DDC_SaveFile保存,以及DDC_OpenFile打开、获取通道组数/通道数/数据长度并用DDC_GetDataValuesDouble读取数据等关键API的调用方法,示例代码可直接运行参考。文档还分析了TDM与TDMS格式的本质区别:TDM带XML头,TDMS索引与数据均为二进制存储;同时基于实测对比两个格式在文件大小、写入耗时、读取耗时上的差异,指出通道数达到万级时写入耗时明显增加、数据长度与耗时近似线性等规律,并提醒中间文件所需空间可能远大于最终数据文件。资源为单个doc文档,包体仅20KB,内容精炼实用,已有995人浏览学习,适合初次接触TDMS的C++开发者快速掌握读写方法。
1. TDMS 文件读写:C++ 落盘链路与性能边界
在自动化测试和数据采集这类项目里,TDMS 文件几乎是一线工程师躲不开的落盘格式。它把通道组、通道和数据值组织成紧凑的二进制结构,C++ 里通过 DDC_* 系列接口就能读写,接口调用链路很短,半天就能上手。但我第一次用这套库做模拟项目 X 的波形落盘时,128 个通道、每通道 102400 个 double 点,写 TDMS 比写 TDM 多花了近一倍时间,中间文件占的磁盘空间也比最终文件大了两倍多,差点把工作盘写满。这篇文章从两种格式的结构差异讲起,把 C++ 读写 TDM/TDMS 的完整流程、性能边界、磁盘空间预估和五个高频坑一次说清,适合正在做采集平台、信号回放和离线分析的 C++ 工程师。
2. TDM 与 TDMS 的结构差异:写慢读不慢的根源在哪
2.1 文件布局对比:XML 头、段索引与数据区
先看一组现象:同样 128 个通道、每通道 102400 个 double 数据,TDM 和 TDMS 写出来的文件大小几乎一样,读取耗时也接近,但是 TDMS 的写入耗时接近 TDM 的两倍。这类反直觉的结果在第一次接触这套库时几乎都会遇到。要解释它,得从两种格式的文件组织方式说起。
TDM 文件由一个 XML 头加上二进制数据区组成。XML 头里保存通道名、单位、数据长度等元数据,可以直接用文本编辑器打开查看,排错时很直观。TDMS 则完全不同,所有信息都以二进制段的方式组织,每段有 lead in、元数据描述和原始数据,元数据本身也是二进制,人眼无法直接读取。
| 对比项 | TDM | TDMS |
|---|---|---|
| 头部/元数据 | XML 文本,可读 | 二进制段索引 |
| 单次写入耗时 | 低 | 约为 TDM 的 2 倍 |
| 单次读取耗时 | 快 | 与 TDM 接近,略多 |
| 多次读取耗时 | 首次后明显下降 | 首次后下降,仍略多于 TDM |
| 同等数据文件大小 | 约 100M 级别 | 与 TDM 几乎一致 |
表格里最能拉开差距的是写入耗时。很多人以为 TDMS 全二进制所以文件更小,实测同等通道数和数据长度下,两格式的最终文件大小几乎一样。TDM 的 XML 头在大数据量下占比可以忽略,体积差异没有想象中明显。但 XML 头也带来一个缺点:解析文本比解析二进制段慢,超大文件首次打开时耗时会偏大。TDMS 的元数据以二进制段组织,打开时只需要定位段表,大文件场景下首次解析体验反而更好。
选型时我一般看下游需求:如果后续工具要直接读通道信息做人工检查,TDM 更直观;如果数据要进测控软件做回放分析,TDMS 更省心。两者在文件体积上的差异基本不用纳入决策。
2.2 写入性能差距的产生:索引维护与段状态更新
为什么 TDMS 写数据比 TDM 多花将近一倍时间?根源在索引结构。TDM 写数据时,只需要维护 XML 头的偏移信息,原始数据按顺序追加进二进制区;TDMS 则要为每个写入的段维护独立元数据,包括段长度、数据类型、通道关系,每次追加数据都要更新段目录。段目录写入和索引偏移计算这两项额外开销,让写耗时几乎翻倍。
另外,TDMS 每追加一段数据,都要在文件的目录区写入新的段指针,段多了之后目录区本身也需要维护。而 TDM 的 XML 头只在文件末尾统一更新一次,写入路径更短。这个结构差异也解释了为什么通道数到达万级时 TDMS 写耗时上升更明显。
读取场景则相反。两者都有索引,首次读取要解析索引建立缓存,耗时相近;第二次以后再读,缓存直接给位置信息,明显变快。TDMS 因为段数量多、目录分散,多次读取时索引查找路径更长,所以稳定后的读耗时始终比 TDM 多一点,但差距很小。换句话说,选型时不应该因为 TDMS 是主流格式就无脑选,如果你的系统写操作非常频繁且间隔很短,TDM 反而更稳。
数据长度和写耗时的关系近似线性,这一点可以用来做全量耗时预估:先写一小段数据测单点耗时,再乘总点数,得到一个大致写时间。注意这个线性关系只适用于通道数基本固定的场景,当通道数涨到 10000 级别,通道创建和索引维护的开销会打破线性度,写耗时明显跳升。实测中这个数量级下应该考虑拆文件分段写,而不是在一个文件里堆出上万通道。
还有一点容易被忽略:DDC_SaveFile 之前,库会把数据缓存在内存缓冲里,如果此时程序异常退出,缓冲中的数据全部丢失,磁盘上只有上次 Save 的旧内容。对长时间采集来说,定期 Save 是必要折中:每积累 N 批数据 Save 一次,崩溃损失不会超过 N 批。
2.3 句柄设计与数据类型:接口层面对性能和使用习惯的影响
DDC_* 系列接口以句柄为操作对象,DDCFileHandle、DDCChannelGroupHandle、DDCChannelHandle 三级句柄贯穿读写全程。句柄是库内部对象的管理凭据,使用方不需要关心内存布局,但释放时机一旦出错,程序连崩溃提示都很难看懂。写入路径上,DDC_SetDataValues 接收 double* 和长度,库内部会做一次数据拷贝,调用返回后原始缓冲区可以立即复用或释放;读取路径上 DDC_GetDataValuesDouble 带起始偏移参数,这是实现分段读取的关键。
通道数据类型也要保持一致。库用 DDC_Double 这类常量标记通道类型,写入用 DDC_Double,读取就必须配套 DDC_GetDataValuesDouble。有人图省事把 float 数组强转成 double* 传给接口,结果是每个数据点被错位解释,读出来全是噪声。这类问题比接口报错更难排查,因为程序不崩溃,数据却完全不对。
通道组的划分同样影响使用体验。把相关性强的通道放进同一组,读取时可以按组一次性取回;所有通道堆在一个组里,后续按信号来源筛选要多绕一层。常见做法是按板卡或传感器类型分组建名,比如把加速度、温度、压力分开,读回来一看组名就知道数据来源。一套库里文件、组、通道三级句柄都维护好,写和读才算真正闭环。
3. 写 TDMS:从建文件到 SaveFile 的完整落盘链路
3.1 创建文件与通道组:结构决定读取时的组织方式
写文件的第一步是 DDC_CreateFile。这个函数在指定路径上创建 TDM 或 TDMS 文件,文件类型由第二个参数控制,传 "TDM" 生成带 XML 头的文件,传 "TDMS" 生成全二进制文件。后面几个空字符串参数对应作者、描述等元数据,通常可以留空,等数据写完再补。接着用 DDC_AddChannelGroup 添加通道组,组是逻辑容器,名字建议写清楚,因为读文件时就是通过组名和通道名来定位数据的。
常见的错误时序是 DDC_CreateFile 之后立刻 DDC_SaveFile,此时文件里没有任何通道组,等于生成一个空文件。之后再添加通道组、通道,再次 SaveFile 才能补上内容。所以第一次 SaveFile 之前,必须把组、通道、数据全部准备到位。
完整的写函数如下:
// 写 TDM/TDMS 格式文件 int CTDMSTestDlg::WriteDDCFile(const char* filePath, int channelNum, int dataLen, const char* fileType /* = "TDM" */) { DWORD begin = ::GetTickCount(); // 1. 创建文件:第二个参数控制格式,后续空串为元数据 DDCFileHandle testFile; DDC_CreateFile(filePath, fileType, "", "", "", "", &testFile); // 2. 增加一个通道组,所有通道挂在这个组下 DDCChannelGroupHandle group01; DDC_AddChannelGroup(testFile, "group01", "", &group01); // 3. 批量创建通道并填充数据 DDCChannelHandle* channels = new DDCChannelHandle[channelNum]; double* channelData = CreateRandomData(dataLen); for (int i = 0; i < channelNum; i++) { char channelIdx[20]; itoa(i, channelIdx, 10); DDC_AddChannel(group01, DDC_Double, channelIdx, "", "", &channels[i]); // 通道名 channelIdx 必须唯一,否则会覆盖之前添加的通道 DDC_SetDataValues(channels[i], channelData, dataLen); } // 4. 保存并关闭:SaveFile 把缓冲真正落盘 DDC_SaveFile(testFile); DWORD end = ::GetTickCount(); DDC_CloseFile(testFile); delete[] channels; delete[] channelData; return end - begin; }逻辑说明:DDC_CreateFile 完成后,文件句柄只是空壳,通道组、通道和数据全部加完后再 DDC_SaveFile,内容才会真正写盘。顺序上,通道组必须先创建,通道必须挂到已存在的组上,不能反过来。DDC_SetDataValues 调用后库会拷贝数据到内部缓冲,所以 channelData 在循环外只生成一份,所有通道共用,在通道数据一致的场景下很省内存。
参数说明:fileType 决定输出格式,传 "TDM" 或 "TDMS";DDC_Double 标记 double 精度通道,读取时要用 DDC_GetDataValuesDouble 配套;channelIdx 由 itoa 生成,对应 0、1、2 这种序号,实际项目中建议换成有物理含义的名字。
如果每个通道数据不同,就不能复用同一份 channelData,要在循环里分别生成并传入。注意 new 出来的 double 数组用完必须 delete[],否则长时间采集的程序一跑就是内存泄漏。
3.2 批量添加通道:命名唯一性是第一原则
批量添加通道最容易翻车的是 DDC_AddChannel 第三个参数。它在同一通道组内作为通道名,必须唯一。如果循环里传同一个字符串,第二次调用会被当成同名通道覆盖,最后文件里只剩一个通道,数据量对不上。代码里 itoa(i, channelIdx, 10) 已经保证了唯一性,但 0、1、2 这种名字后期排查不直观。
我一般会拼成固定长度字符串:
char channelIdx[32]; sprintf(channelIdx, "Ch_%03d", i); // 生成 Ch_001、Ch_002 ...这样通道名在软件里排序时不会出现 Ch_10 排在 Ch_2 前面的情况。只要把通道名改成这种格式,读文件时按名字筛选就能少写很多判断逻辑。
实际项目中,通道组往往不止一个。每块采集板卡建一个组,组内放该板卡的通道,读回时按组区分信号来源。多加一个组只是在 DDC_AddChannelGroup 到 DDC_AddChannel 之间多一次调用,代码结构不变:
DDCChannelGroupHandle groupAccel, groupTemp; DDC_AddChannelGroup(testFile, "Accel", "", &groupAccel); DDC_AddChannelGroup(testFile, "Temp", "", &groupTemp); DDC_AddChannel(groupAccel, DDC_Double, "Ch_001", "", "", &ch1); DDC_AddChannel(groupTemp, DDC_Double, "Ch_001", "", "", &ch2);注意两个不同组里可以出现同名通道,因为唯一性只在组内生效。这个机制用好了可以在多个采集板卡间保持一致的通道编号习惯。
3.3 SaveFile 的时机决定落盘耗时统计口径
很多人在统计写耗时的时候,只把创建通道和数据填充包进计时区间,漏掉 DDC_SaveFile。这个函数把累积在缓冲里的所有内容整理成最终文件,要重排索引、更新元数据,耗时不短。上面的 WriteDDCFile 把 GetTickCount 放在 DDC_CreateFile 之前,把结束点放在 DDC_CloseFile 之后,统计的是完整写链路。实测 TDMS 写耗时为 TDM 的近两倍,其中相当一部分就来自 SaveFile 的段索引整理。
另一个细节:DDC_SaveFile 之后通道句柄仍然有效,可以继续追加数据再 Save 一次,但每次 SaveFile 都会重建索引,频繁 Save 会让写耗时成倍上升。常见做法是全部数据填充完只 Save 一次,除非故障恢复需求明确要求分块落盘,否则不要在循环里每写一个通道就 Save。
写完后可以用文件 API 检查最终文件大小,判断数据是否完整落盘。如果文件大小与估算偏差过大,多半是通道名重复覆盖或 SaveFile 被漏掉。这个小习惯在批量跑的脚本里很有用。
4. 读 TDMS:三级遍历、句柄获取和数据分块
4.1 打开文件,进入通道组遍历
读取第一步是 DDC_OpenFile,第二个参数同样传 "TDM" 或 "TDMS",打开成功拿到 DDCFileHandle。然后用 DDC_GetNumChannelGroups 获取通道组数,得到 groupNum 后直接 new 一个句柄数组开始遍历,这里藏着一个容易踩空的步骤:数组分配好了,但元素还没有拿到对应组的句柄。我见过至少三次这种问题,表现是 DDC_GetNumChannels(groups[i], ...) 返回 0 或随机值。正确做法是先按索引把组句柄从文件中取出来,再进入循环调用。
DDC_OpenFile 打开文件时,如果路径不存在或格式不匹配,函数返回错误码,但句柄值可能没有变化。常见做法是先检查返回的 ddcFile 句柄是否有效再做后续操作,避免带着无效句柄一路走到 GetNumChannelGroups,得到无意义的组数。拿到 groupNum 后先判断是否大于 0,空文件场景下直接 return,比进入循环更干净。
这里给出补全后的读取代码:
int CTDMSTestDlg::ReadDDCFile(const char* filePath, const char* fileType /* = "TDM" */) { DWORD begin = ::GetTickCount(); DDCFileHandle ddcFile; DDC_OpenFile(filePath, fileType, &ddcFile); // 打开文件 unsigned int groupNum = 0; DDC_GetNumChannelGroups(ddcFile, &groupNum); // 获取通道组数量 if (groupNum == 0) { DDC_CloseFile(ddcFile); return 0; } DDCChannelGroupHandle* groups = new DDCChannelGroupHandle[groupNum]; for (unsigned int i = 0; i < groupNum; i++) { // 按索引取出第 i 个组句柄,函数名以库头文件为准 DDC_GetChannelGroupByIndex(ddcFile, i, &groups[i]); unsigned int channelNum = 0; DDC_GetNumChannels(groups[i], &channelNum); // 获取当前组通道数量 DDCChannelHandle* channels = new DDCChannelHandle[channelNum]; for (unsigned int j = 0; j < channelNum; j++) { // 按索引取出第 j 个通道句柄 DDC_GetChannelByIndex(groups[i], j, &channels[j]); unsigned __int64 dataLen = 0; DDC_GetNumDataValues(channels[j], &dataLen); // 获取通道数据长度 if (dataLen == 0) continue; double* data = new double[dataLen]; DDC_GetDataValuesDouble(channels[j], 0, dataLen, data); // 从偏移 0 拉全量数据 // 对 data 做真正的处理:统计、滤波、绘图,处理完释放 delete[] data; } delete[] channels; // 组内通道句柄释放 } delete[] groups; // 所有组遍历完再释放,不能放在循环里 DWORD end = ::GetTickCount(); DDC_CloseFile(ddcFile); return end - begin; }逻辑说明:读取顺序是文件→通道组→通道→数据,对应 OpenFile → GetNumChannelGroups → 取组句柄 → GetNumChannels → 取通道句柄 → GetNumDataValues → GetDataValuesDouble。每一级句柄都有生命周期,组句柄在文件关闭前有效,通道句柄在组存在期间有效,释放顺序和获取顺序相反,先释放通道数组,再释放组数组,最后关文件。
参数说明:DDC_GetDataValuesDouble 的第二个参数是起始偏移,第三个参数是读取长度,这里传 0 和 dataLen 表示从第一个点读到最后一个点。如果只需要某一段波形,改起始偏移即可,不需要把整个通道读进内存。
4.2 内存分配:data 长度、释放时机和分批读取
DDC_GetNumDataValues 返回的数据长度是 unsigned __int64,对应通道里 double 点个数。直接 new double[dataLen] 在大数据量时要注意内存:一个通道 1000 万点就是 80MB,100 个通道同时开数组会吃掉 8GB。常见做法是按通道读取、用完即释放,避免同时持有多个通道的 data 数组。
更稳的方案是分段读取。DDC_GetDataValuesDouble 的第二个参数是起始偏移,第三个参数是读取长度,可以固定每次读 4096 个点循环拉取:
const unsigned __int64 chunkSize = 4096; for (unsigned __int64 offset = 0; offset < dataLen; offset += chunkSize) { unsigned __int64 readLen = (dataLen - offset > chunkSize) ? chunkSize : (dataLen - offset); double* chunk = new double[readLen]; DDC_GetDataValuesDouble(channels[j], offset, readLen, chunk); // 处理 chunk delete[] chunk; }这个写法把单次内存开销控制在几十 KB 级别,适合嵌入式或内存受限环境。代价是多次调用库函数有额外开销,如果数据量不大,一次性读取反而更快。具体阈值可以在自己机器上测,我的经验是单通道点数小于 500 万时一次读更划算。
另外要注意,DDC_GetNumDataValues 返回 0 时,说明该通道没有任何数据。如果此时继续 new double[0] 并调用读取接口,不同库版本表现不一样,有的返回成功,有的访问越界。所以代码里先判断 dataLen == 0 就 continue,这个分支在空通道存在的文件里一定会被触发。
4.3 重复读取时的缓存效应
DDC_GetDataValuesDouble 第一次读取时,库要解析索引定位数据;第二次打开同一文件再读,索引已缓存在内存里,定位快很多。所以如果写循环不断重读同一文件,耗时从第一次到第二次明显下降,之后趋于稳定。这一点直接影响性能对比口径:比较 TDM 和 TDMS 的读取速度,不能用第一次读取的数据下结论,要多读几次取稳定值。实测稳定后,TDMS 的读耗时仍然比 TDM 多一点,但差距很小。
实际操作上,可以在循环里对同一文件反复读 5 次,取最后 3 次的平均值作为对比口径。第一次的耗时通常明显偏高,不计入统计。这个口径下的对比结果才符合线上多次访问同一文件的实际表现。
4.4 通道数据类型的匹配问题
库用 DDC_Double 等常量标记通道类型。写入用 DDC_Double,读取就必须用 DDC_GetDataValuesDouble;写入用 DDC_Float 却用 Double 接口读,数据长度和数值都会错位。调试时看到数据量大了一倍或明显乱码,先检查读写两端的类型常量是否一致。TDMS 元数据里其实存了通道类型,但库不会自动做类型转换,这个匹配动作必须由程序自己保证。
我一般会在读取代码里加一个通道类型字段的断言,把写入时记录的类型和读取时使用的接口类型放在一起校验,校验不过直接报错而不是带着错误数据继续跑。这样可以避免波形离线分析时对着一堆乱码找半天的尴尬。
5. 避坑与常见问题:空间预估、通道覆盖和崩溃三连
5.1 磁盘空间:最终文件 12G,中间文件要吃 32G
现象:写一个约 12GB 的 TDMS 文件,磁盘剩余 20GB,写到一半返回失败,检查磁盘发现可用空间被吃掉 28GB 以上,最终文件还没完成。
原因:第三方库写段时会在目标目录生成较大的中间文件,用于组装段索引和数据。中间文件体积可以达到最终文件的 2 倍以上,12GB 数据大概需要 32GB 临时空间。
解决:写之前先检查目标盘符剩余空间,按最终文件体积的 2.5~3 倍判断余量。最终文件大小的估算公式可以按通道数 × 数据长度 × 8 字节再乘 1.1 的元数据系数来算,偏差通常在 10% 以内。这个判断可以做进启动逻辑:
double freeGB = GetFreeSpaceGB("D:"); // 目标盘符剩余空间 double estFinalGB = estimateFinalSize(channelNum, dataLen); // 估算最终文件大小 if (freeGB < estFinalGB * 2.5) // 余量不足直接拦截 { // 提示磁盘空间不足,拒绝写入,避免中途失败 return -1; }其中 GetFreeSpaceGB 用 Windows 的 GetDiskFreeSpaceExA 实现,单位是 GB。注意检查的是目标文件所在盘符,不是程序所在盘符。这个拦截动作把中途写盘失败前置到用户交互阶段。小规模数据不需要担心,128 通道、每通道 102400 点,最终文件 100MB 左右,中间文件开销完全可控。
5.2 通道名不唯一,后写的数据覆盖先写的数据
现象:100 个通道写入完成后,读取时只看到 1 个通道,或者通道数量正确但数据内容完全一样。
原因:DDC_AddChannel 的第三个参数是通道名,同一通道组内必须唯一。循环里把通道名写死成同一个字符串,库认为你重复添加同一个通道,后一次 DDC_SetDataValues 覆盖前一次数据。
解决:循环里生成唯一通道名,推荐 sprintf 拼带序号的名字。调试期在循环里打日志打印通道名,确认每一轮进入 DDC_AddChannel 时的名字都不同,再放开全量写入。保存后可以用读接口读回组内通道数,与写入通道数比对,不一致就是通道名有重复。这步检查可以做成断言放在开发期测试用例里,每次构建自动跑。
5.3 delete[] groups 放在循环内,第二次进入直接崩
现象:读取多个通道组的文件时,程序在第一个组处理完成后崩溃,或内存检测工具报 double free。
原因:参考网络范例时,有人把 delete[] groups 写在 for 循环内部,循环执行一次就把整个组句柄数组释放并置空,第二次进入循环访问已释放内存,行为未定义。
解决:delete[] groups 必须放在所有组遍历完成后、DDC_CloseFile 之前。释放原则就一条:谁 new 的、在哪一层 new 的,就在那一层的结束位置释放。拿到代码先看 delete 的位置,不要照抄循环内的释放。用调试版运行时如果输出 detected memory leaks 相关提示,基本就是这类问题。
5.4 组句柄没填充就调用 GetNumChannels
现象:GetNumChannelGroups 返回组数为 8,但循环里 GetNumChannels 拿到的通道数都是 0,甚至访问 groups[i] 直接访问冲突。
原因:只 new 了 DDCChannelGroupHandle 数组,没有先把文件里第 i 个组的句柄取出来赋值,数组元素未初始化就把空句柄传给库函数。
解决:遍历开始时按索引取组句柄,4.1 中 DDC_GetChannelGroupByIndex 那一行不能省。如果库版本没有按索引取句柄的接口,就要在 OpenFile 后先拿到所有组句柄再遍历。判断方法也很直接:打印 groupNum 和每个组的 channelNum,如果组数正确但通道数全为 0,基本可以断定句柄未填充。
5.5 计时区间漏掉 SaveFile,性能结论完全失真
现象:用 GetTickCount 统计 TDMS 写耗时,数值只有几十毫秒,但肉眼观察文件生成明显卡顿。
原因:计时开始于循环填充数据之前,结束于循环之后,DDC_SaveFile 不在计时范围内。SaveFile 做索引整理和落盘,耗时占比很高,漏掉它得出的结论比实际快了 1 倍以上。
解决:计时开始点放在 DDC_CreateFile 之前,结束点放在 DDC_CloseFile 之后,让整个写链路都在计时区间内。用同样的口径对比 TDM 与 TDMS,才能得到可复现的基准数据。也可以配合文件修改时间验证:SaveFile 前记录一次文件修改时间,SaveFile 后再记录一次,两次时间不同说明 Save 生效。
6. 验证与进阶:基准测试和空间阈值怎么落到工程里
6.1 基准测试的标准口径
拿到库之后,先跑一组基准,用相同的通道数和数据长度分别生成 TDM 与 TDMS,记录文件大小、写耗时和读耗时。比如 128 通道 × 102400 点 × double 的规模:
| 指标 | TDM | TDMS |
|---|---|---|
| 最终文件大小 | 约 100M | 约 100M |
| 写耗时(含 SaveFile) | 基准 | 基准的 1.9~2.0 倍 |
| 首次读取耗时 | 基准 | 略高于基准 |
| 第二次读取耗时 | 明显下降 | 明显下降,仍高于 TDM |
这个对照做完,选型就清楚了:写多读少选 TDM,读多写少或要和测控软件生态对接选 TDMS。如果批量任务里两种格式都存在,基准数据还可以用来估算整个批次的处理时长。
6.2 数据规模与耗时的边界判断
实测中数据长度增加和写耗时基本线性,所以可以用小批量测试推全量耗时。通道数增加到 10000 级别时,写耗时明显偏离线性,说明瓶颈转向通道创建和索引维护。要提前知道自己机器上的拐点,就分别固定数据长度改变通道数、固定通道数改变数据长度,各跑一组曲线。从那以后,我每次写 TDMS 都强制走这三步:先估算最终文件大小并查磁盘剩余空间,要求剩余空间达到文件体积 2.5 倍;再跑 128 通道 × 102400 点的小批量基准,确认 TDMS 和 TDM 的耗时比在 2 倍附近;最后才放开全量数据写入。这套流程走下来,很少因为中途磁盘写满或句柄释放过早翻车。希望帮到你。
本文还有配套的精品资源,点击获取