陷阱 - 在無緩衝的流上寫入小寫是低效的

請考慮以下程式碼將一個檔案複製到另一個檔案:

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new FileInputStream(args[0]);
             OutputStream os = new FileOutputStream(args[1])) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

(我們已經考慮省略了正常的引數檢查,錯誤報告等等,因為它們與此示例的要點無關。)

如果你編譯上面的程式碼並使用它來複制一個巨大的檔案,你會發現它非常慢。實際上,它至少比標準 OS 檔案複製實用程式慢幾個數量級。

在這裡新增實際的效能測量!

上面的示例很慢(在大檔案的情況下)的主要原因是它在無緩衝的位元組流上執行單位元組讀取和單位元組寫入。提高效能的簡單方法是使用緩衝流包裝流。例如:

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new BufferedInputStream(
                     new FileInputStream(args[0]));
             OutputStream os = new BufferedOutputStream(
                     new FileOutputStream(args[1]))) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

這些微小的變化將使資料複製速率提高至少幾個數量級,具體取決於各種平臺相關因素。緩衝流包裝器使資料以更大的塊讀取和寫入。例項都將緩衝區實現為位元組陣列。

  • 使用 is,資料一次從檔案讀入緩衝區幾千位元組。當呼叫 read() 時,實現通常會從緩衝區返回一個位元組。如果緩衝區已清空,它將僅從基礎輸入流中讀取。

  • os 的行為是類似的。呼叫 os.write(int) 將單個位元組寫入緩衝區。僅當緩衝區已滿或者重新整理或關閉 os 時,才會將資料寫入輸出流。

基於字元的流怎麼樣?

你應該知道,Java I / O 提供了不同的 API 來讀取和寫入二進位制和文字資料。

  • InputStreamOutputStream 是基於流的二進位制 I / O 的基本 API
  • ReaderWriter 是基於流的文字 I / O 的基本 API。

對於文字 I / O,BufferedReaderBufferedWriterBufferedInputStreamBufferedOutputStream 的等價物。

為什麼緩衝流會產生這麼大的差異?

緩衝流有助於提高效能的真正原因是應用程式與作業系統對話的方式:

  • Java 應用程式中的 Java 方法或 JVM 的本機執行時庫中的本機過程呼叫很快。它們通常採用幾條機器指令,對效能影響最小。

  • 相比之下,對作業系統的 JVM 執行時呼叫並不快。它們涉及一種稱為系統呼叫的東西。系統呼叫的典型模式如下:

    1. 將 syscall 引數放入暫存器。
    2. 執行 SYSENTER 陷阱指令。
    3. 陷阱處理程式切換到特權狀態並更改虛擬記憶體對映。然後它排程到程式碼來處理特定的系統呼叫。
    4. 系統呼叫處理程式檢查引數,注意它沒有被告知訪問使用者程序不應該看到的記憶體。
    5. 執行系統呼叫特定的工作。在 read 系統呼叫的情況下,這可能涉及:
      1. 檢查在檔案描述符的當前位置是否有要讀取的資料
      2. 呼叫檔案系統處理程式從磁碟(或儲存的任何位置)獲取所需資料到緩衝區快取中,
      3. 將資料從緩衝區快取記憶體複製到 JVM 提供的地址
      4. 調整 thstream 指標檔案描述符位置
    6. 從系統呼叫返回。這需要再次更改 VM 對映並切換到特權狀態。

可以想象,執行單個系統呼叫可以獲得數千條機器指令。保守地說,比常規方法呼叫至少長兩個數量級。 (可能是三個或更多。)

鑑於此,緩衝流產生重大影響的原因是它們大大減少了系統呼叫的數量。緩衝輸入流不是為每個 read() 呼叫執行系統呼叫,而是根據需要將大量資料讀入緩衝區。大多數對緩衝流的 read() 呼叫都會進行一些簡單的邊界檢查並返回之前讀過的 byte。類似的推理適用於輸出流情況,也適用於字元流情況。

(有些人認為緩衝的 I / O 效能來自於讀取請求大小與磁碟塊大小,磁碟旋轉延遲等等之間的不匹配。實際上,現代作業系統使用了許多策略來確保應用程式通常不需要等待磁碟。這不是真正的解釋。)

緩衝流總是贏嗎?

不總是。如果你的應用程式要進行大量讀取或寫入,緩衝流肯定是一個勝利。但是,如果你的應用程式只需要對大型的 byte[]char[] 執行大量讀取或寫入操作,那麼緩衝流將不會給你帶來任何實際好處。實際上甚至可能存在(微小的)效能損失。

這是用 Java 複製檔案的最快方法嗎?

不,不是。當你使用 Java 的基於流的 API 來複制檔案時,會產生至少一個額外的記憶體到記憶體的資料副本的成本。如果你使用 NIO ByteBufferChannel API,可以避免這種情況。 ( 在此處新增指向單獨示例的連結。