Tại sao kiểu Tuple mới trong .Net 4.0 là kiểu tham chiếu (lớp) chứ không phải kiểu giá trị (struct)


89

Có ai biết câu trả lời và / hoặc có ý kiến ​​về điều này?

Vì các bộ giá trị thường không lớn lắm, nên tôi cho rằng sẽ hợp lý hơn khi sử dụng các cấu trúc hơn là các lớp cho các bộ này. Bạn nói gì?


1
Đối với bất kỳ ai đến đây sau năm 2016. Trong c # 7 và mới hơn, các chữ Tuple thuộc loại họ ValueTuple<...>. Xem tài liệu tham khảo tại C # các loại tuple
Tamir Daniely

Câu trả lời:


94

Microsoft đã tạo ra tất cả các kiểu tham chiếu tuple type vì sự đơn giản.

Cá nhân tôi nghĩ rằng đây là một sai lầm. Các bộ dữ liệu có nhiều hơn 4 trường là rất bất thường và dù sao cũng nên được thay thế bằng một kiểu thay thế khác (chẳng hạn như loại bản ghi trong F #), vì vậy chỉ những bộ mã nhỏ mới được quan tâm thực tế. Các điểm chuẩn của riêng tôi cho thấy rằng các bộ không đóng hộp lên đến 512 byte vẫn có thể nhanh hơn các bộ đóng hộp.

Mặc dù hiệu quả bộ nhớ là một mối quan tâm, nhưng tôi tin rằng vấn đề nổi trội là chi phí của bộ thu gom rác .NET. Việc phân bổ và thu thập rất tốn kém trên .NET vì trình thu gom rác của nó chưa được tối ưu hóa nhiều (ví dụ như so với JVM). Hơn nữa, .NET GC (máy trạm) mặc định vẫn chưa được song song hóa. Do đó, các chương trình song song sử dụng các bộ giá trị bị dừng lại vì tất cả các lõi đều tranh giành bộ thu gom rác dùng chung, phá hủy khả năng mở rộng. Đây không chỉ là mối quan tâm chính mà AFAIK, đã hoàn toàn bị Microsoft bỏ qua khi họ kiểm tra vấn đề này.

Một mối quan tâm khác là công văn ảo. Các kiểu tham chiếu hỗ trợ các kiểu con và do đó, các thành viên của chúng thường được gọi thông qua công văn ảo. Ngược lại, các kiểu giá trị không thể hỗ trợ các kiểu con nên việc gọi thành viên là hoàn toàn không rõ ràng và luôn có thể được thực hiện như một lời gọi hàm trực tiếp. Điều phối ảo là cực kỳ đắt trên phần cứng hiện đại vì CPU không thể dự đoán nơi bộ đếm chương trình sẽ kết thúc. JVM sử dụng rất nhiều thời gian để tối ưu hóa việc gửi ảo nhưng .NET thì không. Tuy nhiên, .NET cung cấp một lối thoát khỏi điều phối ảo dưới dạng các loại giá trị. Vì vậy, việc biểu diễn các bộ giá trị dưới dạng các loại giá trị có thể đã cải thiện đáng kể hiệu suất ở đây. Ví dụ, gọiGetHashCode trên 2 tuple một triệu lần mất 0,17 giây nhưng gọi nó trên cấu trúc tương đương chỉ mất 0,008 giây, tức là kiểu giá trị nhanh hơn 20 lần so với kiểu tham chiếu.

Một tình huống thực tế mà các vấn đề về hiệu suất với các bộ giá trị thường phát sinh là trong việc sử dụng các bộ giá trị làm khóa trong từ điển. Tôi thực sự tình cờ tìm thấy chủ đề này bằng cách theo một liên kết từ câu hỏi Stack Overflow F # chạy thuật toán của tôi chậm hơn Python! trong đó chương trình F # của tác giả hóa ra lại chậm hơn so với Python của anh ta chính xác vì anh ta đang sử dụng các bộ giá trị đóng hộp. Việc mở hộp theo cách thủ công bằng structkiểu viết tay khiến chương trình F # của anh ấy nhanh hơn nhiều lần và nhanh hơn Python. Những vấn đề này sẽ không bao giờ phát sinh nếu các bộ giá trị được đại diện bởi các loại giá trị chứ không phải các loại tham chiếu để bắt đầu bằng ...


2
@Bent: Vâng, đó chính xác là những gì tôi làm khi bắt gặp các bộ giá trị trên một con đường nóng ở F #. Mặc dù vậy, sẽ rất tuyệt nếu họ cung cấp cả bộ giá trị đóng hộp và chưa đóng hộp trong .NET Framework ...
JD

18
Về công văn ảo, tôi nghĩ rằng lỗi của bạn đã đặt nhầm chỗ: các Tuple<_,...,_>loại có thể đã được niêm phong, trong trường hợp đó, không cần công văn ảo mặc dù là các loại tham chiếu. Tôi tò mò hơn về lý do tại sao chúng không được niêm phong hơn là tại sao chúng là loại tham chiếu.
kvb

2
Từ thử nghiệm của tôi, đối với trường hợp trong đó một tuple sẽ được tạo ở một chức năng và trả về chức năng khác, sau đó không bao giờ được sử dụng lại, cấu trúc trường tiếp xúc dường như mang lại hiệu suất vượt trội cho bất kỳ mục dữ liệu kích thước nào không quá lớn ngăn xếp. Các lớp bất biến chỉ tốt hơn nếu các tham chiếu sẽ được truyền đủ để biện minh cho chi phí xây dựng của chúng (mục dữ liệu càng lớn, chúng càng ít phải vượt qua vòng để sự cân bằng có lợi cho chúng). Vì một tuple được cho là đại diện đơn giản cho một loạt các biến được gắn với nhau, một cấu trúc sẽ có vẻ lý tưởng.
supercat

2
"bộ giá trị chưa được đóng hộp lên đến 512 byte vẫn có thể nhanh hơn được đóng hộp" - đó là kịch bản nào? Bạn thể phân bổ cấu trúc 512B nhanh hơn một cá thể lớp chứa 512B dữ liệu, nhưng việc chuyển nó xung quanh sẽ chậm hơn 100 lần (giả sử là x86). Có điều gì đó tôi đang bỏ qua?
Groo


45

Lý do rất có thể là vì chỉ các bộ giá trị nhỏ hơn mới có ý nghĩa như các kiểu giá trị vì chúng sẽ có dấu vết bộ nhớ nhỏ. Các bộ giá trị lớn hơn (tức là các bộ có nhiều thuộc tính hơn) sẽ thực sự bị ảnh hưởng về hiệu suất vì chúng sẽ lớn hơn 16 byte.

Thay vì có một số bộ giá trị là các loại giá trị và những bộ khác là các loại tham chiếu và buộc các nhà phát triển phải biết đó là loại mà tôi sẽ tưởng tượng, những người ở Microsoft nghĩ rằng việc tạo ra tất cả các loại tham chiếu đơn giản hơn.

Ah, những nghi ngờ đã được xác nhận! Vui lòng xem Building Tuple :

Quyết định quan trọng đầu tiên là xem có nên coi các bộ giá trị như một loại tham chiếu hoặc giá trị hay không. Vì chúng không thay đổi được bất cứ khi nào bạn muốn thay đổi các giá trị của một bộ giá trị, bạn phải tạo một bộ giá trị mới. Nếu chúng là các kiểu tham chiếu, điều này có nghĩa là có thể có nhiều rác được tạo ra nếu bạn đang thay đổi các phần tử trong một bộ theo một vòng lặp chặt chẽ. Bộ giá trị F # là các loại tham chiếu, nhưng có cảm giác từ nhóm rằng họ có thể nhận ra sự cải thiện hiệu suất nếu hai và có lẽ ba, bộ giá trị thay thế là các loại giá trị. Một số nhóm đã tạo các bộ giá trị nội bộ đã sử dụng giá trị thay vì các loại tham chiếu, vì các kịch bản của họ rất nhạy cảm với việc tạo nhiều đối tượng được quản lý. Họ nhận thấy rằng việc sử dụng một loại giá trị mang lại cho họ hiệu suất tốt hơn. Trong bản nháp đầu tiên của chúng tôi về đặc điểm kỹ thuật tuple, chúng tôi đã giữ các bộ giá trị hai, ba và bốn phần tử làm kiểu giá trị, với phần còn lại là kiểu tham chiếu. Tuy nhiên, trong một cuộc họp thiết kế bao gồm các đại diện từ các ngôn ngữ khác, người ta đã quyết định rằng thiết kế "tách rời" này sẽ gây nhầm lẫn, do ngữ nghĩa hơi khác nhau giữa hai loại. Sự nhất quán trong hành vi và thiết kế được xác định là ưu tiên cao hơn so với việc tăng hiệu suất tiềm năng. Dựa trên đầu vào này, chúng tôi đã thay đổi thiết kế để tất cả các bộ giá trị đều là loại tham chiếu, mặc dù chúng tôi đã yêu cầu nhóm F # thực hiện một số điều tra hiệu suất để xem liệu nó có bị tăng tốc khi sử dụng một loại giá trị cho một số kích thước của bộ giá trị hay không. Nó có một cách tốt để kiểm tra điều này, vì trình biên dịch của nó, được viết bằng F #, là một ví dụ điển hình về một chương trình lớn sử dụng các bộ giá trị trong nhiều tình huống khác nhau. Cuối cùng, nhóm F # nhận thấy rằng nó không cải thiện được hiệu suất khi một số bộ giá trị là kiểu giá trị thay vì kiểu tham chiếu. Điều này khiến chúng tôi cảm thấy tốt hơn về quyết định sử dụng các loại tham chiếu cho tuple.


3
Cuộc thảo luận tuyệt vời ở đây: blog.msdn.com/bclteam/archive/2009/07/07/…
Keith Adler

À, tôi hiểu rồi. Tôi vẫn còn một chút nhầm lẫn rằng các loại giá trị làm bất cứ điều gì không có nghĩa là trong thực tế ở đây: P
Bent Rasmussen

Tôi vừa đọc nhận xét về không có giao diện chung chung nào và khi nhìn vào đoạn mã trước đó, đó chính xác là một điều khác khiến tôi chú ý. Nó thực sự khá khó hiểu về sự khéo léo của các loại Tuple. Nhưng, tôi đoán bạn luôn có thể tự tạo ... Dù sao thì không có hỗ trợ cú pháp nào trong C #. Tuy nhiên, ít nhất là ... Tuy nhiên, việc sử dụng generic và các ràng buộc mà nó vẫn còn bị hạn chế trong .Net. Có một tiềm năng đáng kể cho các thư viện trừu tượng rất chung chung nhưng các thư viện chung có thể cần những thứ bổ sung như kiểu trả về hiệp phương sai.
Bent Rasmussen

7
Giới hạn "16 byte" của bạn là không có thật. Khi tôi kiểm tra điều này trên .NET 4, tôi thấy rằng GC quá chậm nên các bộ lưu trữ được mở hộp lên đến 512 byte vẫn có thể nhanh hơn. Tôi cũng đặt câu hỏi về kết quả điểm chuẩn của Microsoft. Tôi cá là họ đã bỏ qua song song (trình biên dịch F # không song song) và đó là nơi mà việc tránh GC thực sự mang lại hiệu quả vì GC máy trạm của .NET cũng không song song.
JD

Vì tò mò, tôi tự hỏi liệu nhóm biên dịch có thử nghiệm ý tưởng làm cho các bộ giá trị trở thành cấu trúc EXPOSED-FIELD không? Nếu ta có một thể hiện của một kiểu với những đặc điểm khác nhau, và cần một thể hiện đó là giống hệt nhau ngoại trừ một đặc điểm đó là khác nhau, một struct tiếp xúc với trường có thể thực hiện điều đó nhiều nhanh hơn so với bất kỳ loại nào khác, và lợi thế duy nhất phát triển như struct get to hơn.
supercat 23/12/12

7

Nếu các loại .NET System.Tuple <...> được định nghĩa là cấu trúc, chúng sẽ không thể mở rộng. Ví dụ: một bộ ba số nguyên dài hiện có tỷ lệ như sau:

type Tuple3 = System.Tuple<int64, int64, int64>
type Tuple33 = System.Tuple<Tuple3, Tuple3, Tuple3>
sizeof<Tuple3> // Gets 4
sizeof<Tuple33> // Gets 4

Nếu bộ ba bậc ba được định nghĩa là một cấu trúc, kết quả sẽ như sau (dựa trên một ví dụ thử nghiệm mà tôi đã triển khai):

sizeof<Tuple3> // Would get 32
sizeof<Tuple33> // Would get 104

Vì các bộ giá trị có hỗ trợ cú pháp tích hợp trong F # và chúng được sử dụng cực kỳ thường xuyên trong ngôn ngữ này, các bộ giá trị "struct" sẽ khiến các lập trình viên F # có nguy cơ viết các chương trình kém hiệu quả mà không hề hay biết. Nó sẽ xảy ra rất dễ dàng:

let t3 = 1L, 2L, 3L
let t33 = t3, t3, t3

Theo ý kiến ​​của tôi, các bộ giá trị "struct" sẽ gây ra khả năng cao là tạo ra sự kém hiệu quả đáng kể trong lập trình hàng ngày. Mặt khác, các bộ giá trị "lớp" hiện đang tồn tại cũng gây ra sự kém hiệu quả nhất định, như @Jon đã đề cập. Tuy nhiên, tôi nghĩ rằng tích của "xác suất xảy ra" nhân với "thiệt hại tiềm ẩn" với các cấu trúc sẽ cao hơn nhiều so với hiện tại với các lớp. Do đó, việc thực hiện hiện tại là ít ác hơn.

Lý tưởng nhất là sẽ có cả bộ giá trị "class" và bộ giá trị "struct", cả hai đều có hỗ trợ cú pháp trong F #!

Chỉnh sửa (2017-10-07)

Các bộ dữ liệu cấu trúc hiện được hỗ trợ đầy đủ như sau:

  • Tích hợp vào mscorlib (.NET> = 4.7) dưới dạng System.ValueTuple
  • Có sẵn dưới dạng NuGet cho các phiên bản khác
  • Hỗ trợ cú pháp trong C #> = 7
  • Hỗ trợ cú pháp trong F #> = 4.1

2
Nếu tránh sao chép không cần thiết, một cấu trúc trường tiếp xúc kích thước bất kỳ sẽ hiệu quả hơn một lớp không thay đổi có cùng kích thước, trừ khi mỗi phiên bản được sao chép đủ lần để chi phí sao chép đó vượt qua chi phí tạo đối tượng heap ( số lượng bản sao hòa vốn thay đổi theo kích thước đối tượng). Sao chép như vậy có thể tránh được nếu ai muốn một cấu trúc mà giả vờ là bất biến, nhưng cấu trúc được thiết kế để xuất hiện như là các bộ sưu tập của các biến (đó là những gì struct ) có thể được sử dụng một cách hiệu quả ngay cả khi họ là rất lớn.
supercat

2
Có thể F # không chơi tốt với ý tưởng chuyển các cấu trúc theo refhoặc có thể không thích thực tế là không phải là "cấu trúc bất biến", đặc biệt là khi được đóng hộp. Thật tệ .net không bao giờ triển khai khái niệm truyền tham số bởi một thực thi const ref, vì trong nhiều trường hợp, ngữ nghĩa như vậy là thứ thực sự cần thiết.
supercat

1
Nhân tiện, tôi coi chi phí khấu hao của GC là một phần của chi phí phân bổ các đối tượng; nếu L0 GC là cần thiết sau mỗi megabyte phân bổ, thì chi phí phân bổ 64 byte là khoảng 1/16.000 chi phí của một L0 GC, cộng với một phần chi phí của bất kỳ L1 hoặc L2 GC nào trở nên cần thiết khi hệ quả của nó.
supercat

4
"Tôi nghĩ rằng tích số của xác suất xảy ra số lần thiệt hại tiềm ẩn sẽ cao hơn nhiều với các cấu trúc so với hiện tại với các lớp". FWIW, tôi rất hiếm khi nhìn thấy các bộ giá trị trong tự nhiên và coi chúng là một lỗ hổng thiết kế nhưng tôi rất thường thấy mọi người phải vật lộn với hiệu suất khủng khiếp khi sử dụng các bộ giá trị (ref) làm khóa trong một Dictionary, ví dụ: tại đây: stackoverflow.com/questions/5850243 /…
JD

3
@Jon Đã hai năm kể từ khi tôi viết câu trả lời này và bây giờ tôi đồng ý với bạn rằng sẽ tốt hơn nếu ít nhất 2 và 3 bộ là cấu trúc. Một đề xuất giọng nói của người dùng ngôn ngữ F # đã được đưa ra về vấn đề này. Vấn đề có một số cấp bách, vì đã có sự gia tăng lớn các ứng dụng trong dữ liệu lớn, tài chính định lượng và chơi game trong những năm gần đây.
Marc Sigrist

4

Đối với 2 bộ giá trị, bạn vẫn luôn có thể sử dụng KeyValuePair <TKey, TValue> từ các phiên bản trước của Hệ thống loại chung. Đó là một kiểu giá trị.

Một sự làm rõ nhỏ đối với bài viết của Matt Ellis là sự khác biệt về ngữ nghĩa sử dụng giữa các kiểu tham chiếu và giá trị chỉ là "nhỏ" khi tính bất biến có hiệu lực (tất nhiên, sẽ là trường hợp ở đây). Tuy nhiên, tôi nghĩ tốt nhất là trong thiết kế BCL không đưa ra sự nhầm lẫn khi để Tuple chuyển qua một loại tham chiếu ở một số ngưỡng.


Nếu một giá trị sẽ được sử dụng một lần sau khi nó được trả về, cấu trúc trường tiếp xúc có kích thước bất kỳ sẽ hoạt động tốt hơn bất kỳ loại nào khác, chỉ với điều kiện là nó không quá lớn đến mức có thể thổi bay ngăn xếp. Chi phí xây dựng một đối tượng lớp sẽ chỉ được hoàn lại nếu tham chiếu kết thúc được chia sẻ nhiều lần. Đôi khi nó hữu ích cho một loại không đồng nhất có kích thước cố định có mục đích chung là một lớp, nhưng có những lúc khác khi cấu trúc sẽ tốt hơn - ngay cả đối với những thứ "lớn".
supercat

Cảm ơn bạn đã thêm quy tắc ngón tay cái hữu ích này. Tuy nhiên, tôi hy vọng rằng bạn không hiểu sai vị trí của tôi: Tôi là một người nghiện giá trị. ( stackoverflow.com/a/14277068 không nên nghi ngờ gì).
Glenn Slayden

Các kiểu giá trị là một trong những tính năng tuyệt vời của .net, nhưng rất tiếc người đã viết msdn dox không nhận ra rằng có nhiều trường hợp sử dụng riêng biệt cho chúng và các trường hợp sử dụng khác nhau nên có các hướng dẫn khác nhau. Phong cách của struct MSDN khuyến cáo chỉ nên được sử dụng với cấu trúc đại diện cho một giá trị đồng nhất, nhưng nếu một trong những nhu cầu để đại diện cho một số giá trị độc lập gắn chặt cùng với băng keo, người ta không nên sử dụng rằng phong cách của struct - ta nên sử dụng một cấu trúc với các trường công cộng tiếp xúc.
supercat

0

Tôi không biết nhưng nếu bạn đã từng sử dụng F # Tuples là một phần của ngôn ngữ. Nếu tôi tạo .dll và trả về một loại Tuples, thật tuyệt khi có một loại để đưa nó vào. Tôi nghi ngờ rằng F # là một phần của ngôn ngữ (.Net 4), một số sửa đổi đối với CLR đã được thực hiện để phù hợp với một số cấu trúc phổ biến trong F #

Từ http://en.wikibooks.org/wiki/F_Sharp_Programming/Tuples_and_Records

let scalarMultiply (s : float) (a, b, c) = (a * s, b * s, c * s);;

val scalarMultiply : float -> float * float * float -> float * float * float

scalarMultiply 5.0 (6.0, 10.0, 20.0);;
val it : float * float * float = (30.0, 50.0, 100.0)
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.