最快的Matlab文件读取方法?

38

我的MATLAB程序正在读取一个大约有700万行的文件,并且在I/O上浪费了太多时间。我知道每一行都是格式化为两个整数,但我不知道它们占用多少字符。str2num非常慢,我应该使用哪个MATLAB函数代替它?

注意:我必须逐行操作,而不能将整个文件存储在内存中,因此无法使用读取整个矩阵的命令。

fid = fopen('file.txt');
tline = fgetl(fid);
while ischar(tline)
    nums = str2num(tline);    
    %do stuff with nums
    tline = fgetl(fid);
end
fclose(fid);

3
你是如何确定瓶颈在于输入/输出的?我倾向于认为瓶颈更可能在于你对数字进行的操作。如果你能将数据分块进行处理,向量化该操作,你可能会看到更好的性能表现。 - gnovice
目前使用sscanf(tline, '%d %d', 2) ,速度已经快了很多,但仍然不够理想。 - user714403
1
@gnovice,因为当我仅仅读取文件时(即将%do stuff注释掉),它所需的时间几乎相同。 - user714403
如果你使用的是HDD,你可能需要考虑投资一下SSD。 - zamazalotta
3
尝试使用fscanf(fid, '%d %d', 100000)读取一大块内容,然后循环处理该块中的数字,而不是使用sscanf。并使用profile on -timer real确认你花费时间的位置。 - Andrew Janke
4个回答

63

问题描述

这是一个普遍的难题,没有什么比测试更能回答了。以下是我的假设:

  1. 一个格式良好的ASCII文件,包含两列数字。没有标题,没有不一致的行等。

  2. 该方法必须适用于读取文件,这些文件太大而无法保存在内存中(尽管我的耐心有限,因此我的测试文件只有500,000行)。

  3. 实际操作(OP所谓的“对数字进行处理”)必须逐行执行,不能矢量化。

讨论

考虑到这一点,答案和评论似乎鼓励在三个方面提高效率:

  • 以较大的批次读取文件
  • 更有效地执行字符串到数字的转换(通过批处理或使用更好的函数)
  • 使实际处理更加高效(我已经通过上述规则3排除了这种情况)。

结果

我编写了一个快速脚本来测试这些主题的摄入速度(和结果的一致性)。结果如下:

  • 初始代码。 68.23秒。582582个检查
  • 每行使用sscanf一次。 27.20秒。582582个检查
  • 使用fscanf进行大批量处理。 8.93秒。582582个检查
  • 使用textscan进行大批量处理。 8.79秒。582582个检查
  • 将大批量读取到内存中,然后使用sscanf。 8.15秒。582582个检查
  • 使用Java单行文件阅读器和单行sscanf。 63.56秒。582582个检查
  • 使用Java单项令牌扫描器。 81.19秒。582582个检查
  • 完全批处理操作(不符合规定)。 1.02秒。508680个检查(违反规则3)

总结

超过原始时间的一半(68-> 27秒)被str2num调用中的低效率消耗掉了,可以通过切换sscanf来消除这种情况。

剩余时间的另外2/3(27-> 8秒)可以通过对文件读取和字符串转换使用更大的批次来减少。

如果我们愿意违反原始帖子中的第三条规则,那么另外7/8的时间可以通过切换到完全数字处理来减少。然而,有些算法不适合这种情况,所以我们将其保留。(请注意,最后一个条目的“检查”值不匹配。)

最后,与我之前在此回复中的编辑直接矛盾的是,通过切换可用的缓存Java单行读取器并不能节省任何时间。事实上,该解决方案比使用本地读取器的相应单行结果要慢2-3倍(63秒对27秒)。

上述所有解决方案的示例代码如下。


示例代码

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Create a test file
cd(tempdir);
fName = 'demo_file.txt';
fid = fopen(fName,'w');
for ixLoop = 1:5
    d = randi(1e6, 1e5,2);
    fprintf(fid, '%d, %d \n',d);
end
fclose(fid);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Initial code
CHECK = 0;
tic;
fid = fopen('demo_file.txt');
tline = fgetl(fid);
while ischar(tline)
    nums = str2num(tline);
    CHECK = round((CHECK + mean(nums) ) /2);
    tline = fgetl(fid);
end
fclose(fid);
t = toc;
fprintf(1,'Initial code.  %3.2f sec.  %d check \n', t, CHECK);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Using sscanf, once per line
CHECK = 0;
tic;
fid = fopen('demo_file.txt');
tline = fgetl(fid);
while ischar(tline)
    nums = sscanf(tline,'%d, %d');
    CHECK = round((CHECK + mean(nums) ) /2);
    tline = fgetl(fid);
end
fclose(fid);
t = toc;
fprintf(1,'Using sscanf, once per line.  %3.2f sec.  %d check \n', t, CHECK);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Using fscanf in large batches
CHECK = 0;
tic;
bufferSize = 1e4;
fid = fopen('demo_file.txt');
scannedData = reshape(fscanf(fid, '%d, %d', bufferSize),2,[])' ;
while ~isempty(scannedData)
    for ix = 1:size(scannedData,1)
        nums = scannedData(ix,:);
        CHECK = round((CHECK + mean(nums) ) /2);
    end
    scannedData = reshape(fscanf(fid, '%d, %d', bufferSize),2,[])' ;
end
fclose(fid);
t = toc;
fprintf(1,'Using fscanf in large batches.  %3.2f sec.  %d check \n', t, CHECK);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Using textscan in large batches
CHECK = 0;
tic;
bufferSize = 1e4;
fid = fopen('demo_file.txt');
scannedData = textscan(fid, '%d, %d \n', bufferSize) ;
while ~isempty(scannedData{1})
    for ix = 1:size(scannedData{1},1)
        nums = [scannedData{1}(ix) scannedData{2}(ix)];
        CHECK = round((CHECK + mean(nums) ) /2);
    end
    scannedData = textscan(fid, '%d, %d \n', bufferSize) ;
end
fclose(fid);
t = toc;
fprintf(1,'Using textscan in large batches.  %3.2f sec.  %d check \n', t, CHECK);



%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Reading in large batches into memory, incrementing to end-of-line, sscanf
CHECK = 0;
tic;
fid = fopen('demo_file.txt');
bufferSize = 1e4;
eol = sprintf('\n');

dataBatch = fread(fid,bufferSize,'uint8=>char')';
dataIncrement = fread(fid,1,'uint8=>char');
while ~isempty(dataIncrement) && (dataIncrement(end) ~= eol) && ~feof(fid)
    dataIncrement(end+1) = fread(fid,1,'uint8=>char');  %This can be slightly optimized
end
data = [dataBatch dataIncrement];

while ~isempty(data)
    scannedData = reshape(sscanf(data,'%d, %d'),2,[])';
    for ix = 1:size(scannedData,1)
        nums = scannedData(ix,:);
        CHECK = round((CHECK + mean(nums) ) /2);
    end

    dataBatch = fread(fid,bufferSize,'uint8=>char')';
    dataIncrement = fread(fid,1,'uint8=>char');
    while ~isempty(dataIncrement) && (dataIncrement(end) ~= eol) && ~feof(fid)
        dataIncrement(end+1) = fread(fid,1,'uint8=>char');%This can be slightly optimized
    end
    data = [dataBatch dataIncrement];
end
fclose(fid);
t = toc;
fprintf(1,'Reading large batches into memory, then sscanf.  %3.2f sec.  %d check \n', t, CHECK);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Using Java single line readers + sscanf
CHECK = 0;
tic;
bufferSize = 1e4;
reader =  java.io.LineNumberReader(java.io.FileReader('demo_file.txt'),bufferSize );
tline = char(reader.readLine());
while ~isempty(tline)
    nums = sscanf(tline,'%d, %d');
    CHECK = round((CHECK + mean(nums) ) /2);
    tline = char(reader.readLine());
end
reader.close();
t = toc;
fprintf(1,'Using java single line file reader and sscanf on single lines.  %3.2f sec.  %d check \n', t, CHECK);

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Using Java scanner for file reading and string conversion
CHECK = 0;
tic;
jFile = java.io.File('demo_file.txt');
scanner = java.util.Scanner(jFile);
scanner.useDelimiter('[\s\,\n\r]+');
while scanner.hasNextInt()
    nums = [scanner.nextInt() scanner.nextInt()];
    CHECK = round((CHECK + mean(nums) ) /2);
end
scanner.close();
t = toc;
fprintf(1,'Using java single item token scanner.  %3.2f sec.  %d check \n', t, CHECK);


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Reading in large batches into memory, vectorized operations (non-compliant solution)
CHECK = 0;
tic;
fid = fopen('demo_file.txt');
bufferSize = 1e4;
eol = sprintf('\n');

dataBatch = fread(fid,bufferSize,'uint8=>char')';
dataIncrement = fread(fid,1,'uint8=>char');
while ~isempty(dataIncrement) && (dataIncrement(end) ~= eol) && ~feof(fid)
    dataIncrement(end+1) = fread(fid,1,'uint8=>char');  %This can be slightly optimized
end
data = [dataBatch dataIncrement];

while ~isempty(data)
    scannedData = reshape(sscanf(data,'%d, %d'),2,[])';
    CHECK = round((CHECK + mean(scannedData(:)) ) /2);

    dataBatch = fread(fid,bufferSize,'uint8=>char')';
    dataIncrement = fread(fid,1,'uint8=>char');
    while ~isempty(dataIncrement) && (dataIncrement(end) ~= eol) && ~feof(fid)
        dataIncrement(end+1) = fread(fid,1,'uint8=>char');%This can be slightly optimized
    end
    data = [dataBatch dataIncrement];
end
fclose(fid);
t = toc;
fprintf(1,'Fully batched operations.  %3.2f sec.  %d check \n', t, CHECK);

(译文)

关于Ben所提到的问题,如果您按行读取文件,则瓶颈始终是文件I/O。

我知道有时候您无法将整个文件都放入内存中。我通常会一次性读入大量字符(大约1e5、1e6等,具体取决于您系统的内存)。然后,我将进一步读取单个字符(或返回单个字符)以获取一个整数行数,然后运行字符串解析(例如 sscanf)。

接下来,您可以一次处理生成的大型矩阵的一行,在读取完整个文件之前重复这个过程。

它有点繁琐,但并不难。我通常看到比单行阅读器快90%以上的速度提升。



2
你测试过这个Java东西吗?Matlab的fopen I/O已经缓冲,就像C的stdio一样;切换到调用Java类只会增加开销。对我来说,它比OP原始的fgetl慢4倍。开销可能不是磁盘I/O本身,而是在循环中操作小数据块的操作开销。 - Andrew Janke
我测试了它的基本功能,但没有测试速度。你是对的,这个想法很糟糕。即将进行重大修改。 - Pursuit
@Max。对的。已修复。CHECK值仍然不匹配,这是有意的。但它至少应该表示实际上正在处理所有读取的数据。tic/toc时间没有改变足以引起摘要的更改(实际上略微更快,可能是由于计算机上的其他活动)。 - Pursuit
我正在尝试使用您的方法,但数据格式需要先知道 '%d, %d'。我想读取一个包含NxK值的文本文件,列由空格分隔,行由\n分隔。您能否在您的答案中添加一个“通用”的“FastFileRead”函数? - Pedro77
@Pursuit,Textscan测试只读取10,000行吗?fopen是必要的吗? - gciriani
显示剩余3条评论

4
我使用 memmapfile() 取得了良好的结果(速度方面)。这样可以将内存数据复制的数量最小化,并利用内核的IO缓冲。您需要足够的空闲地址空间(但不是实际的空闲内存)来映射整个文件,并且需要足够的自由内存来存储输出变量(很明显!)
以下示例代码将文本文件读入 int32 类型的双列矩阵 data 中。
fname = 'file.txt';
fstats = dir(fname);
% Map the file as one long character string
m = memmapfile(fname, 'Format', {'uint8' [ 1 fstats.bytes] 'asUint8'});
textdata = char(m.Data(1).asUint8);
% Use textscan() to parse the string and convert to an int32 matrix
data = textscan(textdata, '%d %d', 'CollectOutput', 1);
data = data{:};
% Tidy up!
clear('m')

您可能需要调整textscan()的参数以获得您想要的结果 - 请参阅在线文档。


我认为 memmapfile 仅在像这样顺序地读取整个文件时使用时并不占优势。您可以直接对文件进行相同的 textscan() 调用,并使用更少的内存获得相同的结果。Memmapfile 更适用于大型文件的分散(非顺序)访问。 - Andrew Janke
2
memmapfile的优点在于它避免了从内核地址空间到用户地址空间的文件数据的内存复制开销 - 内核只需在由文件组成的磁盘块支持下分配用户空间中的页面。然而,像往常一样,请勿猜测,进行基准测试! - Max
但是当你调用 char(m.Data(1).asUint8) 时,你最终不是也只是复制了一遍吗? - Andrew Janke
2
这是实际将数据“读入”内存的代码行,尽管当访问页面时,数据由虚拟内存系统故障转移。在缓冲IO中,内核从磁盘读取到内核空间中的缓冲区,然后将数据复制到用户空间。在内存映射时,数据直接进入用户空间。 - Max

3

即使您无法将整个文件放入内存中,也应使用矩阵读取函数读取大批量数据。

可能您甚至可以使用向量操作进行一些数据处理,这将进一步加快速度。


使用fscanf按块读取数据,这将是原始代码的近似替代品,并且比重复使用num2strsscanf调用要快得多。 - Andrew Janke

1
我发现MATLAB读取csv文件比文本文件快得多,因此如果可以使用其他软件将文本文件转换为csv文件,则可以显着加快Matlab的操作速度。

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