【问题标题】:Java FileChannel Vs BufferedReader - Spring Batch - ReaderJava FileChannel 与 BufferedReader - Spring Batch - Reader
【发布时间】:2018-08-10 04:57:38
【问题描述】:

我们处理大型文件(有时每个文件 50 GB)。应用程序读取这一文件,并根据业务逻辑写入多个输出文件(4-6)。

文件中的记录是可变长度的,记录中的每个字段都是用分隔符分隔的。

理解使用 FileChannel 和 ByteBuffer 读取文件总是比使用 BufferedReader.readLine 然后使用分隔符分割更好。

  • BufferSizes 尝试了 10240(10KB) 甚至更多
  • 提交间隔 - 5000、10000 等

下面是我们使用文件通道读取的方法:

  • 逐字节读取。检查读取的字节是否为新行 char(10) - 这意味着行尾。
  • 检查分隔符字节。捕获字节数组中读取的字节(我们初始化这个字节数组,最大字段大小为 350 字节),直到遇到分隔符字节。
  • 将直到此时读取的这些字节转换为使用 UTF-8 编码的字符串 - 具体而言是 new String(byteArr, 0, index,"UTF-8") - index 是在分隔符之前读取的字节数。李>

使用这种使用 FileChannel 读取文件的方法需要 57 分钟来处理文件。

我们想减少这个时间并尝试使用 BufferredReader.readLine() 然后使用分隔符分割,看看它的票价。

令人震惊的是,同一个文件只用了 7 分钟就完成了处理。

这里有什么问题?为什么 FileChannel 比缓冲读取器花费更多时间,然后使用字符串拆分。

我一直认为 ReadLine 和 Split 组合会对性能产生很大影响?

如果我以错误的方式使用 FileChannel,任何人都可以解释一下吗?一个

提前致谢。希望我已经正确地总结了这个问题。

以下是示例代码:

while (inputByteBuffer.hasRemaining() && (b = inputByteBuffer.get()) != 0){
        boolean endOfField = false;
        if (b == 10){
            break;
        }
        else{
            if (b == 94){//^
                if (!inputByteBuffer.hasRemaining()){
                    inputByteBuffer.clear();
                    noOfBytes = inputFileChannel.read(inputByteBuffer);
                    inputByteBuffer.flip();
                }
                if (inputByteBuffer.hasRemaining()){
                    byte b2 = inputByteBuffer.get();
                    if (b2 == 124){//|
                        if (!inputByteBuffer.hasRemaining()){
                            inputByteBuffer.clear();
                            noOfBytes = inputFileChannel.read(inputByteBuffer);
                            inputByteBuffer.flip();
                        }

                        if (inputByteBuffer.hasRemaining()){
                            byte b3 = inputByteBuffer.get();
                            if (b3 == 94){//^
                                String field = new String(fieldBytes, 0, index, encoding);
                                if(fieldIndex == -1){
                                    fields = new String[sizeFromAConfiguration];
                                }else{
                                    fields[fieldIndex] = field;
                                }

                                fieldBytes = new byte[maxFieldSize];
                                endOfField = true;
                                fieldIndex++;
                            }
                            else{
                                fieldBytes = addFieldBytes(fieldBytes, b, index);
                                index++;
                                fieldBytes = addFieldBytes(fieldBytes, b2, index);
                                index++;
                                fieldBytes = addFieldBytes(fieldBytes, b3, index);
                            }
                        }
                        else{
                            endOfFile = true;
                            //fields.add(new String(fieldBytes, 0, index, encoding));
                            fields[fieldIndex] = new String(fieldBytes, 0, index, encoding);
                            fieldBytes = new byte[maxFieldSize];
                            endOfField = true;
                        }
                    }else{
                        fieldBytes = addFieldBytes(fieldBytes, b, index);
                        index++;
                        fieldBytes = addFieldBytes(fieldBytes, b2, index);

                    }
                }else{
                    endOfFile = true;
                    fieldBytes = addFieldBytes(fieldBytes, b, index);
                }
            }
            else{
                fieldBytes = addFieldBytes(fieldBytes, b, index);
            }
        }

        if (!inputByteBuffer.hasRemaining()){
            inputByteBuffer.clear();
            noOfBytes = inputFileChannel.read(inputByteBuffer);
            inputByteBuffer.flip();
        }

        if (endOfField){
            index = 0;
        }
        else{
            index++;
        }

    }

【问题讨论】:

  • BufferedReader 不会逐字节读取,您也不应该读取。您应该选择一个大小合理的缓冲区(BufferedReader 有一个 8192 字节的缓冲区)。是的,它会更难实现,但你不会浪费 CPU 周期一次读取一个字节。
  • 逐字节读取任何文件是最坏的情况。任何事情都会对此有所改进。 readLine() 大约是最好的情况。应避免拆分或创建字符串。
  • 可能是我说的不对,我说的是逐字节读取,我们先把10240字节读入字节缓冲区,然后用ByteBuffer.get()方法检查是哪个字节。跨度>
  • 由于您显然无法准确描述它,您当然应该发布一些代码。
  • if (!inputByteBuffer.hasRemaining()){ inputByteBuffer.clear(); noOfBytes = inputFileChannel.read(inputByteBuffer); inputByteBuffer.flip(); }

标签: java performance spring-batch bufferedreader filechannel


【解决方案1】:

这里的主要问题是非常快速地创建一个新字节[](fieldBytes = new byte[maxFieldSize];)。

由于每次迭代都会创建一个新数组,因此经常启动垃圾收集,这会触发“停止世界”以回收内存。

而且,对象创建可能很昂贵。

我们宁愿初始化字节数组一次,然后跟踪索引以将字段转换为带有结束索引的字符串。

无论如何,BufferedReader 比 FileChannel 快,至少可以读取 ASCII 文件,并且为了保持代码简单,我们继续使用 Bufferred Reader 本身。

使用 Bufferred 阅读器,无需繁琐的逻辑来查找分隔符和填充对象,可以减少开发和测试工作。

【讨论】:

    【解决方案2】:

    我确实尝试了 NIO 的所有可能选项(根据我的知识和研究,在这篇文章中提供),发现它在读取文本文件方面没有任何地方接近 BufferedReader。

    将 BufferedReader 更改为使用 StringBuilder 代替 StringBuffer,我没有看到任何显着的性能改进(某些文件只有几秒钟,其中一些文件使用 StringBuffer 本身效果更好)。

    删除同步块也没有带来太多/任何改进。而且不值得对我们没有任何好处的东西进行调整。

    以下是大约 50 GB 的文件所花费的时间(读取、处理、写入 - 处理和写入所花费的时间并不重要 - 甚至不到 20% 的时间) 蔚来:71.67(分钟) IO(BufferedReader):10.84(分钟)

    感谢大家抽出宝贵时间阅读和回复这篇文章并提供建议。

    【讨论】:

      【解决方案3】:

      虽然无法确定特定代码的行为方式,但我想最好的方法是像您一样对其进行分析。FileChannel 虽然被认为更快,但实际上对您的情况没有帮助。但这可能不是因为从文件中读取,但是您对读取的内容进行了实际处理。 在处理文件时我想指出的一篇文章是 https://www.redgreencode.com/why-is-java-io-slow/

      还有对应的 Github 代码库 Java IO benchmark

      我想指出这段代码使用了两个世界的组合 fos = new FileOutputStream(outputFile); outFileChannel = fos.getChannel(); bufferedWriter = new BufferedWriter(Channels.newWriter(outFileChannel, "UTF-8"));

      既然是在你的情况下阅读的,我会考虑

      File inputFile = new File("C:\\input.txt");
      FileInputStream fis = new FileInputStream(inputFile);
      FileChannel inputChannel = fis.getChannel();
      BufferedReader bufferedReader = new BufferedReader(Channels.newReader(inputChannel,"UTF-8"));
      

      我还将调整块大小,并且使用 Spring 批处理总是反复试验才能找到最佳位置。

      在完全不相关的说明中,您无法使用 BufferedReader 问题的原因是由于字符加倍,我假设这种情况更常见于 ebcdic 字符。我将简单地运行这样的循环来识别麻烦制造者和从源头上消除。

      import java.io.UnsupportedEncodingException;
      
      public class EbcdicConvertor {
      
          public static void main(String[] args) throws UnsupportedEncodingException {
              int index = 0;
              for (int i = -127; i < 128; i++) {
                  byte[] b = new byte[1];
                  b[0] = (byte) i;
                  String cp037 = new String(b, "CP037");
                  if (cp037.getBytes().length == 2) {
                      index++;
                      System.out.println(i + "::" + cp037);
                  }
              }
              System.out.println(index);
          }
      }
      

      上面的答案没有测试我的实际假设。这是一个测量时间的实际程序。结果在 200 MB 的文件中不言自明

      import java.io.File;
      import java.io.FileInputStream;
      import java.io.FileReader;
      import java.io.RandomAccessFile;
      import java.nio.ByteBuffer;
      import java.nio.channels.Channels;
      import java.nio.channels.FileChannel;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.Scanner;
      import java.util.regex.Pattern;
      
      public class ReadComplexDelimitedFile {
          private static long total = 0;
          private static final Pattern DELIMITER_PATTERN = Pattern.compile("\\^\\|\\^");
      
          private void readFileUsingScanner() {
      
              String s;
              try (Scanner stdin = new Scanner(new File(this.getClass().getResource("input.txt").getPath()))) {
                  while (stdin.hasNextLine()) {
                      s = stdin.nextLine();
                      String[] fields = DELIMITER_PATTERN.split(s, 0);
                      total = total + fields.length;
                  }
              } catch (Exception e) {
                  System.err.println("Error");
              }
      
          }
      
          private void readFileUsingCustomBufferedReader() {
      
              try (BufferedReader stdin = new BufferedReader(new FileReader(new File(this.getClass().getResource("input.txt").getPath())))) {
                  String s;
                  while ((s = stdin.readLine()) != null) {
                      String[] fields = DELIMITER_PATTERN.split(s, 0);
                      total += fields.length;
                  }
              } catch (Exception e) {
                  System.err.println("Error");
              }
      
          }
      
          private void readFileUsingBufferedReader() {
      
              try (java.io.BufferedReader stdin = new java.io.BufferedReader(new FileReader(new File(this.getClass().getResource("input.txt").getPath())))) {
                  String s;
                  while ((s = stdin.readLine()) != null) {
                      String[] fields = DELIMITER_PATTERN.split(s, 0);
                      total += fields.length;
                  }
              } catch (Exception e) {
                  System.err.println("Error");
              }
      
          }
      
      
          private void readFileUsingBufferedReaderFileChannel() {
              try (FileInputStream fis = new FileInputStream(this.getClass().getResource("input.txt").getPath())) {
                  try (FileChannel inputChannel = fis.getChannel()) {
                      try (BufferedReader stdin = new BufferedReader(Channels.newReader(inputChannel, "UTF-8"))) {
                          String s;
                          while ((s = stdin.readLine()) != null) {
                              String[] fields = DELIMITER_PATTERN.split(s, 0);
                              total = total + fields.length;
                          }
                      }
                  } catch (Exception e) {
                      System.err.println("Error");
                  }
              } catch (Exception e) {
                  System.err.println("Error");
              }
      
          }
      
          private void readFileUsingBufferedReaderByteFileChannel() {
              try (FileInputStream fis = new FileInputStream(this.getClass().getResource("input.txt").getPath())) {
                  try (FileChannel inputChannel = fis.getChannel()) {
                      try (BufferedReader stdin = new BufferedReader(Channels.newReader(inputChannel, "UTF-8"))) {
                          int b;
                          StringBuilder sb = new StringBuilder();
                          while ((b = stdin.read()) != -1) {
                              if (b == 10) {
      
                                  total = total + DELIMITER_PATTERN.split(sb, 0).length;
                                  sb = new StringBuilder();
                              } else {
                                  sb.append((char) b);
                              }
                          }
                      }
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
              } catch (Exception e) {
                  System.err.println("Error");
              }
      
          }
      
          private void readFileUsingFileChannelStream() {
      
              try (RandomAccessFile fis = new RandomAccessFile(new File(this.getClass().getResource("input.txt").getPath()), "r")) {
                  try (FileChannel inputChannel = fis.getChannel()) {
                      ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
                      ByteBuffer recordBuffer = ByteBuffer.allocate(250);
                      int recordLength = 0;
                      while ((inputChannel.read(byteBuffer)) != -1) {
                          byte b;
                          byteBuffer.flip();
                          while (byteBuffer.hasRemaining() && (b = byteBuffer.get()) != -1) {
                              if (b == 10) {
                                  recordBuffer.flip();
                                  total = total + splitIntoFields(recordBuffer, recordLength);
                                  recordBuffer.clear();
                                  recordLength = 0;
                              } else {
                                  ++recordLength;
                                  recordBuffer.put(b);
                              }
                          }
                          byteBuffer.clear();
                      }
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
      
          }
      
          private int splitIntoFields(ByteBuffer recordBuffer, int recordLength) {
              byte b;
              String[] fields = new String[17];
              int fieldCount = -1;
              StringBuilder sb = new StringBuilder();
              for (int i = 0; i < recordLength - 1; i++) {
                  b = recordBuffer.get(i);
                  if (b == 94 && recordBuffer.get(++i) == 124 && recordBuffer.get(++i) == 94) {
                      fields[++fieldCount] = sb.toString();
                      sb = new StringBuilder();
                  } else {
                      sb.append((char) b);
                  }
              }
              fields[++fieldCount] = sb.toString();
              return fields.length;
      
          }
      
      
          public static void main(String args[]) {
              //JVM wamrup
              for (int i = 0; i < 100000; i++) {
                  total += i;
              }
              // We know scanner is slow-Still warming up
              ReadComplexDelimitedFile readComplexDelimitedFile = new ReadComplexDelimitedFile();
              List<Long> longList = new ArrayList<>(50);
              for (int i = 0; i < 50; i++) {
                  total = 0;
                  long startTime = System.nanoTime();
                  readComplexDelimitedFile.readFileUsingScanner();
                  long stopTime = System.nanoTime();
                  long timeDifference = stopTime - startTime;
                  longList.add(timeDifference);
      
              }
              System.out.println("Time taken for readFileUsingScanner");
              longList.forEach(System.out::println);
              // Actual performance test starts here
      
              longList = new ArrayList<>(10);
              for (int i = 0; i < 10; i++) {
                  total = 0;
                  long startTime = System.nanoTime();
                  readComplexDelimitedFile.readFileUsingBufferedReaderFileChannel();
                  long stopTime = System.nanoTime();
                  long timeDifference = stopTime - startTime;
                  longList.add(timeDifference);
      
              }
              System.out.println("Time taken for readFileUsingBufferedReaderFileChannel");
              longList.forEach(System.out::println);
              longList.clear();
              for (int i = 0; i < 10; i++) {
                  total = 0;
                  long startTime = System.nanoTime();
                  readComplexDelimitedFile.readFileUsingBufferedReader();
                  long stopTime = System.nanoTime();
                  long timeDifference = stopTime - startTime;
                  longList.add(timeDifference);
      
              }
              System.out.println("Time taken for readFileUsingBufferedReader");
              longList.forEach(System.out::println);
              longList.clear();
              for (int i = 0; i < 10; i++) {
                  total = 0;
                  long startTime = System.nanoTime();
                  readComplexDelimitedFile.readFileUsingCustomBufferedReader();
                  long stopTime = System.nanoTime();
                  long timeDifference = stopTime - startTime;
                  longList.add(timeDifference);
      
              }
              System.out.println("Time taken for readFileUsingCustomBufferedReader");
              longList.forEach(System.out::println);
              longList.clear();
              for (int i = 0; i < 10; i++) {
                  total = 0;
                  long startTime = System.nanoTime();
                  readComplexDelimitedFile.readFileUsingBufferedReaderByteFileChannel();
                  long stopTime = System.nanoTime();
                  long timeDifference = stopTime - startTime;
                  longList.add(timeDifference);
      
              }
              System.out.println("Time taken for readFileUsingBufferedReaderByteFileChannel");
              longList.forEach(System.out::println);
              longList.clear();
              for (int i = 0; i < 10; i++) {
                  total = 0;
                  long startTime = System.nanoTime();
                  readComplexDelimitedFile.readFileUsingFileChannelStream();
                  long stopTime = System.nanoTime();
                  long timeDifference = stopTime - startTime;
                  longList.add(timeDifference);
      
              }
              System.out.println("Time taken for readFileUsingFileChannelStream");
              longList.forEach(System.out::println);
      
          }
      }
      

      BufferedReader 写得很早,因此我们可以重写与此示例相关的一些部分。例如,我们不关心 \r 和 skipLF 或 skipCR 或那些东西 我们将读取文件(无需同步) 通过扩展不需要StringBuffer,即使可以使用StringBuilder。立即看到性能改进。

      危险的 hack,删除同步并用 StringBuilder 替换 StringBuffer 未经适当测试且不知道自己在做什么,请勿使用它

      public String readLine() throws IOException {
              StringBuilder s = null;
              int startChar;
      
      
              bufferLoop:
              for (; ; ) {
      
                  if (nextChar >= nChars)
                      fill();
                  if (nextChar >= nChars) { /* EOF */
                      if (s != null && s.length() > 0)
                          return s.toString();
                      else
                          return null;
                  }
                  boolean eol = false;
                  char c = 0;
                  int i;
      
                  /* Skip a leftover '\n', if necessary */
      
      
                  charLoop:
                  for (i = nextChar; i < nChars; i++) {
                      c = cb[i];
                      if (c == '\n') {
                          eol = true;
                          break charLoop;
                      }
                  }
      
                  startChar = nextChar;
                  nextChar = i;
      
                  if (eol) {
                      String str;
                      if (s == null) {
                          str = new String(cb, startChar, i - startChar);
                      } else {
                          s.append(cb, startChar, i - startChar);
                          str = s.toString();
                      }
                      nextChar++;
                      return str;
                  }
      
                  if (s == null)
                      s = new StringBuilder(defaultExpectedLineLength);
                  s.append(cb, startChar, i - startChar);
              }
          }
      

      Java 8 英特尔 i5 12 GB RAM Windows 10 结果:

      readFileUsingBufferedReaderFileChannel 花费的时间::

      • 2581635057 1849820885 1763992972 1770510738 1746444157 1733491399 1740530125 1723907177 1724280512 1732445638

      readFileUsingBufferedReader 所用时间

      • 1851027073 1775304769 1803507033 1789979554 1786974538 1802675458 1789672780 1798036307 1789847714 1785302003

      readFileUsingCustomBufferedReader 所用时间

      1. 1745220476 1721039975 1715383650 1728548462 1724746005 1718177466 1738026017 1748077438 1724608192 1736294175

      readFileUsingBufferedReaderByteFileChannel 所用时间

      • 2872857919 2480237636 2917488143 2913491126 2880117231 2904614745 2911756298 2878777496 2892169722 2888091211

      readFileUsingFileChannelStream 所用时间

      • 3039447073 2896156498 2538389366 2906287280 2887612064 2929288046 2895626578 2955326255 2897535059 2884476915

      进程以退出代码 0 结束

      【讨论】:

      • FileChannel 不具有“非阻塞性质”。
      • 我的错,已编辑
      • 无论如何,根据我的分析,bufferedReader readline 比所有其他方法更快是结论性的
      【解决方案4】:

      常量hasRemaining()/read() 检查以及常量get() 调用会导致大量开销。将get() 整个缓冲区放入一个数组并直接处理它可能会更好,只有在你到达末尾时才调用read()

      要回答 cmets 中的问题,您不应为每次读取分配新的 ByteBuffer。这是昂贵的。继续使用同一个。请注意,在此应用程序中使用DirectByteBuffer。这不合适:仅当您希望数据保持在 JVM/JNI 边界以南时才合适,例如当只是在频道之间复制时。

      但我想我会扔掉它,或者改写它,使用BufferedReader.read(),而不是readLine(),然后使用字符串拆分,并使用与您在这里的逻辑大致相同的逻辑,当然您不这样做'不需要继续调用hasRemaining() 并填充缓冲区,BufferedReader 会自动为您完成。

      您必须注意将read() 的结果存储到int 中,并在每个read() 之后检查它是否为-1。

      我不清楚你实际上应该使用Reader,除非你知道你有多字节文本。可能一个简单的BufferedInputStream 会更合适。

      【讨论】:

      • 感谢您的详细解释。我们的文件是 UTF-8 编码的,并且有一个字符可以构成两个字节的特殊字符。我们必须转换为字符串来执行一些业务逻辑。
      • 我会尝试上述建议并在此处发布任何改进/发现
      • 我尝试将 bytebuffer 的内容复制到 byte[],然后遍历 byte[],但它看起来比以前花费了更多的时间:if (byteArrayIndex == 0 || byteArrayIndex == byteArray .length){ inputByteBuffer.clear();字节数组索引 = 0; int noOfBytes = inputFileChannel.read(inputByteBuffer); inputByteBuffer.flip(); byteArray = 新字节[noOfBytes]; inputByteBuffer.get(byteArray,0,noOfBytes); }
      • 一个原因可能是代码的原作者没有分析代码。 :)
      猜你喜欢
      • 2023-03-15
      • 1970-01-01
      • 1970-01-01
      • 2016-04-15
      • 2018-12-22
      • 1970-01-01
      • 2020-08-24
      • 2020-01-22
      • 1970-01-01
      相关资源
      最近更新 更多