Struct và class trong C#: Ai nhanh hơn

Trong bài viết này, tôi sẽ so sánh structclass trong C#.

Hiểu sự khác biệt về hiệu suất giữa hai loại này sẽ giúp bạn có sự lựa chọn đúng đắn cho mọi trường hợp.

Ví dụ

Hãy xem đoạn code sau đây:

public class PointClass
{
    public int X { get; set; }
    public int Y { get; set; }
    public PointClass(int x, int y)
    {
        X = x;
        Y = y;
    }
}

public class PointClassFinalized : PointClass
{
    public PointClassFinalized(int x, int y) : base(x, y)
    {
    }
    ~PointClassFinalized()
    {
        // added a finalizer to slow down the GC

    }
}

public struct PointStruct
{
    public int X { get; set; }
    public int Y { get; set; }
    public PointStruct(int x, int y)
    {
        X = x;
        Y = y;
    }
}

public class StructsTest : PerformanceTest
{
    protected override bool MeasureTestA()
    {
        // access array elements
        var list = new PointClassFinalized[Iterations];
        for (int i = 0; i < Iterations; i++)
        {
            list[i] = new PointClassFinalized(i, i);
        }
        return true;
    }

    protected override bool MeasureTestB()
    {
        // access array elements
        var list = new PointClass[Iterations];
        for (int i = 0; i < Iterations; i++)
        {
            list[i] = new PointClass(i, i);
        }
        return true;
    }

    protected override bool MeasureTestC()
    {
        // access array elements
        var list = new PointStruct[Iterations];
        for (int i = 0; i < Iterations; i++)
        {
            list[i] = new PointStruct(i, i);
        }
        return true;
    }
}

Ở đoạn code ở trên có class PointClass và struct PointStruct, cả hai đều chứa các thuộc tính X và Y có kiểu int. Và cũng có một class PointClassFinalized với một phương thức hủy.

Phương thức MeasureTestA khởi tạo một mảng có 1.000.000 thể hiện của class PointClassFinalized.

Phương thức MeasureTestB cũng tương tự, nhưng nó khởi tạo một mảng có 1.000.000 thể hiện của class PointClass.

Cuối cùng là phương thức MeasureTestC khởi tạo một mảng có 1.000.000 thể hiện của struct PointStruct .

Bạn nghĩ phương thức nào sẽ chạy xong nhanh nhất?

Kết quả benchmark

Hãy cùng tìm hiểu:

Bạn có mong đợi điều đó?

Bây giờ hãy tập trung vào phương thức MeasureTestBMeasureTestC. Sự khác biệt duy nhất giữa hai phương thức này là một phương thức khởi tạo mảng cho thể hiện của lớp và phương thức kia khởi tạo mảng của struct.

Phương thức MeasureTestC khởi tạo mảng của struct và chạy chỉ trong 17 mili giây nhanh hơn 8,6 lần so với phương thức MeasureTestB khởi tạo mảng cho lớp.

Đó là một sự khác biệt!

Vậy điều gì đang xảy ra ở đây?

Giải thích kết quả benchmark

Sự khác biệt là do cách struct và class được lưu trữ trong bộ nhớ. Đây là cách phân bổ bộ nhớ cho danh sách các thể hiện của class PointClass:

Struct và class trong C#: Ai nhanh hơn

Danh sách là một biến cục bộ, vì vậy nó được lưu trữ trên bộ nhớ stack. Nó tham chiếu đến một mảng các thể hiện của class PointClass trên bộ nhớ heap.

Nhưng đây là điểm mấu chốt: class PointClass là một kiểu tham chiếu (reference type), vì vậy nó được lưu trữ ở nơi khác trên bộ nhớ heap. Danh sách chỉ duy trì một mảng các tham chiếu đối tượng trỏ đến các thể hiện của class PointClass được lưu trữ ở nơi khác trên bộ nhớ heap.

Lưu ý các mũi tên màu cam trong sơ đồ. Đây là các phần tử mảng tham chiếu đến các đối tượng ở nơi khác trên bộ nhớ heap.

Để lấp đầy mảng với các đối tượng, phương thức MeasureTestB phải phân bổ 1.000.000 đối tượng trên bộ nhớ heap và lưu trữ các tham chiếu của chúng trong mảng. Đó là rất nhiều công việc!

Khi bạn truy cập một phần tử mảng cụ thể, trình thực thi .NET cần truy xuất tham chiếu đối tượng và sau đó 'theo dõi' tham chiếu đó để nhận được thể hiện của PointClass.

Và khi kết thúc thực thi phương thức MeasureTestB, trình thu gom rác .NET phải hủy mọi thể hiện của PointClass để lấy lại bộ nhớ.

Lớp PointClassFinalized trong phương thức MeasureTestA có một phương thức hủy thủ công giúp làm chậm quá trình này hơn nữa.

.NET Framework chạy tất cả các trình hủy trên một luồng duy nhất, vì vậy bây giờ luồng đó phải xử lý 1.000.000 đối tượng lần lượt trước khi trình thu gom rác có thể lấy lại bộ nhớ.

Và bạn có thể thấy rõ điều này trong kết quả benchmark. Phương thức MeasureTestA chậm hơn MeasureTestB tới 1,7 lần .

Bây giờ so sánh điều này với cách cấp phát bộ nhớ của danh sách các thể hiện của struct PoinstStruct:

Struct và class trong C#: Ai nhanh hơn

Struct là kiểu giá trị (value type), có nghĩa là chúng được lưu trữ nội bộ bên trong kiểu dữ liệu chứa của chúng. Vì vậy, bây giờ tất cả các thể hiện của PointStruct được lưu trữ ngay bên trong mảng. Chỉ có một đối tượng duy nhất trên ở trên bộ nhớ heap.

Để khởi tạo mảng, trình thực thi .NET giờ đây có thể ghi trực tiếp các giá trị X và Y vào các phần tử mảng chính xác. Không cần phải phân bổ các đối tượng mới trên bộ nhớ heap và lưu trữ các tham chiếu của chúng.

Và khi bạn truy cập một phần tử mảng cụ thể, trình thực thi .NET có thể truy xuất trực tiếp struct vì nó được lưu trữ ngay tại đó, bên trong mảng.

khi kết thúc thực thi phương thức MeasureTestC, trình thu gom rác .NET giờ chỉ cần loại bỏ một đối tượng duy nhất.

Tất cả những điều này gộp lại chính là lý do tại sao phương thức MeasureTestC là nhanh nhất.

Vì vậy, điều đó có nghĩa là bạn nên luôn sử dụng một struct?

Trời ơi không! Đây là những gì bạn cần làm:

  • Khi bạn lưu trữ hơn 30–40 byte dữ liệu, hãy sử dụng class.
  • Khi bạn lưu trữ các kiểu tham chiếu, hãy sử dụng class.
  • Khi bạn lưu trữ lên đến vài nghìn thể hiện, hãy sử dụng class.
  • Khi danh sách của bạn tồn tại lâu dài, hãy sử dụng lớp.
  • Trong tất cả các trường hợp khác, hãy sử dụng struct.
Lập Trình C#Lập Trình C# Cơ Bản
Bài Viết Liên Quan:
int[] và int[,] trong C#: Ai nhanh hơn
Trung Nguyen 10/10/2020
int[] và int[,] trong C#: Ai nhanh hơn

Hiểu được sự khác biệt giữa các loại mảng trong C# sẽ giúp bạn chọn cấu trúc dữ liệu chính xác cho mọi trường hợp.

Best practice cho performance trong C#
Trung Nguyen 03/10/2020
Best practice cho performance trong C#

Mục tiêu của bài viết này là cung cấp một danh sách không đầy đủ các code mẫu cần tránh, vì chúng rủi ro hoặc performance kém.

Đọc ghi file (File I/O) trong C#
Trung Nguyen 26/04/2020
Đọc ghi file (File I/O) trong C#

Hướng dẫn này sẽ giúp bạn tìm hiểu về đọc ghi file (File I/O) trong C# và sử dụng các lớp tiện ích để đọc ghi file.

Reflection trong C#
Trung Nguyen 19/04/2020
Reflection trong C#

Reflection trong C# là gì? Ứng dụng của Reflection trong C#. Cách khai báo và sử dụng Reflection trong C#.