问题描述
这是一个普遍的难题,没有什么比测试更能回答了。以下是我的假设:
一个格式良好的ASCII文件,包含两列数字。没有标题,没有不一致的行等。
该方法必须适用于读取文件,这些文件太大而无法保存在内存中(尽管我的耐心有限,因此我的测试文件只有500,000行)。
实际操作(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秒)。
上述所有解决方案的示例代码如下。
示例代码
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);
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);
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);
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);
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);
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');
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');
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);
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);
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);
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');
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');
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%以上的速度提升。
fscanf(fid, '%d %d', 100000)
读取一大块内容,然后循环处理该块中的数字,而不是使用sscanf。并使用profile on -timer real
确认你花费时间的位置。 - Andrew Janke