CodeGym /课程 /C# SELF /性能和手动缓冲区管理

性能和手动缓冲区管理

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

1. 引言

为什么需要缓冲以及性能对比

我们已经明白,缓冲是一种把数据分成更大的“包”来处理的策略,这样访问磁盘不是成千上万次,而是更少次数,每次传输更多数据。对于大数据量来说,这能带来显著的性能提升。

什么时候该考虑 IO 性能

  • 如果你在处理大文件(GB、TB——比如公司多年来累积的日志集合)。
  • 如果需要最低延迟响应(比如实时日志处理)。
  • 如果对文件的访问次数非常多(比如批量重命名/复制图片档案)。
  • 在面试中想证明你懂得现代优化方法(并且喜欢测速度,而不是只“让它能跑”)。

在 .NET 中,大多数文件流默认已经有缓冲,但有时需要微调或者采用特殊策略。

主要“选手”——有哪些缓冲

默认缓冲 能否改大小 适用场景
FileStream
有(4096 字节) 是(通过构造函数) 基础文件流
BufferedStream
有(4096 字节) 是(通过构造函数) 包装在其他流上的“包装器”
StreamReader/Writer
有(1024/1024 字节) 是(构造函数) 处理文本
  • BufferedStream 可以把另一个流“装一层”,以提升性能(比如基础流缓冲不好,或者你需要更大的缓冲)。
  • 缓冲大小是在速度和内存占用之间的折中。

2. 示例:

我们来比较三种复制大文件的方法:

  1. 不使用缓冲——每次一个字节(糟糕的示例,只是为了说明)
  2. 默认缓冲——标准的 FileStreamCopyTo
  3. 手动管理缓冲——自己提供缓冲,优化大小

为了试验,写一个简单的文件复制工具,放进你的应用。假设文件叫做 BigFile.bin

class FileCopyBenchmarks
{
    // 按单字节复制(反面例子 — 不要这样做!)
    public static void CopyOneByte(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        int b;
        while ((b = input.ReadByte()) != -1)
        {
            output.WriteByte((byte)b);
        }
    }

    // 使用 FileStream 的默认缓冲复制
    public static void CopyWithDefaultBuffer(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        input.CopyTo(output); // 使用内部缓冲(通常是 81920 字节)
    }

    // 使用自定义缓冲手动复制
    public static void CopyWithCustomBuffer(string source, string dest, int bufferSize = 1024 * 1024)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            output.Write(buffer, 0, bytesRead);
        }
    }
}

哪个函数更快?我们来测下执行时间。

如何正确测量性能

在 .NET 中测时间最简单的是用 Stopwatch

static void Measure(Action action, string description)
{
    var sw = Stopwatch.StartNew();
    action();
    sw.Stop();
    Console.WriteLine($"{description}: {sw.ElapsedMilliseconds} 毫秒");
}

现在用不同方法复制同一个文件:

string source = "BigFile.bin";
string dest1 = "copy1.bin";
string dest2 = "copy2.bin";
string dest3 = "copy3.bin";

// 事先创建 BigFile.bin(比如 100-500 MB),或使用任何大的文件。

Measure(() => FileCopyBenchmarks.CopyOneByte(source, dest1), "CopyOneByte (每次 1 字节)");
Measure(() => FileCopyBenchmarks.CopyWithDefaultBuffer(source, dest2), "CopyWithDefaultBuffer (默认)");
Measure(() => FileCopyBenchmarks.CopyWithCustomBuffer(source, dest3, 1024 * 1024), "CopyWithCustomBuffer (1 MB)");

危险点和坑

  • 如果连续多次运行,操作系统缓存可能会把磁盘“预热”,后续测量会更快——要真实评估,最好重启程序并清空缓存。
  • 如果文件很小(10–20 KB),缓冲的优势看不出来——文件越大,差距越明显。
  • 如果把缓冲设得太大(比如 100 MB),内存占用会暴涨,影响整个系统。

结果可视化:表格

方法 500 MB 文件时耗时(毫秒)
每次一个字节 100 000+
标准 FileStream / CopyTo 1 000 — 5 000
手动缓冲 1 MB 700 — 1 200

这些数字是大概值,但趋势明显——缓冲越大,磁盘访问次数越少,速度越快。

3. 手动缓冲管理的结构

为什么有时还想手动调缓冲大小?一个简单比喻:你搬家。可以一次搬一只杯子,也可以一次搬一个大箱子。但箱子也有极限,不然你搬不动!

手动缓冲读取是怎么工作的

// 手动控制缓冲大小示例
int bufferSize = 1024 * 1024; // 1 MB
byte[] buffer = new byte[bufferSize];
int read;
while ((read = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
    outputStream.Write(buffer, 0, read);
}
  • Read 方法会尽量填满整个缓冲,但如果文件结束可能会返回较少的字节数。
  • 缓冲大小通常在 32 KB 到 48 MB 之间选择——再大通常提升有限。
  • 别忘了内存使用,尤其当你有很多并发操作或线程时。

尝试不同缓冲大小

在示例里改变缓冲大小(32 KB、128 KB、1 MB、4 MB),看看性能峰值在哪里。通常“黄金中间值”大约是 1 MB。

什么时候手动缓冲更有用

  • 需要控制内存占用(比如程序跑在弱鸡服务器上)。
  • 当流本身没有自动缓冲(比如 NetworkStream 或自定义流)。
  • 处理大量并行操作时——可以给每个任务分配合适大小的缓冲。
  • 想把非常大的文件处理得尽可能快(比如转换超大的 CSV)。

4. 最佳实践和常见错误

能不能把缓冲做得巨大无比?理论上能,但没必要。超大缓冲有时反而降速:内存白白占着,系统缓存策略可能受影响。

手动缓冲并不能万能提速。对于小文件或者已经被优化过的流(比如具有大内部缓冲的 FileStream)收益有限,而且代码会更复杂。

常见陷阱:忘记关闭流或者不处理异常,会导致文件被锁。操作文件时用 using 和错误处理(try-catch)。

2
任务
C# SELF, 第 41 级, 课程 3
已锁定
使用手动缓冲区复制文件
使用手动缓冲区复制文件
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION