2

对于我的应用程序,我必须编写一个自定义的“readline”方法,因为我想检测并保留 ASCII 文本文件中的换行符结尾。JavareadLine()方法不知道遇到了哪个换行符序列 ( \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()”,它会告诉在读取文本文件时遇到了哪个换行符。

4

4 回答 4

2

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

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

于 2012-11-19T16:40:51.407 回答
2

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

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

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

尝试提高受 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 ms (~) 的范围内,通常在基准测试中被忽略。

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

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

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

于 2012-11-23T18:57:00.693 回答
1

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

您可以在下面尝试(用于lineSeparator()获取行分隔符并在写入之前附加):

        out.write(lineArray.get(i)+System.lineSeparator());
于 2012-11-19T16:45:53.093 回答
0

不要重新发明轮子。
检查 BufferedReader#readLine() 代码
复制、粘贴并进行所需的更改以将行分隔符保留在行内

于 2012-11-23T21:11:01.927 回答