CodeGym /课程 /C# SELF /处理大文件的优化

处理大文件的优化

C# SELF
第 41 级 , 课程 4
可用

1. 介绍

当你处理小文件时,通常不会注意太多:写了、读了——就忘了。但一旦文件大小开始超过大约 100–500 MB,更别说几个 GB,就会出现有趣的问题:

  • 操作变慢,尤其是直接“一口气”处理时——比如 File.ReadAllBytes()File.WriteAllText() 可能会拖慢整个程序。
  • 可能会耗尽内存,出现 OutOfMemoryException
  • 系统会开始使用 swap(分页),这会导致整个系统变慢。
  • 并行操作可能会给磁盘造成过大负载。

在真实场景中这很常见:

  • 服务器日志(一天几个 GB)。
  • 处理大的 CSV 或 XML 文件的导入导出。
  • 视频、音频、压缩包、二进制文件的处理。
  • 复制大型程序集或备份。

2. 处理大文件的优化策略

在动手优化之前——先明确我们想要加速或改进什么。以下是最常见的目标:

  • “尽可能快地读/写文件,并且不搞崩溃系统”。
  • “分块处理文件,以免占满内存”。
  • “不在内存中制造多余的数据副本”。
  • “并行处理大文件(如果可行)”。

通用做法:

  • 流式读写:使用流和缓冲,按块读/写(FileStream, BufferedStream)。
  • 直接在磁盘上操作文件——不要在内存中做中间拷贝。
  • 谨慎管理缓冲和内存:不要把整个文件放到内存里。
  • 异步操作——如果需要不阻塞主线程(下一讲会详细讲)。

3. 流式读写:基本模式

核心原则:按块处理文件! 在现代 C# 中可以很容易地用 FileStreamBufferedStream 等流类来实现。


// 打开一个用于读取的流:
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 是对任意流的包装,可以让读/写不是按单字节,而是按块进行。

使用示例:


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 会加快速度,尤其当你以少量字节频繁读取时,而文件系统偏好块访问。

有趣的事实:在新版 .NET 的 FileStream 中已经内置了缓冲,所以手动包一层 BufferedStream 在处理“原始”流(比如网络流或非标准设备)时收益更明显。

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 天的旧日志归档,不是简单移动文件,而是把它们压缩到一个归档里。如果文件很大——就按块读写,避免“压垮”内存。

简化起见,这里用流拷贝的标准方式:


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 不适合——需要 NTFS/exFAT

对大文件的迭代处理


+------------------+
|    Start         |
+------------------+
        |
        v
+------------------------------+
| 打开读取流                    |
+------------------------------+
        |
        v
+------------------------------+
| 当未到达文件末尾             |
+------------------------------+
        |
        v
+---------------------------+
| 读取一块数据               |
+---------------------------+
        |
        v
+---------------------------+
| 处理该块                   |
+---------------------------+
        |
        v
+--------------------------+
| 下一个块                   |
+--------------------------+
        |
        v
+--------------------+
| 关闭流              |
+--------------------+

10. 有用的细节

把文件当作流: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:忘记关闭流。
如果用完不关闭流,文件句柄会被占用,影响其他程序并可能引发操作系统错误。始终使用 using——更安全、更干净。

错误 №4:同时访问同一个文件。
尝试在程序不同部分同时读写同一文件,很容易碰到 IOException。即便有时“能用”,也不可靠。

2
任务
C# SELF, 第 41 级, 课程 4
已锁定
大文件复制
大文件复制
1
调查/小测验
数据缓冲原理第 41 级,课程 4
不可用
数据缓冲原理
输入输出优化
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION