gpt4 book ai didi

java - 在java中快速解析数字字符串

转载 作者:塔克拉玛干 更新时间:2023-11-02 08:41:23 26 4
gpt4 key购买 nike

关于如何在 Java 中将包含 double 字的 ASCII 文件解析为 double 组,我发现了很多不同的建议。我目前使用的大致如下:

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 秒。用 C 编写的类似代码,使用 fscanf,需要 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);

最佳答案

(总结了一些我已经在评论中提到的东西:)

您应该小心使用手动基准测试。问题的答案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 ...

显然,扫描器强加了相当大的开销,当更直接地从流中读取时可以避免这种开销。

这可能不是最终答案,因为可能有更高效和/或更优雅的解决方案(我期待看到它们!),但也许它至少有帮助。


EDIT

一个小评论:一般来说,这些方法之间存在一定的概念差异。粗略地说,区别在于谁决定读取的元素数量。在伪代码中,这个区别是

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 大小,并且潜在的错误(例如输入中的数字太少或太多)不会'出现或以其他方式处理。

关于java - 在java中快速解析数字字符串,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33064276/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com