Cấu trúc so với lớp


93

Tôi sắp tạo 100.000 đối tượng bằng mã. Chúng là những cái nhỏ, chỉ có 2 hoặc 3 thuộc tính. Tôi sẽ đặt chúng trong một danh sách chung và khi có chúng, tôi sẽ lặp lại chúng và kiểm tra giá trị avà có thể cập nhật giá trị b.

Việc tạo các đối tượng này dưới dạng lớp hoặc dạng cấu trúc nhanh hơn / tốt hơn?

BIÊN TẬP

a. Các thuộc tính là các kiểu giá trị (ngoại trừ chuỗi tôi nghĩ?)

b. Họ có thể (chúng tôi chưa chắc chắn) có một phương thức xác thực

CHỈNH SỬA 2

Tôi đã tự hỏi: các đối tượng trên heap và stack được xử lý như nhau bởi bộ thu gom rác, hay nó hoạt động khác nhau?


2
Họ sẽ chỉ có các trường công khai, hay họ cũng sẽ có các phương thức? Các kiểu có phải là kiểu nguyên thủy, chẳng hạn như số nguyên không? Chúng sẽ được chứa trong một mảng hay trong một cái gì đó như List <T>?
JeffFerguson

14
Danh sách các cấu trúc có thể thay đổi? Cẩn thận với vận tốc.
Anthony Pegram

1
@Anthony: tôi sợ rằng tôi đang bỏ lỡ trò đùa vận tốc: -s
Michel

5
Trò đùa vận tốc là từ XKCD. Nhưng khi bạn đang ném xung quanh 'kiểu giá trị được cấp phát trên stack' quan niệm sai lầm / chi tiết thi hành (xóa như được áp dụng) thì đó là Eric Lippert bạn cần phải coi chừng ...
Greg Beech

Câu trả lời:


137

Việc tạo các đối tượng này dưới dạng lớp hoặc dạng cấu trúc có nhanh hơn không?

Bạn là người duy nhất có thể xác định câu trả lời cho câu hỏi đó. Hãy thử cả hai cách, đo lường chỉ số hiệu suất có ý nghĩa, tập trung vào người dùng, có liên quan và sau đó bạn sẽ biết liệu thay đổi có ảnh hưởng có ý nghĩa đến người dùng thực trong các tình huống có liên quan hay không.

Các cấu trúc sử dụng ít bộ nhớ heap hơn (vì chúng nhỏ hơn và dễ dàng thu gọn hơn, không phải vì chúng nằm "trên ngăn xếp"). Nhưng chúng mất nhiều thời gian để sao chép hơn một bản sao tham chiếu. Tôi không biết chỉ số hiệu suất của bạn về tốc độ hoặc mức sử dụng bộ nhớ; có một sự cân bằng ở đây và bạn là người biết nó là gì.

Là nó tốt hơn để tạo ra các đối tượng như lớp hoặc như struct?

Có thể là class, có thể là struct. Theo nguyên tắc chung: Nếu đối tượng là:
1. Nhỏ
2. Về mặt logic là một giá trị bất biến
3. Có rất nhiều trong số chúng
Vậy thì tôi sẽ xem xét việc biến nó thành một cấu trúc. Nếu không, tôi sẽ gắn bó với một loại tham chiếu.

Nếu bạn cần thay đổi một số trường của một cấu trúc, tốt hơn là bạn nên xây dựng một phương thức khởi tạo trả về một cấu trúc hoàn toàn mới với trường được đặt chính xác. Điều đó có lẽ hơi chậm hơn (đo lường nó!) Nhưng về mặt logic thì dễ dàng hơn để suy luận.

Các đối tượng trên heap và stack có được bộ thu gom rác xử lý như nhau không?

Không , chúng không giống nhau vì các đối tượng trên ngăn xếp là gốc rễ của tập hợp . Người thu gom rác không cần phải hỏi "thứ này trên đống rác có còn sống không?" bởi vì câu trả lời cho câu hỏi đó luôn là "Có, nó ở trên ngăn xếp". (Bây giờ, bạn không thể dựa vào đó để giữ cho một đối tượng tồn tại bởi vì ngăn xếp là một chi tiết triển khai. Jitter được phép giới thiệu các tối ưu hóa, chẳng hạn như đăng ký những gì thường sẽ là giá trị ngăn xếp và sau đó nó không bao giờ nằm ​​trên ngăn xếp vì vậy GC không biết rằng nó vẫn còn sống. Một đối tượng đã đăng ký có thể bị thu thập dữ liệu con của nó, ngay sau khi sổ đăng ký giữ nó sẽ không được đọc lại.)

Tuy nhiên, thu gom rác không phải đối xử với các đối tượng trên stack như còn sống, cùng một cách mà nó đối xử với bất kỳ đối tượng biết đến là còn sống là còn sống. Đối tượng trên ngăn xếp có thể tham chiếu đến các đối tượng được cấp phát đống cần được duy trì tồn tại, vì vậy GC phải xử lý các đối tượng ngăn xếp giống như các đối tượng được cấp phát đống còn sống nhằm mục đích xác định tập hợp sống. Nhưng rõ ràng chúng không được coi là "vật thể sống" cho mục đích thu gọn đống, vì ngay từ đầu chúng không nằm trên đống.

Rõ chưa?


Eric, bạn có biết nếu trình biên dịch hoặc jitter sử dụng tính bất biến (có lẽ nếu được thực thi với readonly) để cho phép tối ưu hóa. Tôi sẽ không để điều đó ảnh hưởng đến lựa chọn về khả năng thay đổi (tôi là một người hiểu chi tiết về hiệu quả trên lý thuyết, nhưng trên thực tế, động thái đầu tiên của tôi đối với hiệu quả là luôn cố gắng đảm bảo tính đúng đắn nhất có thể và do đó không cần phải lãng phí chu kỳ CPU và chu kỳ não trên các lần kiểm tra và các trường hợp biên, và có thể thay đổi hoặc bất biến phù hợp sẽ giúp ích ở đó), nhưng nó sẽ chống lại bất kỳ phản ứng đầu gối nào đối với câu nói của bạn về tính bất biến có thể chậm hơn.
Jon Hanna

@Jon: Trình biên dịch C # tối ưu hóa dữ liệu const nhưng không phải dữ liệu chỉ đọc . Tôi không biết liệu trình biên dịch jit có thực hiện bất kỳ tối ưu hóa bộ nhớ đệm nào trên các trường chỉ đọc hay không.
Eric Lippert

Rất tiếc, như tôi biết kiến ​​thức về tính bất biến cho phép một số tối ưu hóa, nhưng đã đạt đến giới hạn kiến ​​thức lý thuyết của tôi tại thời điểm đó, nhưng chúng là giới hạn mà tôi muốn kéo dài. Trong khi đó "nó có thể được nhanh hơn cả hai phương diện, đây là lý do tại sao, bây giờ thử nghiệm và tìm ra được áp dụng trong trường hợp này" rất hữu ích để có thể nói :)
Jon Hanna

Tôi muốn giới thiệu để đọc simple-talk.com/dotnet/.net-framework/... và bài viết của riêng bạn (@ Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/... để bắt đầu lặn vào chi tiết. Xung quanh còn rất nhiều bài viết hay khác. BTW, sự khác biệt trong việc xử lý 100 000 đối tượng nhỏ trong bộ nhớ hầu như không đáng chú ý vì có một số chi phí bộ nhớ (~ 2,3 MB) cho lớp. Nó có thể được dễ dàng kiểm tra bằng thử nghiệm đơn giản.
Nick Martyshchenko

Vâng, điều đó rõ ràng. Cảm ơn rất nhiều về câu trả lời đầy đủ của bạn (bao quát thì tốt hơn? Google dịch đã cung cấp 2 bản dịch. Tôi muốn nói rằng bạn đã dành thời gian để không viết một câu trả lời ngắn gọn, nhưng cũng dành thời gian để viết tất cả chi tiết).
Michel

23

Đôi khi structbạn không cần gọi hàm tạo new () và chỉ định trực tiếp các trường làm cho nó nhanh hơn nhiều so với bình thường.

Thí dụ:

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i].id = i;
    list[i].isValid = true;
}

nhanh hơn khoảng 2 đến 3 lần

Value[] list = new Value[N];
for (int i = 0; i < N; i++)
{
    list[i] = new Value(i, true);
}

ở đâu Valuelà a structvới hai trường ( idisValid).

struct Value
{
    int id;
    bool isValid;

    public Value(int i, bool isValid)
    {
        this.i = i;
        this.isValid = isValid;
    }
}

Mặt khác là các mục cần được di chuyển hoặc các loại giá trị được chọn, tất cả những gì mà việc sao chép sẽ làm bạn chậm lại. Để có câu trả lời chính xác, tôi nghi ngờ bạn phải lập hồ sơ mã của mình và kiểm tra nó.


Rõ ràng là mọi thứ sẽ nhanh hơn rất nhiều khi bạn cũng thống nhất các giá trị vượt qua ranh giới bản địa.
leppie

Tôi khuyên bạn nên sử dụng một tên khác list, vì mã được chỉ định sẽ không hoạt động với a List<Value>.
supercat

7

Các cấu trúc có vẻ giống với các lớp, nhưng có những điểm khác biệt quan trọng mà bạn cần lưu ý. Trước hết, các lớp là kiểu tham chiếu và cấu trúc là kiểu giá trị. Bằng cách sử dụng các cấu trúc, bạn có thể tạo các đối tượng hoạt động giống như các kiểu tích hợp sẵn và tận hưởng các lợi ích của chúng.

Khi bạn gọi toán tử Mới trên một lớp, nó sẽ được cấp phát trên heap. Tuy nhiên, khi bạn khởi tạo một cấu trúc, cấu trúc đó sẽ được tạo trên ngăn xếp. Điều này sẽ mang lại hiệu suất tăng. Ngoài ra, bạn sẽ không xử lý các tham chiếu đến một phiên bản của cấu trúc như bạn làm với các lớp. Bạn sẽ làm việc trực tiếp với thể hiện struct. Do đó, khi truyền một cấu trúc cho một phương thức, nó sẽ được truyền theo giá trị thay vì dưới dạng một tham chiếu.

Thêm ở đây:

http://msdn.microsoft.com/en-us/library/aa288471(VS.71).aspx


4
Tôi biết nó nói điều đó trên MSDN, nhưng MSDN không kể toàn bộ câu chuyện. Stack so với heap là một chi tiết triển khai và các cấu trúc không phải lúc nào cũng có trên ngăn xếp. Đối với chỉ một blog gần đây về điều này, hãy xem: blog.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram

"... nó được truyền bởi giá trị ..." cả tham chiếu và cấu trúc đều được truyền theo giá trị (trừ khi một người sử dụng 'ref') - đó là liệu một giá trị hoặc tham chiếu đang được truyền có khác nhau hay không, tức là cấu trúc được truyền theo từng giá trị , các đối tượng lớp được truyền tham chiếu theo giá trị và tham số được đánh dấu ref sẽ chuyển tham chiếu theo tham chiếu.
Paul Ruane

10
Bài báo đó sai lệch về một số điểm chính và tôi đã yêu cầu nhóm MSDN sửa đổi hoặc xóa nó.
Eric Lippert

2
@supercat: để giải quyết điểm đầu tiên của bạn: điểm lớn hơn là trong mã được quản lý nơi giá trị hoặc tham chiếu đến giá trị được lưu trữ phần lớn không liên quan . Chúng tôi đã làm việc chăm chỉ để tạo ra một mô hình bộ nhớ mà hầu hết thời gian cho phép các nhà phát triển cho phép người chạy thay mặt họ đưa ra các quyết định lưu trữ thông minh. Những sự khác biệt này rất quan trọng khi việc không hiểu chúng dẫn đến hậu quả sụp đổ như ở C; không quá nhiều trong C #.
Eric Lippert

1
@supercat: để giải quyết điểm thứ hai của bạn, không có cấu trúc có thể thay đổi nào chủ yếu là xấu. Ví dụ, void M () {S s = new S (); s.Blah (); N (s); }. Refactor thành: void DoBlah (S s) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. Điều đó vừa tạo ra một lỗi vì S là một cấu trúc có thể thay đổi. Bạn có ngay lập tức thấy lỗi không? Hay thực tế là S là một cấu trúc có thể thay đổi hide lỗi từ bạn?
Eric Lippert

6

Mảng cấu trúc được biểu diễn trên heap trong một khối bộ nhớ liền kề, trong khi một mảng đối tượng được biểu diễn dưới dạng khối tham chiếu liền kề với chính các đối tượng thực tế ở nơi khác trên heap, do đó yêu cầu bộ nhớ cho cả đối tượng và cho các tham chiếu mảng của chúng .

Trong trường hợp này, vì bạn đang đặt chúng trong một List<>(và a List<>được sao lưu vào một mảng), sẽ hiệu quả hơn, sử dụng cấu trúc khôn ngoan hơn.

(Tuy nhiên, hãy lưu ý rằng các mảng lớn sẽ tìm đường trên Large Object Heap, nếu thời gian tồn tại của chúng dài, có thể có ảnh hưởng xấu đến việc quản lý bộ nhớ của quy trình của bạn. Hãy nhớ rằng bộ nhớ không phải là yếu tố duy nhất được xem xét.)


Bạn có thể sử dụng reftừ khóa để giải quyết vấn đề này.
leppie

"Tuy nhiên, hãy cẩn thận, rằng các mảng lớn sẽ tìm thấy đường đi của chúng trên Large Object Heap, nếu thời gian tồn tại của chúng dài, có thể có ảnh hưởng xấu đến việc quản lý bộ nhớ của quy trình của bạn." - Tôi không chắc tại sao bạn lại nghĩ như vậy? Việc được cấp phát trên LOH sẽ không gây ra bất kỳ ảnh hưởng xấu nào đến việc quản lý bộ nhớ trừ khi (có thể) đó là một đối tượng tồn tại trong thời gian ngắn và bạn muốn lấy lại bộ nhớ nhanh chóng mà không cần đợi bộ sưu tập Gen 2.
Jon Artus

@Jon Artus: LOH không bị nén chặt. Bất kỳ vật thể tồn tại lâu sẽ chia LOH thành vùng bộ nhớ trống trước và vùng sau. Bộ nhớ liền kề được yêu cầu để cấp phát và nếu những vùng này không đủ lớn để cấp phát thì nhiều bộ nhớ hơn được cấp phát cho LOH (tức là bạn sẽ nhận được phân mảnh LOH).
Paul Ruane

4

Nếu chúng có ngữ nghĩa giá trị, thì bạn có thể nên sử dụng cấu trúc. Nếu chúng có ngữ nghĩa tham chiếu, thì có lẽ bạn nên sử dụng một lớp. Có những ngoại lệ, chủ yếu nghiêng về việc tạo một lớp ngay cả khi có ngữ nghĩa giá trị, nhưng hãy bắt đầu từ đó.

Đối với chỉnh sửa thứ hai của bạn, GC chỉ xử lý đống, nhưng có rất nhiều không gian đống hơn không gian ngăn xếp, vì vậy việc đặt mọi thứ vào ngăn xếp không phải lúc nào cũng thắng. Bên cạnh đó, danh sách các kiểu cấu trúc và danh sách các kiểu lớp sẽ nằm trên heap theo cả hai cách, vì vậy điều này không liên quan trong trường hợp này.

Biên tập:

Tôi bắt đầu coi thuật ngữ xấu xa là có hại. Rốt cuộc, tạo một lớp có thể thay đổi là một ý tưởng tồi nếu nó không cần thiết một cách chủ động và tôi sẽ không loại trừ việc sử dụng một cấu trúc có thể thay đổi. Mặc dù vậy, đó là một ý tưởng tồi và hầu như luôn luôn là một ý tưởng tồi, nhưng chủ yếu là nó không trùng khớp với ngữ nghĩa giá trị, vì vậy sẽ không có ý nghĩa gì khi sử dụng cấu trúc trong trường hợp nhất định.

Có thể có những ngoại lệ hợp lý với các cấu trúc lồng nhau riêng tư, trong đó tất cả việc sử dụng cấu trúc đó do đó bị hạn chế trong một phạm vi rất hạn chế. Tuy nhiên, điều này không áp dụng ở đây.

Thực sự, tôi nghĩ rằng "nó đột biến nên đó là một diễn xuất xấu" không tốt hơn nhiều so với việc tiếp tục về đống và ngăn xếp (ít nhất cũng có một số tác động đến hiệu suất, ngay cả khi thường xuyên bị xuyên tạc). "Nó biến đổi, vì vậy rất có thể không hợp lý khi coi nó là có ngữ nghĩa giá trị, vì vậy nó là một cấu trúc xấu" chỉ hơi khác một chút, nhưng quan trọng là tôi nghĩ.


3

Giải pháp tốt nhất là đo đi, đo lại, sau đó đo thêm một số nữa. Có thể có chi tiết về những gì bạn đang làm có thể gây khó khăn cho một câu trả lời đơn giản, dễ hiểu như "sử dụng cấu trúc" hoặc "sử dụng lớp".


đồng ý với phần đo lường, nhưng theo ý kiến ​​của tôi, đó là một ví dụ thẳng thắn và rõ ràng, và tôi nghĩ rằng có thể một số điều chung chung có thể được nói về nó. Và hóa ra, một số người đã làm như vậy.
Michel

3

Về cơ bản, cấu trúc là một tập hợp các trường. Trong .NET, một cấu trúc có thể "giả vờ" là một đối tượng và đối với mỗi kiểu cấu trúc .NET định nghĩa ngầm một kiểu đối tượng heap với các trường và phương thức giống nhau - là một đối tượng heap - sẽ hoạt động như một đối tượng . Một biến chứa một tham chiếu đến một đối tượng đống như vậy (cấu trúc "đóng hộp") sẽ thể hiện ngữ nghĩa tham chiếu, nhưng một biến chứa một cấu trúc trực tiếp chỉ đơn giản là một tập hợp các biến.

Tôi nghĩ rằng phần lớn sự nhầm lẫn giữa cấu trúc và lớp bắt nguồn từ thực tế là các cấu trúc có hai trường hợp sử dụng rất khác nhau, nên có các hướng dẫn thiết kế rất khác nhau, nhưng các hướng dẫn MS không phân biệt giữa chúng. Đôi khi cần có một thứ gì đó hoạt động như một vật thể; trong trường hợp đó, các hướng dẫn MS khá hợp lý, mặc dù "giới hạn 16 byte" có lẽ giống 24-32 hơn. Tuy nhiên, đôi khi điều cần thiết là tổng hợp các biến. Một cấu trúc được sử dụng cho mục đích đó chỉ nên bao gồm một loạt các trường công khai và có thể Equalsghi đè, ToStringghi đè vàIEquatable(itsType).Equalsthực hiện. Các cấu trúc được sử dụng làm tập hợp các trường không phải là đối tượng và không nên giả vờ như vậy. Theo quan điểm của cấu trúc, ý nghĩa của trường không nên hơn hoặc kém hơn "thứ cuối cùng được ghi vào trường này". Bất kỳ ý nghĩa bổ sung nào phải được xác định bởi mã khách hàng.

Ví dụ: nếu một cấu trúc tổng hợp biến có các thành viên MinimumMaximum, bản thân cấu trúc sẽ không hứa hẹn điều đó Minimum <= Maximum. Mã nhận được cấu trúc như một tham số sẽ hoạt động như thể nó được truyền các giá trị MinimumMaximumgiá trị riêng biệt . Một yêu cầu Minimumkhông lớn hơn Maximumsẽ được coi như một yêu cầu rằng một Minimumtham số không được lớn hơn một tham số được chuyển riêng Maximum.

Một mô hình hữu ích đôi khi cần xem xét là có một ExposedHolder<T>lớp được định nghĩa như sau:

class ExposedHolder<T>
{
  public T Value;
  ExposedHolder() { }
  ExposedHolder(T val) { Value = T; }
}

Nếu một người có a List<ExposedHolder<someStruct>>, đâu someStructlà cấu trúc tổng hợp biến, người ta có thể làm những việc như thế myList[3].Value.someField += 7;, nhưng việc đưa cho myList[3].Valuemã khác sẽ cung cấp cho nó nội dung Valuethay vì cung cấp cho nó một phương tiện để thay đổi nó. Ngược lại, nếu một người đã sử dụng a List<someStruct>, thì nó sẽ cần thiết để sử dụng var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Nếu một người sử dụng loại lớp có thể thay đổi, việc hiển thị nội dung của myList[3]mã bên ngoài sẽ yêu cầu sao chép tất cả các trường sang một số đối tượng khác. Nếu một người đã sử dụng kiểu lớp không thay đổi hoặc cấu trúc "kiểu đối tượng", thì cần phải xây dựng một thể hiện mới giống như myList[3]ngoại trừ someFieldnó khác và sau đó lưu trữ thể hiện mới đó vào danh sách.

Một lưu ý bổ sung: Nếu bạn đang lưu trữ một số lượng lớn những thứ tương tự, có thể tốt nếu lưu trữ chúng trong các mảng cấu trúc có thể lồng vào nhau, tốt hơn là cố gắng giữ kích thước của mỗi mảng trong khoảng từ 1K đến 64K hoặc hơn. Mảng cấu trúc là đặc biệt, trong việc lập chỉ mục đó, người ta sẽ đưa ra một tham chiếu trực tiếp đến một cấu trúc bên trong, vì vậy người ta có thể nói "a [12] .x = 5;". Mặc dù người ta có thể định nghĩa các đối tượng giống mảng, nhưng C # không cho phép chúng chia sẻ cú pháp như vậy với mảng.



1

Từ góc độ c ++, tôi đồng ý rằng việc sửa đổi thuộc tính cấu trúc sẽ chậm hơn so với một lớp. Nhưng tôi nghĩ rằng chúng sẽ được đọc nhanh hơn do cấu trúc được cấp phát trên ngăn xếp thay vì đống. Đọc dữ liệu từ heap yêu cầu kiểm tra nhiều hơn từ ngăn xếp.


1

Vâng, nếu bạn sử dụng struct afterall, thì hãy loại bỏ chuỗi và sử dụng bộ đệm byte hoặc char có kích thước cố định.

Đó là hiệu suất.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.