筆記:Java的IO性能調整

 本文大多技術圍繞調整磁盤文件 I/O,可是有些內容也一樣適合網絡 I/O 和窗口輸出。

第一部分技術討論底層的I/O問題,而後討論諸如壓縮,格式化和串行化等高級I/O問題。然而這個討論沒有包含應用設計問題,例如搜索算法和數據結構,也沒有討論系統級的問題,例如文件高速緩衝。

Java語言採起兩種大相徑庭的磁盤文件結構。一個是基於字節流,另外一個是字符序列。在Java語言中一個字符有兩個字節表示,而不是像一般的語言如c語言那樣是一個字節。所以,從一個文件讀取字符時須要進行轉換。這個不一樣在某些狀況下是很重要的,就像下面的幾個例子將要展現的那樣。

低級I/O相關的問題:

    緩衝
    讀寫文本文件
    格式化的代價
    隨機訪問

高級I/O問題

    壓縮
    高速緩衝
    分解
    串行化
    獲取文件信息
    更多信息

加速I/O的基本規則

    避免訪問磁盤
    避免訪問底層的操做系統
    避免方法調用
    避免個別的處理字節和字符

很明顯這些規則不能在全部的問題上避免,由於若是可以的話就沒有實際的I/O被執行。

使用緩存減小讀寫次數開銷

使用緩衝加速文件讀取的示例:

對於一個1 MB的輸入文件,以秒爲單位的執行時間是:

 FileInputStream的read方法,每次讀取一個字節,不用緩衝             6.9秒

 BufferedInputStream的read方法使用BufferedInputStream              0.9秒

 FileInputStream的read方法讀取數據到直接緩衝                       0.4秒

或者說在最慢的方法和最快的方法間是17比1的不一樣。

這個巨大的加速並不能證實你應該老是使用第三種方法,即本身作緩衝。這多是一個錯誤的傾向特別是在處理文件結束事件時沒有仔細的實現。在可讀性上它也沒有其它方法好。可是記住時間花費在哪兒了以及在必要的時候如何矯正是頗有用。方法2 或許是對於大多應用的 "正確" 方法.

方法 2 和 3 使用了緩衝技術, 大塊文件被從磁盤讀取,而後每次訪問一個字節或字符。緩衝是一個基本而重要的加速I/O 的技術,並且有幾個類支持緩衝(BufferedInputStream 用於字節, BufferedReader 用於字符)。

緩衝區越大I/O越快嗎?典型的Java緩衝區長1024 或者 2048 字節,一個更大的緩衝區有可能加速 I/O但比重很小,大約5 到10%。

方法1: 讀方法

第一個方法簡單的使用FileInputStream的read方法:

FileInputStream的read方法每次讀取文件的下一個字節,觸發了大量的底層運行時系統調用

優勢:編碼簡單,適用於小文件

缺點:讀寫頻繁,不適用於大文件

 import java.io.*;

  public class intro1 {

    public static void main(String args[]) {

      if (args.length != 1) {

        System.err.println("missing filename");

        System.exit(1);

      }

      try {

        FileInputStream fis = new FileInputStream(args[0]); //創建指向文件的讀寫流

        int cnt = 0;

        int b;

        while ((b = fis.read()) != -1) {  // FileInputStream的read方法每次讀取文件一個字節

          if (b == '\n')

            cnt++;

        }

        fis.close();

        System.out.println(cnt);

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

方法 2: 使用大緩衝區

第二種方法使用大緩衝區避免了上面的問題:

BufferedInputStream的read方法 把文件的字節塊讀入緩衝區,而後每次讀取一個字節,每次填充緩衝只須要訪問一次底層存儲接口

優勢:避免每一個字節的底層讀取,編碼相對不復雜

缺點:緩存佔用了小量內存

import java.io.*;

 public class intro2 {

   public static void main(String args[]) {

    if (args.length != 1) {

      System.err.println("missing filename");

      System.exit(1);

    }

    try {

      FileInputStream fis =  new FileInputStream(args[0]);

      BufferedInputStream bis = new BufferedInputStream(fis); //把文件讀取流指向緩衝區

      int cnt = 0;

      int b;

      while ((b = bis.read()) != -1) {   //BufferedInputStream的read方法 把文件的字節塊獨     

                                  //入緩衝區BufferedInputStream,而後每次讀取一個字節

        if (b == '\n')

          cnt++;

        }

      bis.close();

      System.out.println(cnt);

    }

    catch (IOException e) {

      System.err.println(e);

    }

  }

 }

方法 3: 直接緩衝

FileInputStream的read方法直接讀入字節塊到直接緩衝buf,而後每次讀取一個字節。

優勢:速度最快,

缺點:編碼稍微複雜,可讀性差,佔用了小量內存,

import java.io.*;

  public class intro3 {

    public static void main(String args[]) {

      if (args.length != 1) {

        System.err.println("missing filename");

        System.exit(1);

      }

      try {

        FileInputStream fis = new FileInputStream(args[0]);

        byte buf[] = new byte[2048];

        int cnt = 0;

        int n;

        while ((n = fis.read(buf)) != -1) { // FileInputStream的read方法直接讀入字節塊到

                                     //直接緩衝buf,而後每次讀取一個字節

          for (int i = 0; i < n; i++) {

            if (buf[i] == '\n')

              cnt++;

          }

        }

        fis.close();

        System.out.println(cnt);

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

方法4: 緩衝整個文件

緩衝的極端狀況是事先決定整個文件的長度,而後讀取整個文件。

優勢:把文件底層讀取降到最少,一次,

缺點:大文件會耗盡內存。

import java.io.*;

  public class readfile {

    public static void main(String args[]) {

      if (args.length != 1) {

        System.err.println("missing filename");

        System.exit(1);

      }

      try {

        int len = (int)(new File(args[0]).length());

        FileInputStream fis = new FileInputStream(args[0]);

        byte buf[] = new byte[len];                  //創建直接緩衝

        fis.read(buf);                             // 讀取整個文件

        fis.close();

        int cnt = 0;

        for (int i = 0; i < len; i++) {

          if (buf[i] == '\n')

            cnt++;

        }

        System.out.println(cnt);

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

這個方法很方便,在這裏文件被看成一個字節數組。可是有一個明顯得問題是有可能沒有讀取一個巨大的文件的足夠的內存。

緩衝的另外一個方面是向窗口終端的文本輸出。缺省狀況下, System.out ( 一個PrintStream) 是行緩衝的,這意味着在遇到一個新行符後輸出緩衝區被提交。

格式化的代價

實際上向文件寫數據只是輸出代價的一部分。另外一個可觀的代價是數據格式化。

考慮下面的字符輸出程序

性能對比結果爲:這些程序產生一樣的輸出。運行時間是:

格式化方法
    

示例語句
    

運行時間

簡單的輸出一個固定字符
    

System.out.print(s);
    

1.3秒

使用簡單格式"+"
    

String s = 字符+字符,       

System.out.print(s);

 
    

1.8秒

使用java.text包中的 MessageFormat 類的對象方法
    

String s = fmt.format(values);

System.out.print(s);
    

7.8秒

使用java.text包中的 MessageFormat 類的靜態方法
    

 
    

7.8*1.3秒

最慢的和最快的大約是6比1。若是格式沒有預編譯第三種方法將更慢,使用靜態的方法代替:

第三個方法比前兩種方法慢不少的事實並不意味着你不該該使用它,而是你要意識到時間上的開銷。

在國際化的狀況下信息格式化是很重要的,關心這個問題的應用程序一般從一個綁定的資源中讀取格式而後使用它。

方法 1,簡單的輸出一個固定的字符串,瞭解固有的I/O開銷:

public class format1 {

    public static void main(String args[]) {

      final int COUNT = 25000;

      for (int i = 1; i <= COUNT; i++) {

        String s = "The square of 5 is 25\n";

        System.out.print(s);

      }

    }

  }

方法2,使用簡單格式"+":

public class format2 {

    public static void main(String args[]) {

      int n = 5;

      final int COUNT = 25000;

      for (int i = 1; i <= COUNT; i++) {

        String s = "The square of " + n + " is " +

            n * n + "\n";

        System.out.print(s);

      }

    }

  }

方法 3,第三種方法使用java.text包中的 MessageFormat 類的對象方法:

import java.text.*;

 public class format3 {

   public static void main(String args[]) {

     MessageFormat fmt =

      new MessageFormat("The square of {0} is {1}\n");

      Object values[] = new Object[2];

    int n = 5;

    values[0] = new Integer(n);

    values[1] = new Integer(n * n);

    final int COUNT = 25000;

    for (int i = 1; i <= COUNT; i++) {

      String s = fmt.format(values);

      System.out.print(s);

     }

    }

  }

方法 4,使用MessageFormat.format(String, Object[]) 類的靜態方法

import java.text.*;

  public class format4 {

    public static void main(String args[]) {

      String fmt = "The square of {0} is {1}\n";

      Object values[] = new Object[2];

      int n = 5;

      values[0] = new Integer(n);

      values[1] = new Integer(n * n);

      final int COUNT = 25000;

      for (int i = 1; i <= COUNT; i++) {

        String s =

            MessageFormat.format(fmt, values);

        System.out.print(s);

      }

    }

  }

這比前一個例子多花費1/3的時間。

隨機訪問的性能開銷

RandomAccessFile 是一個進行隨機文件I/O(在字節層次上)的類。這個類提供一個seek方法,和 C/C++中的類似,移動文件指針到任意的位置,而後從那個位置字節能夠被讀取或寫入。

seek方法訪問底層的運行時系統所以每每是消耗巨大的。一個更好的代替是在RandomAccessFile上創建你本身的緩衝,並實現一個直接的字節read方法。read方法的參數是字節偏移量(>= 0)。這樣的一個例子是:

這個程序簡單的讀取字節序列而後輸出它們。

適用的狀況:若是有訪問位置,這個技術是頗有用的,文件中的附近字節幾乎在同時被讀取。例如,若是你在一個排序的文件上實現二分法查找,這個方法可能頗有用。

不適用的狀況:若是你在一個巨大的文件上的任意點作隨機訪問的話就沒有太大價值。

import java.io.*;

  public class ReadRandom {

    private static final int DEFAULT_BUFSIZE = 4096;

    private RandomAccessFile raf;

    private byte inbuf[];

    private long startpos = -1;

    private long endpos = -1;

    private int bufsize;

    public ReadRandom(String name)

     throws FileNotFoundException {

      this(name, DEFAULT_BUFSIZE);

    }

    public ReadRandom(String name, int b)

        throws FileNotFoundException {

      raf = new RandomAccessFile(name, "r");

      bufsize = b;

      inbuf = new byte[bufsize];

    }

    public int read(long pos) {

      if (pos < startpos || pos > endpos) {

        long blockstart = (pos / bufsize) * bufsize;

        int n;

        try {

          raf.seek(blockstart);

          n = raf.read(inbuf);

        }

        catch (IOException e) {

          return -1;

        }

        startpos = blockstart;

        endpos = blockstart + n - 1;

        if (pos < startpos || pos > endpos)

          return -1;

      }

      return inbuf[(int)(pos - startpos)] & 0xffff;

    }

    public void close() throws IOException {

      raf.close();

    }

    public static void main(String args[]) {

      if (args.length != 1) {

        System.err.println("missing filename");

        System.exit(1);

      }

      try {

        ReadRandom rr = new ReadRandom(args[0]);

        long pos = 0;

        int c;

        byte buf[] = new byte[1];

        while ((c = rr.read(pos)) != -1) {

          pos++;

          buf[0] = (byte)c;

          System.out.write(buf, 0, 1);

        }

        rr.close();

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

壓縮的性能開銷

Java提供用於壓縮和解壓字節流的類,這些類包含在java.util.zip 包裏面,這些類也做爲 Jar 文件的服務基礎 ( Jar 文件是帶有附加文件列表的 Zip 文件)。

壓縮的目的是減小存儲空間,同時被壓縮的文件在IO速度不變的狀況下會減小傳輸時間。

壓縮時候要消耗CPU時間,佔用內存。

壓縮時間=數據量/壓縮速度。

IO傳輸時間=數據容量/ IO速度。

傳輸數據的總時間=壓縮時間+I/O傳輸時間

壓縮是提升仍是損害I/O性能很大程度依賴你的硬件配置,特別是和處理器和磁盤驅動器的速度相關。使用Zip技術的壓縮一般意味着在數據大小上減小50%,可是代價是壓縮和解壓的時間。一個巨大(5到10 MB)的壓縮文本文件,使用帶有IDE硬盤驅動器的300-MHz Pentium PC從硬盤上讀取能夠比不壓縮少用大約1/3的時間。

壓縮的一個有用的範例是向很是慢的媒介例如軟盤寫數據。使用高速處理器(300 MHz Pentium)和低速軟驅(PC上的普通軟驅)的一個測試顯示壓縮一個巨大的文本文件而後在寫入軟盤比直接寫入軟盤快大約50% 。

下面的程序接收一個輸入文件並將之寫入一個只有一項的壓縮的 Zip 文件:

import java.io.*;

 import java.util.zip.*;

  public class compress {

    public static void doit(String filein, String fileout) {

      FileInputStream fis = null;

      FileOutputStream fos = null;

      try {

        fis = new FileInputStream(filein);

        fos = new FileOutputStream(fileout);

        ZipOutputStream zos =  new ZipOutputStream(fos);

        ZipEntry ze = new ZipEntry(filein);

        zos.putNextEntry(ze);

        final int BUFSIZ = 4096;

        byte inbuf[] = new byte[BUFSIZ];

        int n;

        while ((n = fis.read(inbuf)) != -1)

          zos.write(inbuf, 0, n);

        fis.close();

        fis = null;

        zos.close();

        fos = null;

      }

      catch (IOException e) {

        System.err.println(e);

      }

      finally {

        try {

          if (fis != null)

            fis.close();

          if (fos != null)

            fos.close();

        }

        catch (IOException e) {

        }

      }

    }

  public static void main(String args[]) {

    if (args.length != 2) {

     System.err.println("missing filenames");

     System.exit(1);

    }

   if (args[0].equals(args[1])) {

     System.err.println("filenames are identical");

     System.exit(1);

      }

      doit(args[0], args[1]);

    }

  }

下一個程序執行相反的過程,將一個假設只有一項的Zip文件做爲輸入而後將之解壓到輸出文件:

import java.io.*;

 import java.util.zip.*;

  public class uncompress {

    public static void doit(String filein, String fileout) {

      FileInputStream fis = null;

      FileOutputStream fos = null;

      try {

        fis = new FileInputStream(filein);

        fos = new FileOutputStream(fileout);

        ZipInputStream zis = new ZipInputStream(fis);

        ZipEntry ze = zis.getNextEntry();

        final int BUFSIZ = 4096;

        byte inbuf[] = new byte[BUFSIZ];

        int n;

        while ((n = zis.read(inbuf, 0, BUFSIZ)) != -1)

          fos.write(inbuf, 0, n);

        zis.close();

        fis = null;

        fos.close();

        fos = null;

      }

      catch (IOException e) {

        System.err.println(e);

      }

      finally {

        try {

          if (fis != null)

            fis.close();

          if (fos != null)

            fos.close();

        }

        catch (IOException e) {

        }

      }

    }

    public static void main(String args[]) {

      if (args.length != 2) {

     System.err.println("missing filenames");

     System.exit(1);

      }

    if (args[0].equals(args[1])) {

     System.err.println("filenames are identical");

     System.exit(1);

      }

      doit(args[0], args[1]);

    }

  }

 

高速緩存

關於硬件的高速緩存的詳細討論超出了本文的討論範圍。可是在有些狀況下軟件高速緩存能被用於加速I/O。考慮從一個文本文件裏面以隨機順序讀取一行的狀況,這樣作的一個方法是讀取全部的行,而後把它們存入一個ArrayList (一個相似Vector的集合類):

import java.io.*;

 import java.util.ArrayList;

  public class LineCache {

    private ArrayList list = new ArrayList();

    public LineCache(String fn) throws IOException {

      FileReader fr = new FileReader(fn);

      BufferedReader br = new BufferedReader(fr);

      String ln;

      while ((ln = br.readLine()) != null)

        list.add(ln);

      br.close();

    }

    public String getLine(int n) {

      if (n < 0)

        throw new IllegalArgumentException();

      return (n < list.size() ? (String)list.get(n) : null);

    }

    public static void main(String args[]) {

      if (args.length != 1) {

        System.err.println("missing filename");

        System.exit(1);

      }

      try {

        LineCache lc = new LineCache(args[0]);

        int i = 0;

        String ln;

        while ((ln = lc.getLine(i++)) != null)

          System.out.println(ln);

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

getLine 方法被用來獲取任意行。這個技術是頗有用的,可是很明顯對一個大文件使用了太多的內存,所以有侷限性。一個代替的方法是簡單的記住被請求的行最近的100行,其它的請求直接從磁盤讀取。這個安排在局域性的訪問時頗有用,可是在真正的隨機訪問時沒有太大做用?

分解

分解 是指將字節或字符序列分割爲像單詞這樣的邏輯塊的過程。Java 提供StreamTokenizer 類, 像下面這樣操做:

import java.io.*;

  public class token1 {

    public static void main(String args[]) {

     if (args.length != 1) {

       System.err.println("missing filename");

       System.exit(1);

      }

      try {

        FileReader fr = new FileReader(args[0]);

        BufferedReader br = new BufferedReader(fr);

        StreamTokenizer st = new StreamTokenizer(br);

        st.resetSyntax();

        st.wordChars('a', 'z');

        int tok;

        while ((tok = st.nextToken()) !=

            StreamTokenizer.TT_EOF) {

          if (tok == StreamTokenizer.TT_WORD)

            ;// st.sval has token

        }

        br.close();

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

這個例子分解小寫單詞 (字母a-z)。若是你本身實現同等地功能,它可能像這樣:

import java.io.*;

  public class token2 {

    public static void main(String args[]) {

      if (args.length != 1) {

        System.err.println("missing filename");

        System.exit(1);

      }

      try {

        FileReader fr = new FileReader(args[0]);

        BufferedReader br = new BufferedReader(fr);

        int maxlen = 256;

        int currlen = 0;

        char wordbuf[] = new char[maxlen];

        int c;

        do {

          c = br.read();

          if (c >= 'a' && c <= 'z') {

            if (currlen == maxlen) {

              maxlen *= 1.5;

              char xbuf[] =

                  new char[maxlen];

              System.arraycopy(

                  wordbuf, 0,

                  xbuf, 0, currlen);

              wordbuf = xbuf;

            }

            wordbuf[currlen++] = (char)c;

          }

          else if (currlen > 0) {

            String s = new String(wordbuf,

                0, currlen);

          // do something with s

            currlen = 0;

          }

        } while (c != -1);

        br.close();

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

第二個程序比前一個運行快大約 20%,代價是寫一些微妙的底層代碼。

StreamTokenizer 是一種混合類,它從字符流(例如 BufferedReader)讀取, 可是同時以字節的形式操做,將全部的字符看成雙字節(大於 0xff) ,即便它們是字母字符。

串行化

串行化 以標準格式將任意的Java數據結構轉換爲字節流。例如,下面的程序輸出隨機整數數組:

  import java.io.*;

  import java.util.*;

  public class serial1 {

    public static void main(String args[]) {

      ArrayList al = new ArrayList();

      Random rn = new Random();

      final int N = 100000;

      for (int i = 1; i <= N; i++)

        al.add(new Integer(rn.nextInt()));

      try {

        FileOutputStream fos = new FileOutputStream("test.ser");

        BufferedOutputStream bos =  new BufferedOutputStream(fos);

        ObjectOutputStream oos =  new ObjectOutputStream(bos);

        oos.writeObject(al);

        oos.close();

      }

      catch (Throwable e) {

        System.err.println(e);

      }

    }

  }

 

而下面的程序讀回數組:

import java.io.*;

 import java.util.*;

  public class serial2 {

    public static void main(String args[]) {

      ArrayList al = null;

      try {

        FileInputStream fis = new FileInputStream("test.ser");

        BufferedInputStream bis = new BufferedInputStream(fis);

        ObjectInputStream ois = new ObjectInputStream(bis);

        al = (ArrayList)ois.readObject();

        ois.close();

      }

      catch (Throwable e) {

        System.err.println(e);

      }

    }

  }

注意咱們使用緩衝提升I/O操做的速度。

有比串行化更快的輸出大量數據而後讀回的方法嗎?可能沒有,除非在特殊的狀況下。例如,假設你決定將文本輸出爲64位的整數而不是一組8字節。做爲文本的長整數的最大長度是大約20個字符,或者說二進制表示的2.5倍長。這種格式看起來不會快。然而,在某些狀況下,例如位圖,一個特殊的格式多是一個改進。然而使用你本身的方案而不是串行化的標準方案將使你捲入一些權衡。

除了串行化實際的I/O和格式化開銷外(使用DataInputStream和 DataOutputStream), 還有其餘的開銷,例如在串行化恢復時的建立新對象的須要。

注意DataOutputStream 方法也能夠用於開發半自定義數據格式,例如:

import java.io.*;

  import java.util.*;

  public class binary1 {

    public static void main(String args[]) {

      try {

        FileOutputStream fos = new FileOutputStream("outdata");

        BufferedOutputStream bos = new BufferedOutputStream(fos);

        DataOutputStream dos = new DataOutputStream(bos);

        Random rn = new Random();

        final int N = 10;

        dos.writeInt(N);

        for (int i = 1; i <= N; i++) {

          int r = rn.nextInt();

          System.out.println(r);

          dos.writeInt(r);

        }

        dos.close();

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

 

和:

 import java.io.*;

  public class binary2 {

    public static void main(String args[]) {

      try {

        FileInputStream fis = new FileInputStream("outdata");

        BufferedInputStream bis = new BufferedInputStream(fis);

        DataInputStream dis = new DataInputStream(bis);

        int N = dis.readInt();

        for (int i = 1; i <= N; i++) {

          int r = dis.readInt();

          System.out.println(r);

        }

        dis.close();

      }

      catch (IOException e) {

        System.err.println(e);

      }

    }

  }

這些程序將10個整數寫入文件而後讀回它們

來自:http://blog.csdn.net/bulinner/archive/2006/05/09/715755.aspx 
相關文章
相關標籤/搜索