CodeGym /Các khóa học /C# SELF /Hiệu năng và quản lý bộ đệm thủ công

Hiệu năng và quản lý bộ đệm thủ công

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Tại sao cần buffering và so sánh hiệu năng

Chúng ta đã biết buffering — đó là chiến lược gom dữ liệu thành các "mẻ" lớn để không phải truy xuất đĩa hàng trăm ngàn lần, mà chỉ thỉnh thoảng, gửi lượng lớn dữ liệu cùng lúc. Điều này tăng đáng kể hiệu năng, nhất là với lượng dữ liệu lớn.

Khi nào nên nghĩ tới hiệu năng I/O

  • Nếu bạn làm việc với file lớn (GB, TB — ví dụ kho logs nhiều năm của công ty bạn).
  • Nếu cần thời gian phản hồi thấp (ví dụ xử lý log real-time).
  • Nếu số lần truy xuất file rất lớn (ví dụ đổi tên/nhân bản hàng loạt ảnh trong archive).
  • Để khoe ở phỏng vấn rằng bạn hiểu cách tối ưu hiện đại (và thích đo tốc độ thay vì "chạy được là được").

Trong .NET hầu hết file streams đã có buffering mặc định, nhưng đôi khi cần tinh chỉnh kỹ hoặc chiến lược đặc biệt.

Những "người chơi" chính — các loại buffer

Lớp Buffer mặc định Có thể đổi kích thước không Áp dụng cho
FileStream
Có (4096 byte) Có (qua constructor) File stream cơ bản
BufferedStream
Có (4096 byte) Có (qua constructor) "Wrapper" quanh một stream
StreamReader/Writer
Có (1024/1024 byte) Có (constructor) Làm việc với text
  • BufferedStream có thể "gói" một stream khác để cải thiện hiệu năng (ví dụ khi stream gốc buffer kém hoặc cần buffer lớn hơn).
  • Kích thước buffer là sự đánh đổi giữa tốc độ và tiêu thụ RAM.

2. Ví dụ:

Ta sẽ so sánh ba cách để copy một file lớn:

  1. Không dùng buffer — từng byte một (tệ, nhưng minh họa rõ!)
  2. Buffer mặc địnhFileStream chuẩn với CopyTo
  3. Quản lý buffer thủ công — tự cung cấp buffer, tối ưu kích thước

Để thử nghiệm tạo một utility đơn giản để copy file, thêm vào app. Giả sử file tên là BigFile.bin.

class FileCopyBenchmarks
{
    // Sao chép từng byte một (ví dụ xấu — đừng làm thế trong thực tế!)
    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);
        }
    }

    // Sao chép bằng buffer mặc định của 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); // Dùng internal buffer (thường là 81920 byte)
    }

    // Sao chép với điều khiển buffer thủ công
    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);
        }
    }
}

Hàm nào nhanh hơn? Ta đo thời gian thực thi.

Cách đo hiệu năng cho đúng

Trong .NET đơn giản nhất là dùng Stopwatch:

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

Bây giờ thử copy cùng một file bằng các cách khác nhau:

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

// Tạo sẵn file BigFile.bin (ví dụ 100-500 MB) hoặc dùng file lớn bất kỳ.

Measure(() => FileCopyBenchmarks.CopyOneByte(source, dest1), "CopyOneByte (từng 1 byte)");
Measure(() => FileCopyBenchmarks.CopyWithDefaultBuffer(source, dest2), "CopyWithDefaultBuffer (mặc định)");
Measure(() => FileCopyBenchmarks.CopyWithCustomBuffer(source, dest3, 1024 * 1024), "CopyWithCustomBuffer (1 MB)");

Những điểm nguy hiểm và bẫy

  • Nếu chạy liên tiếp nhiều lần, cache của OS có thể "làm nóng" đĩa, các lần đo sau sẽ nhanh hơn — để đánh giá thật, hãy restart chương trình và xóa cache.
  • Nếu file nhỏ (10–20 KB), lợi ích của buffering sẽ không rõ — file càng lớn, khác biệt càng lớn.
  • Nếu đặt buffer quá to (ví dụ 100 MB), tiêu thụ RAM có thể tăng vọt và hệ thống sẽ gặp vấn đề.

Trực quan hoá kết quả: bảng

Phương pháp Thời gian (ms) với file 500 MB
Từng 1 byte 100 000+
FileStream chuẩn / CopyTo 1 000 — 5 000
Buffer thủ công 1 MB 700 — 1 200

Số liệu mang tính minh họa, nhưng xu hướng rõ ràng — buffer lớn hơn giảm số lần truy xuất đĩa và tăng tốc.

3. Giải phẫu việc quản lý buffer thủ công

Tại sao đôi khi vẫn muốn chỉnh kích thước buffer? So sánh đơn giản: bạn chuyển nhà. Có thể mang từng cái 1 chén, hoặc xách một thùng to. Nhưng thùng cũng có giới hạn, nếu quá to sẽ không nhấc lên được!

Cách đọc với buffer thủ công hoạt động

// Ví dụ quản lý kích thước buffer thủ công
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);
}
  • Phương thức Read cố gắng lấp đầy buffer, nhưng có thể trả về ít hơn nếu file kết thúc.
  • Kích thước buffer thường chọn trong khoảng 32 KB đến 48 MB — lớn hơn hầu như không đem lại cải thiện thêm.
  • Đừng quên RAM, nhất là khi có nhiều thao tác hoặc nhiều thread cùng dùng buffer.

Thử nghiệm với kích thước buffer

Thử thay đổi kích thước buffer trong ví dụ ( 32 KB, 128 KB, 1 MB, 4 MB) và quan sát điểm đạt peak hiệu năng. Thường "vàng" rơi vào khoảng ~1 MB.

Tình huống khi buffer thủ công có lợi

  • Khi cần kiểm soát RAM dùng (ví dụ chạy trên server yếu).
  • Khi stream không được buffer tự động (NetworkStream, custom streams).
  • Khi có nhiều thao tác song song — có thể cấp buffer riêng từng thread với kích thước tối ưu.
  • Khi muốn tối đa hoá tốc độ xử lý file rất lớn (ví dụ chuyển đổi một CSV lớn).

4. Best practices và lỗi thường gặp

Có nên làm buffer thật to cho nhanh không? Có thể, nhưng tại sao phải thế? Buffer quá lớn đôi khi còn làm chậm: phần RAM để không, hệ thống gặp vấn đề với cache, swap, v.v.

Buffer thủ công không tự động tăng tốc mọi thứ. Với file nhỏ hoặc stream đã tối ưu (ví dụ FileStream đã có internal buffer lớn) lợi ích sẽ rất nhỏ, trong khi code lại phức tạp hơn.

Bẫy điển hình: quên đóng stream hoặc không xử lý exception — file sẽ bị khoá. Hãy dùng using và xử lý lỗi bằng try-catch khi làm việc với file.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION