CodeGym /課程 /C# SELF /優化處理大型檔案的工作

優化處理大型檔案的工作

C# SELF
等級 41 , 課堂 4
開放

1. 介紹

處理小檔案時常常什麼都感覺不到:寫了、讀了 — 然後忘了。但一旦檔案大小開始超過至少 100–500 MB,更別說到 GB,會出現一些有趣的現象:

  • 操作變慢,特別是直接「硬上」處理時 — 比如 File.ReadAllBytes()File.WriteAllText() 可能會突然拖垮整個程式。
  • 可能會耗盡 RAM,出現 OutOfMemoryException
  • 系統開始使用 swap(分頁檔),這會導致整個系統變慢。
  • 並行操作可能會對磁碟造成過度負載。

在真實任務中這類情況很常見:

  • 伺服器日誌(一天幾 GB)。
  • 處理大型 CSV 或 XML 檔案的匯入/匯出。
  • 處理影片、音訊、壓縮檔、二進位檔案。
  • 複製大型組件或做備份。

2. 處理大型檔案的優化策略

在開始優化之前,先弄清楚你想加速或改善什麼。常見目標有:

  • 「儘可能快地讀/寫檔案,同時不把系統拉垮」。
  • 「分段處理檔案,避免佔滿記憶體」。
  • 「不要在記憶體裡生成多餘的資料副本」。
  • 「如果可能的話,並行處理大型檔案」。

通用做法:

  • 串流式讀寫:使用 stream 和 buffer,分段讀寫(FileStreamBufferedStream)。
  • 直接在磁碟上操作檔案 — 避免在記憶體中做中介複製。
  • 謹慎管理緩衝與記憶體:不要把整個檔案保存在 RAM 裡。
  • 非阻塞的 async 操作 — 如果你不想阻塞主執行緒(下一堂會詳談)。

3. 串流式讀寫:基本範式

核心原則:用分段的方式處理檔案! 在現代 C# 裡,使用 FileStreamBufferedStream 等類別很容易做到。


// 打開一個讀取用的 stream:
using FileStream fs = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024 * 1024]; // 1 MB
int bytesRead;

// 一直讀到檔案結尾
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
    // 在這裡處理讀到的資料!
    // 比如練習用:計算所有位元組的總和
    long sum = 0;
    for (int i = 0; i < bytesRead; i++)
        sum += buffer[i];

    Console.WriteLine($"已讀取 {bytesRead} 位元組,總和: {sum}");
}

建議:緩衝大小(例如 64 KB、128 KB、1 MB)應該透過實驗來調整。太小會導致大量磁碟存取;太大不一定有更好效能,卻會消耗更多記憶體。

為什麼這樣做?File.ReadAllBytes()File.ReadAllText() 這種不分段的讀法會試著把整個內容載入記憶體。檔案一大 — 結果很明顯:你會碰到 OutOfMemoryException

4. BufferedStream:為何與何時使用

我們之前的講座提過這個類別,這裡再提醒:BufferedStream 是任何 stream 的包裝器,可以讓讀/寫不是單位位元組而是塊狀操作。

使用範例:


using var fileStream = new FileStream("bigfile.bin", FileMode.Open, FileAccess.Read);
using var bufferedStream = new BufferedStream(fileStream, 1024 * 128);

byte[] buffer = new byte[1024 * 128];
int bytesRead;
while ((bytesRead = bufferedStream.Read(buffer, 0, buffer.Length)) > 0)
{
    // 處理資料
}

有時候用 BufferedStream 會提速,特別是你一次只讀幾個位元組,而檔案系統偏好區塊存取時。

有趣的事實:在新版的 FileStream 中已經內建了緩衝機制,所以手動包一層 BufferedStream 最有益的情境是操作「原始」的 stream,例如網路流或非標準裝置。

5. 讀寫大型文字檔案

二進位檔案比較直觀:分段讀寫。但文字檔,像大型 CSV、日誌、JSON 要怎麼做?

這時候可以用 StreamReader 來讀,StreamWriter 來寫。

逐行讀取:


using var reader = new StreamReader("biglog.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    // 處理這一行
    if (line.Contains("ERROR"))
        Console.WriteLine("發現 ERROR: " + line);
}

為什麼這樣好?

  • 我們不會把整個檔案保存在記憶體中。
  • 而且 StreamReader 內部的緩衝通常已經被調校得不錯。

逐行寫入:


using var writer = new StreamWriter("output.txt");
for (int i = 0; i < 1000000; i++)
    writer.WriteLine($"這是第 {i} 行");

串流式讀取的架構


   [磁碟上的檔案]
          |
     [FileStream]
          |
  [BufferedStream (可選)]
          |
   [StreamReader/StreamWriter (處理文字)]
          |
   [你的程式碼:處理資料]

6. 實作範例

我們繼續發展處理日誌的應用。假設要把 7 天以前的舊日誌壓縮成一個檔案。若某個日誌非常大,我們就分段讀寫,避免把記憶體「埋爆」。

簡化處理,可以用標準的 stream 複製範例:


void CopyLargeFile(string sourcePath, string destPath)
{
    using var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read);
    using var destStream = new FileStream(destPath, FileMode.Create, FileAccess.Write);
    byte[] buffer = new byte[1024 * 256]; // 256 KB
    int bytesRead;
    while ((bytesRead = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
    {
        destStream.Write(buffer, 0, bytesRead);
        // 可以在這裡加進度條!
    }
}

應用場景:

  • 備份檔案的複製
  • 按天或月合併日誌
  • 檔案的預處理(例如行過濾)

7. 如何評估緩衝的效能?

有時你會想知道:「到底快了多少?」可以用計時來比較:


var watch = System.Diagnostics.Stopwatch.StartNew();
CopyLargeFile("source.bin", "dest.bin");
watch.Stop();
Console.WriteLine($"複製時間: {watch.Elapsed.TotalSeconds} 秒");

常見的緩衝值:

  • 4 KB — 最小檔案系統區塊。
  • 64 KB / 128 KB — 實務上幾乎都表現良好。
  • 1 MB 或以上 — 只有在非常快速的 SSD 與超大型檔案時才有意義。

你可以自己試不同的緩衝大小!

8. 在大型檔案中搜尋與處理資料

如果不是單純複製,而是要搜尋特定行、數字或片語,分段處理仍然最有效。

範例:在龐大日誌中找出所有出現錯誤的行


using var reader = new StreamReader("server.log");
using var writer = new StreamWriter("errors.txt");
string? line;
while ((line = reader.ReadLine()) != null)
{
    if (line.Contains("ERROR"))
        writer.WriteLine(line); // 只把需要的行寫入結果
}

這種方式可以處理數 GB 甚至更多的日誌,而不會耗盡大量記憶體。

9. 處理超大型檔案(>2 GB)的注意事項

只要用串流式讀寫,.NET 與 Windows 對任何大小的檔案(甚至數十 TB)都能正常工作。但有些細節要注意!

  • 32 位元應用程式有 2 GB 位址空間限制 — 請使用 x64
  • 對於大檔案,請使用 64 位元平台(AnyCPUx64)。
  • 若檔案超過 4 GB,檔案系統 FAT32 不適合 — 要用 NTFSexFAT

對大型檔案做迭代處理


+------------------+
|    Start         |
+------------------+
        |
        v
+------------------------------+
| 打開讀取的 stream             |
+------------------------------+
        |
        v
+------------------------------+
| 當尚未達到檔案結尾            |
+------------------------------+
        |
        v
+---------------------------+
| 讀取一個資料區塊         |
+---------------------------+
        |
        v
+---------------------------+
| 處理該區塊                |
+---------------------------+
        |
        v
+--------------------------+
| 下一個區塊               |
+--------------------------+
        |
        v
+--------------------+
| 關閉 stream        |
+--------------------+

10. 有用的細節

把檔案當成 stream:FileStream 的常用方法

方法 說明
Read
把部分檔案讀到緩衝區
Write
把緩衝區的部分資料寫到檔案
Seek
跳到檔案中的指定位置
Length
檔案大小(位元組)
Position
檔案中的當前位置

使用 Seek() 的範例:


using var stream = new FileStream("bigfile.bin", FileMode.Open);
// 往前跳 1 GB!
stream.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);

byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
// 現在從檔案中間讀資料!

何時會用到?

  • 為檔案做索引
  • 快速存取特定區塊(例如大型資料庫中的區塊)

多執行緒處理檔案

如果任務允許,可以並行處理大型檔案:把檔案分成多個區塊,然後同時讀/寫不同的部分。但要明白 HDD 在隨機存取下會變慢,SSD 則快得多。

在一般家用情況下,如果不確定就順序處理。並行更適合同時處理多個檔案,而不是把單一檔案拆成多執行緒(否則效益可能有限)。

11. 處理大型檔案時常見錯誤

錯誤 #1:把整個檔案讀到記憶體。
新手常用 File.ReadAllBytes()File.ReadAllText() 去讀超大檔案。若檔案是 GB 級別,程式會直接崩潰 — 記憶體不夠。應該用串流式讀取。

錯誤 #2:使用過小的緩衝。
用超小的緩衝讀檔就像用茶匙喝湯。程式會花很多時間在磁碟存取上,效率很差。請選合理的緩衝大小。

錯誤 #3:忘記關閉 stream。
如果工作結束後不關閉 stream,檔案描述符會被佔用,會影響其他程式並可能導致 OS 層級的錯誤。一定要使用 using — 這樣比較安全也比較乾淨。

錯誤 #4:同時從多處存取同一個檔案。
嘗試同時從不同程式或不同區塊讀寫同一檔案通常會導致 IOException。即便有時「看起來能跑」,也很難保證穩定性。

2
任務
C# SELF, 等級 41, 課堂 4
上鎖
複製大型檔案
複製大型檔案
1
問卷/小測驗
資料緩衝原理,等級 41,課堂 4
未開放
資料緩衝原理
輸入輸出最佳化
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION