【问题标题】:Java match/exceed performance of readlineJava匹配/超过readline的性能
【发布时间】:2012-11-19 16:33:01
【问题描述】:

对于我的应用程序,我必须编写一个自定义的“readline”方法,因为我想检测并保留 ASCII 文本文件中的换行符结尾。 Java readLine() 方法不知道遇到了哪个换行序列(\r\n\r\n)或EOF,所以在写入修改后的文件时我无法输入完全相同的换行序列。

这是我的测试示例的 SSCE。

public class TestLineIO {
    public static java.util.ArrayList<String> readLineArrayFromFile1(java.io.File file) {
        java.util.ArrayList<String> lineArray = new java.util.ArrayList<String>();
        try {
            java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(file));
            String strLine;
            while ((strLine = br.readLine()) != null) {
                lineArray.add(strLine);
            }
            br.close();
        } catch (java.io.IOException e) {
            System.err.println("Could not read file");
            System.err.println(e);
        }
        lineArray.trimToSize();
        return lineArray;
    }


    public static boolean writeLineArrayToFile1(java.util.ArrayList<String> lineArray, java.io.File file) {
        try {
            java.io.BufferedWriter out = new java.io.BufferedWriter(new java.io.FileWriter(file));
            int size = lineArray.size();
            for (int i = 0; i < size; i++) {
                out.write(lineArray.get(i));
                out.newLine();
            }
            out.close();
        } catch (java.io.IOException e) {
            System.err.println("Could not write file");
            System.err.println(e);
            return false;
        }
        return true;
    }


    public static java.util.ArrayList<String> readLineArrayFromFile2(java.io.File file) {
        java.util.ArrayList<String> lineArray = new java.util.ArrayList<String>();
        try {
            java.io.FileInputStream stream = new java.io.FileInputStream(file);
            try {
                java.nio.channels.FileChannel fc = stream.getChannel();
                java.nio.MappedByteBuffer bb = fc.map(java.nio.channels.FileChannel.MapMode.READ_ONLY, 0, fc.size());
                char[] fileArray = java.nio.charset.Charset.defaultCharset().decode(bb).array();
                if (fileArray == null || fileArray.length == 0) {
                    return lineArray;
                }
                int length = fileArray.length;
                int start = 0;
                int index = 0;
                while (index < length) {
                    if (fileArray[index] == '\n') {
                        lineArray.add(new String(fileArray, start, index - start + 1));
                        start = index + 1;
                    } else if (fileArray[index] == '\r') {
                        if (index == length - 1) { //last character in the file
                            lineArray.add(new String(fileArray, start, length - start));
                            start = length;
                            break;
                        } else {
                            if (fileArray[index + 1] == '\n') {
                                lineArray.add(new String(fileArray, start, index - start + 2));
                                start = index + 2;
                                index++;
                            } else {
                                lineArray.add(new String(fileArray, start, index - start + 1));
                                start = index + 1;
                            }
                        }
                    }
                    index++;
                }
                if (start < length) {
                    lineArray.add(new String(fileArray, start, length - start));
                }
            } finally {
                stream.close();
            }
        } catch (java.io.IOException e) {
            System.err.println("Could not read file");
            System.err.println(e);
            e.printStackTrace();
            return lineArray;
        }
        lineArray.trimToSize();
        return lineArray;
    }


    public static boolean writeLineArrayToFile2(java.util.ArrayList<String> lineArray, java.io.File file) {
        try {
            java.io.BufferedWriter out = new java.io.BufferedWriter(new java.io.FileWriter(file));
            int size = lineArray.size();
            for (int i = 0; i < size; i++) {
                out.write(lineArray.get(i));
            }
            out.close();
        } catch (java.io.IOException e) {
            System.err.println("Could not write file");
            System.err.println(e);
            return false;
        }
        return true;
    }


    public static void main(String[] args) {
        System.out.println("Begin");
        String fileName = "test.txt";
        long start = 0;
        long stop = 0;

        start = java.util.Calendar.getInstance().getTimeInMillis();
        java.io.File f = new java.io.File(fileName);
        java.util.ArrayList<String> javaLineArray = readLineArrayFromFile1(f);
        stop = java.util.Calendar.getInstance().getTimeInMillis();
        System.out.println("Total time = " + (stop - start) + " ms");       
        java.io.File oj = new java.io.File(fileName + "_readline.txt");
        writeLineArrayToFile1(javaLineArray, oj);

        start = java.util.Calendar.getInstance().getTimeInMillis();
        java.util.ArrayList<String> myLineArray = readLineArrayFromFile2(f);
        stop = java.util.Calendar.getInstance().getTimeInMillis();
        System.out.println("Total time = " + (stop - start) + " ms");       
        java.io.File om = new java.io.File(fileName + "_custom.txt");
        writeLineArrayToFile2(myLineArray, om);

        System.out.println("End");
    }
}

版本 1 使用 readLine(),而版本 2 是我的版本,它保留了换行符。

在大约 500K 行的文本文件中,版本 1 大约需要 380 毫秒,而版本 2 需要 1074 毫秒。

如何加快 version2 的性能?

我检查了 Google guava 和 apache-commons 库,但找不到合适的替代“readLine()”,它可以告诉在读取文本文件时遇到了哪个换行符。

【问题讨论】:

  • 任何“如何加快 X 速度”问题的第一步是打开分析器并查看 CPU 大部分时间花在哪里。通常,仅仅看到一个令人惊讶的结果就可以提醒您您的代码没有完全按照您的预期执行,并突出显示一个简单的修复。

标签: java newline readline


【解决方案1】:

当问题涉及程序的速度时,您应该牢记的主要一点是,对于该程序中的任何连续进程,速度几乎总是受到以下两个因素之一的限制:CPU(处理能力)或 IO(内存分配和传输速度)。

通常要么你的 CPU 比你的 IO 快,要么相反。正因为如此,您的程序的速度限制几乎总是由其中一个决定,而且通常很容易知道是哪一个:

  • 一个执行大量计算但只对文件进行少量小操作的程序几乎肯定是CPU密集型
  • 从文件中读取大量数据或向文件写入大量数据但对处理要求不高的程序几乎肯定是IO 密集型

在尝试提高受 CPU 限制的程序的速度时,事情有点简单。它主要归结为在减少操作的同时实现相同的目标或效果。

另一方面,这并没有使过程变得更容易。事实上,优化 CPU 受限程序通常比优化 IO 受限程序要困难得多,因为每个与 CPU 相关的操作通常都是唯一的,并且必须单独修改。


虽然一旦您有了经验,通常会更容易,但对于 IO 密集型程序来说,事情并不是那么简单。在处理 IO-bound 进程时,需要考虑更多的东西。

我将使用硬盘驱动器 (HDD) 作为基础,因为我将提到的特性对 HDD 的影响最大(因为它们是机械的),但您应该记住,许多相同的概念也适用,在某种程度上,几乎适用于所有内存存储硬件,包括固态驱动器 (SSD) 甚至 RAM!

这些是大多数内存存储硬件的主要性能特征:

  • 访问时间:也称为响应时间,是硬件实际传输数据所需的时间。

    • 对于 HDD 等机械硬件,这主要与驱动器的机械特性有关,换句话说,它是旋转磁盘和移动“磁头”。因此,机械驱动器的访问时间在彼此之间可能会有很大差异。
    • 对于 SSD 和 RAM 等电路硬件,这个时间不依赖于运动部件,而是依赖于电气连接,因此访问时间非常快速且一致,您不必担心。
  • 寻找时间:硬件在其内部细分中寻找(到达)正确位置以读取或写入地址所需的时间那个部分。

    • 对于机械驱动器,主要是旋转驱动器,寻道时间测量的是执行器臂上的磁头组件移动到将读取或写入数据的磁盘轨道所花费的时间。
      平均寻道时间范围从高端服务器驱动器的 3 毫秒 (~) 到移动驱动器的 15 毫秒 (~),最常见的台式机驱动器的寻道时间通常在 9 毫秒 (~) 左右。
    • 在 RAM 和 SSD 中,没有移动部件,因此寻道时间的测量只是测试电子电路,并在设备内存中的特定位置准备操作。
      典型 SSD 的寻道时间在 0.08 到 0.16 毫秒 (~) 之间,而 RAM 更快。
  • 命令处理时间:也称为命令开销,它是驱动器的电子设备在各种设备之间建立必要通信所需的时间内部组件,因此它可以读取或写入数据。
    这对于机械和电路设备都在 0.003 毫秒 (~) 的范围内,通常在基准测试中被忽略。

  • 稳定时间:这是磁头稳定在目标轨道上并停止振动的时间,因此它们不会读取或写入偏离轨道
    该数量通常非常小(通常小于 0.1 毫秒),并且通常包含在基准测试中作为寻道时间的一部分。

  • 数据传输速率: 也称为 吞吐量,它涵盖以下两个方面:内部速率,即移动数据所需的时间在磁盘表面和驱动器上的控制器之间。以及外部速率,它是在驱动器上的控制器和主机系统中的外部组件之间移动数据的时间。它包含以下几个子因素:

    • 介质速率:驱动器从介质中读取位的速度。换句话说,就是实际的读/写速度。
    • 扇区开销:控制结构和管理驱动器、定位和验证数据以及执行其他支持功能所需的其他信息所需的额外时间(字节)。
    • 分配速度:类似于扇区开销,这是驱动器确定将写入的插槽并将它们注册到其地址字典所花费的时间。仅用于写入操作。
    • 头切换时间:从一个头电切换到另一个头所需的时间;仅适用于多头驱动器,大约为 1 到 2 毫秒。
    • 气缸切换时间:移动到相邻轨道所需的时间;使用名称 圆柱 是因为通常在移动执行器之前读取具有多个磁头或数据表面的驱动器的所有磁道,这意味着 的图像em>圆柱体而不是轨道。该时间是旋转机械驱动器独有的,通常约为 2 到 3 毫秒。

这意味着有关 IO 的主要性能问题是由 IO 和处理之间的来回切换引起的。 可以通过使用缓冲区以及处理和读取/写入更大的数据块而不是每个字节来极大地减少这个问题。

您也可以看到,虽然许多速度特征仍然存在,但 RAM 和 SSD 没有 HDD 的相同内部限制,因此它们的内部和外部传输速率 通常达到驱动器到主机接口的最大功能


块方法示例:

本示例将在桌面上创建一个Test文件夹,并在其中生成一个Test.txt文件。

文件由指定的行数生成,每行包含单词"Test" 重复特定次数(出于文件大小目的)。每行依次以"\r""\n""\r\n" 结尾。


将每个块的结果累积保存在内存中是没有意义的,因为这样做最终会导致整个文件最终进入内存,这几乎与一开始不使用块的问题相同。

因此,在同一个 Test 文件夹中创建一个输出文件,每个块的结果都存储在该文件夹中,一旦该块完成。

使用缓冲区读取基本文件,这些缓冲区另外用作块。

这里的过程只是简单地打印一个文本版本的行分隔符("\\r""\\n""\\r\\n"),然后是": ",然后是行内容;但对于最后一行,使用了"EOF"

要实际操作块,使用基于类的方法可能更容易管理,而不是纯粹基于函数的方法。

不管怎样,代码如下:

public static void main(String[] args) throws FileNotFoundException, IOException {
    File file = new File(TEST_FOLDER, "Test.txt");
    //These settings create a 122 MB file.
    generateTestFile(file, 500000, 50);

    long clock = System.nanoTime();
    processChunks(file, 8 * (int) Math.pow(1024, 2));
    clock = System.nanoTime() - clock;
    float millis = clock / 1000000f;
    float seconds = millis / 1000f;
    System.out.printf(""
                    + "%12d nanos\n"
                    + "%12.3f millis\n"
                    + "%12.3f seconds\n",
                    clock, millis, seconds);
}

public static File prepareResultFile(File source) {
    String ofn = source.getName(); //Original File Name.
    int extPos = ofn.lastIndexOf('.'); //Extension index.
    String ext = ofn.substring(extPos); //Get extension.
    ofn = ofn.substring(0, extPos); //Get name without extension reusing 'ofn'.
    return new File(source.getParentFile(), ofn + "_Result" + ext);
}

public static void processChunks(File file, int buffSize)
                throws FileNotFoundException, IOException {
    //No need for buffers bigger than the file itself.
    if (file.length() < buffSize) {
        buffSize = (int)file.length();
    }
    byte[] buffer = new byte[buffSize];
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), buffSize);

    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(
                    prepareResultFile(file)), buffSize);

    StringBuilder sb = new StringBuilder();
    while (bis.read(buffer) > (-1)) {
        //Check if a "\r\n" was split between chunks.
        boolean skipFirst = false;
        if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\r') {
            if (buffer[0] == '\n') {
                bos.write(("\\r\\n: " + sb.toString() + System.lineSeparator()).getBytes());
                sb = new StringBuilder();
                skipFirst = true;
            }
        }

        for (int i = skipFirst ? 1 : 0; i < buffer.length; i++) {
            if (buffer[i] == '\r') {
                if (i + 1 < buffer.length) {
                    if (buffer[i + 1] == '\n') {
                        bos.write(("\\r\\n: " + sb.toString() + System.lineSeparator()).getBytes());
                        i++; //Skip '\n'.
                    } else {
                        bos.write(("\\r: " + sb.toString() + System.lineSeparator()).getBytes());
                    }
                    sb = new StringBuilder(); //Reset accumulator.
                } else {
                    //A "\r\n" might be split between two chunks.
                }
            } else if (buffer[i] == '\n') {
                bos.write(("\\n: " + sb.toString() + System.lineSeparator()).getBytes());
                sb = new StringBuilder(); //Reset accumulator.
            } else {
                sb.append((char) buffer[i]);
            }
        }
    }
    bos.write(("EOF: " + sb.toString()).getBytes());
    bos.flush();
    bos.close();
    bis.close();
    System.out.println("Finished!");
}

public static boolean generateTestFile(File file, int lines, int elements)
                throws IOException {
    String[] lineBreakers = {"\r", "\n", "\r\n"};
    BufferedOutputStream bos = null;
    try {
        bos = new BufferedOutputStream(new FileOutputStream(file));
        for (int i = 0; i < lines; i++) {
            for (int ii = 1; ii < elements; ii++) {
                bos.write("test ".getBytes());
            }
            bos.write("test".getBytes());
            bos.write(lineBreakers[i % 3].getBytes());
        }
        bos.flush();
        System.out.printf("LOG: Test file \"%s\" created.\n", file.getName());
        return true;
    } catch (IOException ex) {
        System.err.println("ERR: Could not write file.");
        throw ex;
    } finally {
        try {
            bos.close();
        } catch (IOException ex) {
            System.err.println("WRN: Could not close stream.");
            Logger.getLogger(Q_13458142_v2.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

我不知道您使用的是什么 IDE,但如果是 NetBeans,请为您的代码制作内存配置文件并与此配置文件进行比较。您应该注意到处理期间所需的内存量有很大差异。

在这里,块方法的内存使用,不仅包括块本身,还包括程序自己的变量和结构,不会超过 40 MB,即使我们处理大于 100 MB 的文件也是如此。如你看到的:

它在 GB 上花费的时间也很少,在任何给定点上大多不到 5%:

【讨论】:

  • 感谢您冗长而有用的解释。虽然没有明确说明,但我在使用 FileChannel.map 一次读取整个文件时使用了某种缓冲区。
  • @SantoshTiwari - 这就是 Java 细节发挥作用的地方。考虑到您的文件大小为 10-100 MB,很可能您的 HEAP 中的数据超出了您的要求,并迫使 Java 进行完整的垃圾回收,而不是进行较慢但持续的清理。每一个“FGCs”都让程序空闲了片刻;很可能每个时间都需要 0.2 秒或更多,这是不小的时间损失。 --- 这是主观的,但我认为一个(你)应该考虑在处理大于 32 MB 的文件时分块处理(~)。 --- 我将在答案中包含一个块示例。
  • 感谢您的代码。它可以工作 - 经过一些小的改动。 :) 我会稍微修改一下 - 并对其进行基准测试。
  • 你的方法更快。 Java 的 readline 需要 830 毫秒。我的方法需要 2074 毫秒,而你的方法需要 1295 毫秒。我将调整(并彻底测试)你的方法。
【解决方案2】:

第二个版本似乎没有使用 BufferedReader 或其他形式的缓冲区。这可能是速度变慢的原因。

由于您似乎在内存中读取了整个文件,因此您也许可以将其作为一个大字符串(带有缓冲区)读取,然后在内存中对其进行解析以分析行尾。

【讨论】:

  • 关于第二部分 - 除非文件非常大(在这种情况下应该以较小的块读取处理),我相信这将是最合适的方法。它既高效又易于实施。 - PatternMatcher 的简单使用解决了 "newline-type" 问题。 --- 因此,这有我的 [+1]!
  • 你们俩都说对了,对于一个非常大的文件,不建议将整个文件读入内存。不幸的是,这是我的软件的设计限制(整个文件必须以行数组的形式提供给它)。
  • @PhiLho:我正在将整个文件作为单个大字符数组读取,然后对其进行解析以生成行。
  • @SantoshTiwari - 只是出于兴趣,您的文件有多大,内存方面? (/多少 MB?)
  • @SantoshTiwari - 优秀的文章!虽然它从稍微不同的主题(操作系统和方法调用而不是硬件)接近,但原因是相同的(许多小调用而不是一些更大的调用)。 --- 至于性能,同一篇文章表明使用 "FileChannel 和包装数组 ByteBuffer" 和使用 "BufferedInputStream 和内部没有太大区别8 KB 缓冲区" 非常小,而第二个是线程安全的并且更易于实现。因此,除非您要处理大量 (Terabytes) 的大文件 (>=256 MB ~),否则这是第二种方法。
【解决方案3】:

您将输出语句加倍(one for line and one for newline):

你可以试试下面(使用lineSeparator()获取行分隔符并在写入前追加):

        out.write(lineArray.get(i)+System.lineSeparator());

【讨论】:

  • 感谢您的建议。我将修改代码以减少写入操作的调用次数。然而,我担心阅读和生成行列表的时间。
  • 没关系,但我还是选择了你的修改。权衡是用字符串添加替换“out.write()”函数调用。在我的测试中,它并没有太大影响。我创建了一个公共静态最终字段来存储行分隔符,这样我就不会多次调用“System.getProperty("line.separator")”。
【解决方案4】:

不要重新发明轮子。
Check the BufferedReader#readLine() code
复制、粘贴并进行您需要的更改以将行分隔符保留在行内

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-26
    • 2011-06-13
    相关资源
    最近更新 更多