文本文件解析器的Java字符串内存性能改进

时间:2015-07-23 15:22:11

标签: java string performance memory

我正在尝试在处理许多(数百万)字符串时优化我的内存使用量。我有一个约150万行的文件,我正在阅读,其中包含列中的十进制数字。例如,文件可能看起来像

16916576.643 4 -12312674.246 4        39.785 4  16916584.123 3  -5937726.325 3
    36.794 3
16399226.418 6  -4129008.232 6        43.280 6  16399225.374 4  -1891751.787 4
    39.885 4
12415561.671 9 -33057782.339 9        52.412 9  12415567.518 8 -25595925.487 8
    49.950 8
15523362.628 5 -12597312.619 5        40.579 5  15523369.553 5  -9739990.371 5
    42.003 5
12369614.129 8 -28797729.913 8        50.068 8         0.000           0.000  
     0.000  
....

目前,我正在使用String.split("\\s+")分隔这些数字,然后在Double.parseDouble()的每个部分调用String[],这看起来像是:

String[] data = line.split("\\s+");
double firstValue = Double.parseDouble(data[0]);
double secondValue = Double.parseDouble(data[1]);
double thirdValue = Double.parseDouble(data[2]);

这最终会创建很多String个对象。我也可能在行的开头或结尾有空格,所以在分割之前我必须在行上调用trim(),这也会创建另一个String对象。垃圾收集器处理这些String对象,这会导致速度减慢。 Java中是否有更多内存有效的结构来执行此操作?我在考虑使用char[]代替String,但我不确定是否会有相当大的改进。

4 个答案:

答案 0 :(得分:3)

如果您非常肯定这是一个严重的瓶颈,您可以随时将字符串直接解析为Double

// Keep track of my state.
private static class AsDoublesState {

    // null means no number waiting for add.
    Double d = null;
    // null means not seen '.' yet.
    Double div = null;
    // Is it negative.
    boolean neg = false;

    void consume(List<Double> doubles, char ch) {
        // Digit?
        if ('0' <= ch && ch <= '9') {
            double v = ch - '0';
            if (d == null) {
                d = v;
            } else {
                d = d * 10 + v;
            }
            // Count digits after the dot.
            if (div != null) {
                div *= 10;
            }
        } else if (ch == '.') {
            // Decimal point - start tracking how much to divide by.
            div = 1.0;
        } else if (ch == '-') {
            // Negate!
            neg = true;
        } else {
            // Everything else completes the number.
            if (d != null) {
                if (neg) {
                    d = -d;
                }
                if (div != null) {
                    doubles.add(d / div);
                } else {
                    doubles.add(d);
                }
                // Clear down.
                d = null;
                div = null;
                neg = false;
            }
        }
    }
}

private static List<Double> asDoubles(String s) {
    // Grow the list.
    List<Double> doubles = new ArrayList<>();
    // Track my state.
    AsDoublesState state = new AsDoublesState();

    for (int i = 0; i < s.length(); i++) {
        state.consume(doubles, s.charAt(i));
    }
    // Pretend a space on the end to flush an remaining number.
    state.consume(doubles, ' ');
    return doubles;
}

public void test() {
    String s = "16916576.643 4 -12312674.246 4        39.785 4  16916584.123 3  -5937726.325 3    36.794 3";
    List<Double> doubles = asDoubles(s);
    System.out.println(doubles);
}

如果给出错误的数据,这将会严重破坏。例如。 123--56...392.86将是一个完全有效的数字,6.0221413e+23将是两个数字。

使用State改进了AtomicDouble,以避免创建所有这些Double个对象。

// Keep track of my state.
private static class AsDoublesState {

    // Mutable double value.
    AtomicDouble d = new AtomicDouble();
    // Mutable double value.
    AtomicDouble div = new AtomicDouble();
    // Is there a number.
    boolean number = false;
    // Is it negative.
    boolean negative = false;

    void consume(List<Double> doubles, char ch) {
        // Digit?
        if ('0' <= ch && ch <= '9') {
            double v = ch - '0';
            d.set(d.get() * 10 + v);
            number = true;
            // Count digits after the dot.
            div.set(div.get() * 10);
        } else if (ch == '.') {
            // Decimal point - start tracking how much to divide by.
            div.set(1.0);
        } else if (ch == '-') {
            // Negate!
            negative = true;
        } else {
            // Everything else completes the number.
            if (number) {
                double v = d.get();
                if (negative) {
                    v = -v;
                }
                if (div.get() != 0) {
                    v = v / div.get();
                }
                doubles.add(v);
                // Clear down.
                d.set(0);
                div.set(0);
                number = false;
                negative = false;
            }
        }
    }
}

答案 1 :(得分:1)

尝试使用PatternMatcher将字符串与已编译的正则表达式分开:

double[][] out = new double[2][2];
String[] data = new String[2];
data[0] = "1 2";
data[1] = "3 2";

Pattern pat = Pattern.compile("\\s*(\\d+\\.?\\d*)?\\s+?(\\d+\\.?\\d*)?\\s*");
Matcher mat = pat.matcher(data[0]);
mat.find();

out[0][0] = Double.parseDouble(mat.group(1));
out[0][1] = Double.parseDouble(mat.group(2));

mat = pat.matcher(data[1]);
mat.find();
out[1][0] = Double.parseDouble(mat.group(1));
out[1][1] = Double.parseDouble(mat.group(2));

答案 2 :(得分:1)

我们的应用程序中存在类似的问题,其中很多字符串被创建,我们做了很少的事情来帮助我们解决问题。

  1. 如果可用的话,为Java提供更多内存,例如: -Xmx2G for 2gb。
  2. 如果您使用的是32位JVM,那么您最多只能分配4 GB - 理论上的限制。所以转到64位。

  3. 描述您的申请

  4. 您需要一步一步地完成:

    • 从visualvm开始(单击here获取详细信息)并测量创建的String,Double对象的数量,大小,时间等。

    • 使用Flyweight模式和实习生字符串对象之一。 Guava图书馆有Interner。甚至,你甚至可以做到两倍。这将避免重复并使用弱引用缓存对象,例如这里

      Interner<String> interner = Interners.newWeakInterner(); String a = interner.intern(getStringFromCsv()); String b = interner.intern(getStringFromCsv());

    here

    复制
    • 再次描述您的申请。

    您还可以使用扫描仪从文件中读取双倍内容,您的代码将更清晰,扫描程序也会在使用Double.ValueOf时缓存双倍值。

    这是代码

    File file = new File("double_file.txt");
            Scanner scan = new Scanner(file);
            while(scan.hasNextDouble()) {
                System.out.println( scan.nextDouble() );
            }
            scan.close();
    

    您可以使用此代码查看演奏中是否有任何GAIN。

答案 3 :(得分:1)

你根本不需要生产任何垃圾:

  • 使用BufferedInputStream以避免char[]String创建。没有非ASCII字符,因此您可以直接处理字节。
  • 编写类似于this solution的解析器,但避免任何垃圾。
  • 让您的班级提供类似double nextDouble()的方法,该方法会读取字符直到下一个双人组合。
  • 如果您需要按行进行处理,请关注\n(忽略\r,因为它只是\n的一个不必要的附录;使用过\r个{{1}}很久以前用作行分隔符)。