(总结一下我在cmets中已经提到的一些东西:)
您应该小心手动基准测试。问题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 阅读,如原始代码 sn-p 中所示
- 阅读
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 确实是 real 大小,并且潜在的错误(如输入中的数字太少或太多)不会出现或不会出现以其他方式处理。