关于如何将包含双精度数字的ASCII文件解析为Java中的双精度数组,我发现了很多不同的建议。我目前使用的大致如下:
stream = FileInputStream(fname);
breader = BufferedReader(InputStreamReader(stream));
scanner = java.util.Scanner(breader);
array = new double[size]; // size is known upfront
idx = 0;
try {
while(idx<size){
array[idx] = scanner.nextDouble();
idx++;
}
}
catch {...}
对于包含100万个数字的示例文件,此代码大约需要2秒钟。使用fscanf
用C语言编写的类似代码需要0.1秒(!)显然我错了。我猜这次调用nextDouble()
这么多次是错误的,因为开销很大,但我找不到更好的方法。
我不是Java专家,因此我需要一点帮助:你能告诉我如何改进这段代码吗?
编辑相应的C代码如下
fd = fopen(fname, "r+");
vals = calloc(sizeof(double), size);
do{
nel = fscanf(fd, "%lf", vals+idx);
idx++;
} while(nel!=-1);
答案 0 :(得分:3)
(总结我在评论中已经提到的一些事情:)
您应该小心手动基准测试。问题How do I write a correct micro-benchmark in Java?的答案指出了一些基本的警告。然而,这种情况并不容易出现经典陷阱。事实上,情况恰恰相反:当基准完全包括读取文件时,您很可能不会对代码进行基准测试,而主要是硬盘。这涉及缓存的常见副作用。
然而,显然是超出纯文件IO的开销。
你应该知道Scanner
类是非常强大和方便的。但在内部,它是由大型正则表达式组成的野兽,并且隐藏了用户的巨大复杂性 - 当您的意图只是阅读double
值时,这种复杂性根本不是必需的!
有一些开销较少的解决方案。
不幸的是,最简单的解决方案仅适用于输入中的数字由行分隔符分隔的情况。然后,将此文件读入数组可以写为
double result[] =
Files.lines(Paths.get(fileName))
.mapToDouble(Double::parseDouble)
.toArray();
这甚至可能相当快。如果在一行中有多个数字(正如您在评论中提到的那样),则可以对此进行扩展:
double result[] =
Files.lines(Paths.get(fileName))
.flatMap(s -> Stream.of(s.split("\\s+")))
.mapToDouble(Double::parseDouble)
.toArray();
关于如何有效地从文件中读取一组double
值的一般问题,由空格分隔(但不一定用换行符分隔),我写了一个小测试。
这不应该被认为是一个真正的基准,并且需要花费一些时间,但它至少试图解决一些基本问题:它使用不同的方法读取不同大小的文件,多次,以便在后来的运行中,硬盘缓存的效果对于所有方法应该是相同的:
更新以生成样本数据,如评论中所述,并添加了基于流的方法
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StreamTokenizer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Random;
import java.util.Scanner;
import java.util.StringTokenizer;
import java.util.stream.Stream;
public class ReadingFileWithDoubles
{
private static final int MIN_SIZE = 256000;
private static final int MAX_SIZE = 2048000;
public static void main(String[] args) throws IOException
{
generateFiles();
long before = 0;
long after = 0;
double result[] = null;
for (int n=MIN_SIZE; n<=MAX_SIZE; n*=2)
{
String fileName = "doubles"+n+".txt";
for (int i=0; i<10; i++)
{
before = System.nanoTime();
result = readWithScanner(fileName, n);
after = System.nanoTime();
System.out.println(
"size = " + n +
", readWithScanner " +
(after - before) / 1e6 +
", result " + result);
before = System.nanoTime();
result = readWithStreamTokenizer(fileName, n);
after = System.nanoTime();
System.out.println(
"size = " + n +
", readWithStreamTokenizer " +
(after - before) / 1e6 +
", result " + result);
before = System.nanoTime();
result = readWithBufferAndStringTokenizer(fileName, n);
after = System.nanoTime();
System.out.println(
"size = " + n +
", readWithBufferAndStringTokenizer " +
(after - before) / 1e6 +
", result " + result);
before = System.nanoTime();
result = readWithStream(fileName, n);
after = System.nanoTime();
System.out.println(
"size = " + n +
", readWithStream " +
(after - before) / 1e6 +
", result " + result);
}
}
}
private static double[] readWithScanner(
String fileName, int size) throws IOException
{
try (
InputStream is = new FileInputStream(fileName);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
Scanner scanner = new Scanner(br))
{
// Do this to avoid surprises on systems with a different locale!
scanner.useLocale(Locale.ENGLISH);
int idx = 0;
double array[] = new double[size];
while (idx < size)
{
array[idx] = scanner.nextDouble();
idx++;
}
return array;
}
}
private static double[] readWithStreamTokenizer(
String fileName, int size) throws IOException
{
try (
InputStream is = new FileInputStream(fileName);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr))
{
StreamTokenizer st = new StreamTokenizer(br);
st.resetSyntax();
st.wordChars('0', '9');
st.wordChars('.', '.');
st.wordChars('-', '-');
st.wordChars('e', 'e');
st.wordChars('E', 'E');
double array[] = new double[size];
int index = 0;
boolean eof = false;
do
{
int token = st.nextToken();
switch (token)
{
case StreamTokenizer.TT_EOF:
eof = true;
break;
case StreamTokenizer.TT_WORD:
double d = Double.parseDouble(st.sval);
array[index++] = d;
break;
}
} while (!eof);
return array;
}
}
// This one is reading the whole file into memory, as a String,
// which may not be appropriate for large files
private static double[] readWithBufferAndStringTokenizer(
String fileName, int size) throws IOException
{
double array[] = new double[size];
try (
InputStream is = new FileInputStream(fileName);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr))
{
StringBuilder sb = new StringBuilder();
char buffer[] = new char[1024];
while (true)
{
int n = br.read(buffer);
if (n == -1)
{
break;
}
sb.append(buffer, 0, n);
}
int index = 0;
StringTokenizer st = new StringTokenizer(sb.toString());
while (st.hasMoreTokens())
{
array[index++] = Double.parseDouble(st.nextToken());
}
return array;
}
}
private static double[] readWithStream(
String fileName, int size) throws IOException
{
double result[] =
Files.lines(Paths.get(fileName))
.flatMap(s -> Stream.of(s.split("\\s+")))
.mapToDouble(Double::parseDouble)
.toArray();
return result;
}
private static void generateFiles() throws IOException
{
for (int n=MIN_SIZE; n<=MAX_SIZE; n*=2)
{
String fileName = "doubles"+n+".txt";
if (!new File(fileName).exists())
{
System.out.println("Creating "+fileName);
writeDoubles(new FileOutputStream(fileName), n);
}
else
{
System.out.println("File "+fileName+" already exists");
}
}
}
private static void writeDoubles(OutputStream os, int n) throws IOException
{
OutputStreamWriter writer = new OutputStreamWriter(os);
Random random = new Random(0);
int numbersPerLine = random.nextInt(4) + 1;
for (int i=0; i<n; i++)
{
writer.write(String.valueOf(random.nextDouble()));
numbersPerLine--;
if (numbersPerLine == 0)
{
writer.write("\n");
numbersPerLine = random.nextInt(4) + 1;
}
else
{
writer.write(" ");
}
}
writer.close();
}
}
它比较了4种方法:
Scanner
进行阅读,与原始代码段一样StreamTokenizer
String
,并使用StringTokenizer
Stream
行,然后将其平面映射到Stream
个令牌,然后映射到DoubleStream
将文件作为一个大的String
读取可能并不适合所有情况:当文件变得(更大)时,将整个文件作为String
保留在内存中可能不可行解。
测试运行(在相当旧的PC上,使用慢速硬盘驱动器(无固态))大致显示了这些结果:
...
size = 1024000, readWithScanner 9932.940919, result [D@1c7353a
size = 1024000, readWithStreamTokenizer 1187.051427, result [D@1a9515
size = 1024000, readWithBufferAndStringTokenizer 1172.235019, result [D@f49f1c
size = 1024000, readWithStream 2197.785473, result [D@1469ea2 ...
显然,扫描程序会产生相当大的开销,当从流中直接读取时可能会避免这种开销。
这可能不是最终答案,因为可能会有更高效和/或更优雅的解决方案(我期待看到它们!),但也许它至少有用。
修改
一句小话:一般来说,这些方法之间存在一定的概念差异。粗略地说,区别在于谁决定了读取的元素数量。在伪代码中,这种差异是
double array[] = new double[size];
for (int i=0; i<size; i++)
{
array[i] = readDoubleFromInput();
}
与
double array[] = new double[size];
int index = 0;
while (thereAreStillNumbersInTheInput())
{
double d = readDoubleFromInput();
array[index++] = d;
}
你使用扫描仪的原始方法就像第一个一样,而我提出的解决方案与第二个解决方案更相似。但是,假设size
确实是真正的大小,并且潜在的错误(例如输入中的数字太少或太多)不应该在这里产生很大差异。出现或以其他方式处理。