MATLAB中.mat文件的开销过大。

6
我正在解析一个大型的文本文件,其中充满了数据,然后将其保存为*.mat文件,以便我可以轻松地只加载其中的部分内容(有关读取文件的更多信息,请参见此处,有关数据,请参见此处)。为此,我一次读入一行,解析该行,然后将其附加到文件中。问题在于,文件本身比其中包含的数据大了3个数量级以上!
以下是我的代码简化版本:
database = which('01_hit12.par');
[directory,filename,~] = fileparts(database);
matObj = matfile(fullfile(directory,[filename '.mat']),'Writable',true);

fidr = fopen(database);
hitranTemp = fgetl(fidr);
k = 1;
while ischar(hitranTemp)
    if abs(hitranTemp(1)) == 32;
        hitranTemp(1) = '0';
    end

    hitran = textscan(hitranTemp,'%2u%1u%12f%10f%10f%5f%5f%10f%4f%8f%15c%15c%15c%15c%6u%2u%2u%2u%2u%2u%2u%1c%7f%7f','delimiter','','whitespace','');

    matObj.moleculeNumber(1,k)      = uint8(hitran{1});
    matObj.isotopeologueNumber(1,k) = uint8(hitran{2});
    matObj.vacuumWavenumber(1,k)    = hitran{3};
    matObj.lineIntensity(1,k)       = hitran{4};
    matObj.airWidth(1,k)            = single(hitran{6});
    matObj.selfWidth(1,k)           = single(hitran{7});
    matObj.lowStateE(1,k)           = single(hitran{8});
    matObj.tempDependWidth(1,k)     = single(hitran{9});
    matObj.pressureShift(1,k)       = single(hitran{10});

    if rem(k,1e4) == 0;
        display(sprintf('line %u (%2.2f)',k,100*k/K));
    end
    hitranTemp = fgetl(fidr);
    k = k + 1;
end
fclose(fidr);

由于代码解析时间过长且文件大小正在变得非常庞大,因此当解析了224,515行中的13,813行后我停止了代码。但是最后的输出表明我仅刚刚清除了10k行。我清除了内存,然后运行了以下命令:

S = whos('-file','01_hit12.mat');
fileBytes = sum([S.bytes]);

T = dir(which('01_hit12.mat'));
diskBytes = T.bytes;

disp([fileBytes diskBytes diskBytes/fileBytes])

并获得输出:

524894 896189009 1707.37141022759

为什么会多出895,664,115字节的空间?我知道帮助页面上说应该会有一些额外的开销,但是我觉得近1GB的描述性头部有点过分了!

新信息:
我尝试预先分配文件,认为也许MATLAB在执行循环时对于矩阵进行扩展时会做同样的事情,并在每次写入时重新分配整个矩阵的磁盘空间,但这不是原因。用适当数据类型的零填充文件会使我的简短检查脚本返回以下结果:

8531570 71467 0.00837677004349727

这对我来说更有意义。Matlab正在稀疏地保存文件,因此磁盘文件大小比内存中完整矩阵的大小小得多。然而,一旦开始用实际数据替换值,文件大小就会开始飙升超出所有合理的界限。

新的新信息:
在数据的子集上尝试了这个方法,长度为100行。要流式传输到磁盘,数据必须采用v7.3格式,因此我将子集通过我的脚本运行,加载到内存中,然后重新保存为v7.0格式。以下是结果:

v7.3: 3800 8752 2.30
v7.0: 3800 2561 0.67

难怪v7.3格式不是默认的。有人知道解决这个问题的方法吗?这是一个错误还是一个特性?


看起来对我来说肯定是个漏洞,但你可以通过分块写入来解决它。我尝试了10,000个块,文件大小还算合理。 - chappjc
我也发现v7.3的压缩,或者说缺乏压缩,非常糟糕。我记得之前向MathWorks抱怨过这个问题,他们回复说这会增加开销! - chappjc
你为什么要使用matfile来保存数据?为什么不直接使用save函数呢? - grantnz
1
我正在处理的这个测试文件可能全部放入内存,但这是针对完整的HITRAN 2012数据库的测试用例,该数据库是一个700 Mb的文本文件。将其加载到内存中,然后将其分配到不同的变量中,然后保存到磁盘中,这会使我的计算机崩溃。第一次尝试时,我不得不进行硬重置并丢失了许多其他工作。 - craigim
1
这就是诀窍。我没有足够的内存来避免它。我的主要目标是将数据文件转换为Matlab二进制文件,我可以在其中加载单个变量,比如线频率或分子,按照条件搜索,比如范围或同位素,然后只加载满足条件的数据库部分。以本地文本形式,我必须将整个文件加载到内存中(或进行逐行搜索,这需要很长时间)。如果我可以将其转换为与“matfile”命令兼容的合理大小的二进制文件,则可以节省时间和内存。 - craigim
显示剩余3条评论
1个回答

3
这对我来说似乎是一个bug。一种解决方法是向预分配的数组分块写入。 首先进行预分配:
fid = fopen('01_hit12.par', 'r');
data = fread(fid, inf, 'uint8');
nlines = nnz(data == 10) + 1;
fclose(fid);

matObj.moleculeNumber = zeros(1,nlines,'uint8');
matObj.isotopeologueNumber = zeros(1,nlines,'uint8');
matObj.vacuumWavenumber = zeros(1,nlines,'double');
matObj.lineIntensity = zeros(1,nlines,'double');
matObj.airWidth = zeros(1,nlines,'single');
matObj.selfWidth = zeros(1,nlines,'single');
matObj.lowStateE = zeros(1,nlines,'single');
matObj.tempDependWidth = zeros(1,nlines,'single');
matObj.pressureShift = zeros(1,nlines,'single');

接下来,为了分块每次写入10000个字节,我对您的代码进行了修改,如下所示:

... % your code plus pre-alloc first
bs = 10000;
while ischar(hitranTemp)
    if abs(hitranTemp(1)) == 32;
        hitranTemp(1) = '0';
    end

    for ii = 1:bs,
        hitran{ii} = textscan(hitranTemp,'%2u%1u%12f%10f%10f%5f%5f%10f%4f%8f%15c%15c%15c%15c%6u%2u%2u%2u%2u%2u%2    u%1c%7f%7f','delimiter','','whitespace','');
        hitranTemp = fgetl(fidr);
        if hitranTemp==-1, bs=ii; break; end
    end

    % this part really ugly, sorry! trying to keep it compact...
    matObj.moleculeNumber(1,k:k+bs-1)      = uint8(builtin('_paren',cellfun(@(c)c{1},hitran),1:bs));
    matObj.isotopeologueNumber(1,k:k+bs-1) = uint8(builtin('_paren',cellfun(@(c)c{2},hitran),1:bs));
    matObj.vacuumWavenumber(1,k:k+bs-1)    = builtin('_paren',cellfun(@(c)c{3},hitran),1:bs);
    matObj.lineIntensity(1,k:k+bs-1)       = builtin('_paren',cellfun(@(c)c{4},hitran),1:bs);
    matObj.airWidth(1,k:k+bs-1)            = single(builtin('_paren',cellfun(@(c)c{5},hitran),1:bs));
    matObj.selfWidth(1,k:k+bs-1)           = single(builtin('_paren',cellfun(@(c)c{6},hitran),1:bs));
    matObj.lowStateE(1,k:k+bs-1)           = single(builtin('_paren',cellfun(@(c)c{7},hitran),1:bs));
    matObj.tempDependWidth(1,k:k+bs-1)     = single(builtin('_paren',cellfun(@(c)c{8},hitran),1:bs));
    matObj.pressureShift(1,k:k+bs-1)       = single(builtin('_paren',cellfun(@(c)c{9},hitran),1:bs));

    k = k + bs;
    fprintf('.');
end
fclose(fidr);

最终的磁盘空间大小为21,393,408字节。使用情况如下:
>> S = whos('-file','01_hit12.mat');
>> fileBytes = sum([S.bytes]);
>> T = dir(which('01_hit12.mat'));
>> diskBytes = T.bytes; ratio = diskBytes/fileBytes;
>> fprintf('%10d whos\n%10d disk\n%10.6f\n',fileBytes,diskBytes,ratio)
   8531608 whos
  21389582 disk
  2.507099

仍然相当低效,但并非失控。

我已经运行了这个版本一整夜(不是说它需要那么长时间,但我在离开工作前运行了它)。早上我要么有一个20Mb的文件可以处理,要么就有一个30Gb的怪物会吞噬我的硬盘。当您关注主题并拥有数据文件时,如果您能看一下我上周发布的有关MATLAB在使用textscan时吞噬前导空格的链接问题(因此是if(abs(hitranTemp)==32);hitranTemp(1)='0';end块),我将不胜感激。当您解析数据时,您是否看到相同的情况? - craigim
应该已经完成了。 :) 这只花了我几分钟的时间,因为只有大约20个写入操作,而不是>200,000个。 - chappjc
我的电脑只有6个月大,但对我来说它仍然运行得相当缓慢。我开始让它运行,等我收拾好东西准备离开时,它仍在吃力地运转。 - craigim
搞定了!实际上,当它完成时,文件大小为8531570个whos,7263675个磁盘,比率为0.851388。 - craigim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接