java中数字字符串的快速解析
对于如何在Java中将包含双精度数字的ASCII文件解析为双精度数字数组,我发现了很多不同的建议。我目前使用的大致如下:java中数字字符串的快速解析,java,arrays,performance,parsing,Java,Arrays,Performance,Parsing,对于如何在Java中将包含双精度数字的ASCII文件解析为双精度数字数组,我发现了很多不同的建议。我目前使用的大致如下: stream = FileInputStream(fname); breader = BufferedReader(InputStreamReader(stream)); scanner = java.util.Scanner(breader); array = new double[size]; // size is known upfront idx = 0; t
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 {...}
(总结我在评论中已经提到的一些事情:)
您应该小心使用手动基准测试。这个问题的答案指出了一些基本的警告。然而,这种情况并不容易出现经典陷阱。事实上,情况可能恰恰相反:当基准测试仅包含读取文件时,您很可能不是在对代码进行基准测试,而主要是对硬盘进行基准测试。这涉及到缓存的常见副作用
然而,显然,除了纯文件IO之外,还有一个开销
您应该知道,Scanner
类功能强大且非常方便。但在内部,它是一个由大型正则表达式组成的庞然大物,并且对用户隐藏了巨大的复杂性——当您打算只读取双值时,这种复杂性根本不需要
有一些开销较小的解决方案
不幸的是,最简单的解决方案仅适用于输入中的数字由行分隔符分隔的情况。然后,将该文件读入数组可以写成
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
值的一般问题,我编写了一个小测试
这不应被视为一个真正的基准,也不应对此持保留态度,但它至少试图解决一些基本问题:它使用不同的方法多次读取不同大小的文件,因此在以后的运行中,硬盘缓存对所有方法的效果都应相同:
更新以生成注释中所述的样本数据,并添加了基于流的方法
显然,扫描器施加了相当大的开销,这在更直接地从流中读取时可以避免
这可能不是最终的答案,因为可能会有更高效和/或更优雅的解决方案(我期待着看到它们!),但可能至少会有所帮助
编辑
一点小小的评论:一般来说,这两种方法在概念上存在一定的差异。粗略地说,区别在于谁决定读取的元素数量。在伪代码中,这种差异是
double array[] = new double[size];
for (int i=0; i<size; i++)
{
array[i] = readDoubleFromInput();
}
您最初使用扫描仪的方法与第一种类似,而我提出的解决方案与第二种更为相似。但是,假设大小确实是实际大小,并且潜在的错误(例如输入中的数字太少或太多)不会出现或以其他方式处理,那么这不会造成很大的差异。您能告诉我们“类似的代码是用C编写的”吗?您是否分析了代码以检查哪些部分是性能瓶颈?@DraganBozanovic不,我没有。我没有太多的Java经验,我使用emacs来编程。这是一段非常小的代码,我想瓶颈应该很容易找到。。它确实位于while循环中:),我不怀疑它来自数组[idx]
和内存分配。在您调用的库中有很多代码。尽管如此,我还是建议您尝试使用分析器。另外,请记住Java具有JIT,这可能会在优化代码时在开始时消耗一些时间。例如,您是否可以尝试将同一段代码执行100次,并检查最后几次迭代的执行时间?10次迭代需要10倍以上的时间。时间都花在循环中了。非常感谢你,Marco13:)这看起来真的很好!我将使用它,看看它如何在我的数据上工作,但最后一个解决方案似乎给出了“C”结果。我在想,你能不能修改它,以便在有限大小的缓冲区上使用StringTokenizer,并将其放入循环中?通过这种方式,您可以在更大的缓冲区上工作,并且仍然使用StringTokenizer。无论如何,我确信我做错了,再次感谢你的帮助:-)@angainor肯定有混合解决方案,比如在“块”(而不是一个大的、单一的缓冲区)中读取文件,然后处理这些块。更详细的分析可能是值得的,但它们可能会变得更复杂-例如,必须确保数据块不会分割数字,如0.123 0.234 0.3
(cut)45 0.456
-这当然可以处理,但可能有点麻烦。顺便说一句:我会在几分钟后添加一个小的(更一般的)评论作为编辑…@Marco13:只是好奇而已。我们可以直接将流标记为双数,而不是来回将字符串解析为双数吗?@dragon66理论上,我们可以读取每个字符,并像在FloatingDecimal.readJavaFormatString
中一样处理它,使用字符串的字符。然而,一个字符一个字符地读取流通常不是一个好主意:通常应该进行一些缓冲(即,至少应该将一定数量的字符读入char[]
),这将非常接近字符串。很难预测任何性能优势,但我的直觉是,努力与性能增益的比率将相当糟糕…@Marco13:我的意思是,在您的代码中,您使用case StreamTokenizer.TT_WORD作为字符串获取下一个令牌。为什么不使用case StreamTokenizer.TT_编号?
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();
}
}
...
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;
}