Dlaczego nowy typ krotki w .Net 4.0 jest typem referencyjnym (klasą), a nie typem wartości (struct)

89

Czy ktoś zna odpowiedź i / lub ma na ten temat opinię?

Ponieważ krotki normalnie nie byłyby bardzo duże, przypuszczam, że bardziej sensowne byłoby użycie do nich struktur niż klas. Co powiesz?

Bent Rasmussen
źródło
1
Dla każdego, kto potyka się tutaj po 2016 roku. W języku c # 7 i nowszych literały Tuple należą do rodziny czcionek ValueTuple<...>. Zobacz odniesienia w C # typy krotek
Tamir Daniely

Odpowiedzi:

94

Firma Microsoft stworzyła wszystkie typy odwołań do typów krotek w celu uproszczenia.

Osobiście uważam, że to był błąd. Krotki z więcej niż 4 polami są bardzo nietypowe i i tak powinny zostać zastąpione bardziej typową alternatywą (taką jak typ rekordu w F #), więc tylko małe krotki są praktyczne. Moje własne testy porównawcze pokazały, że rozpakowane krotki do 512 bajtów mogą być nadal szybsze niż krotki w pudełkach.

Chociaż wydajność pamięci jest jednym z problemów, uważam, że dominującą kwestią jest obciążenie modułu odśmiecania pamięci .NET. Alokacja i zbieranie są bardzo kosztowne w .NET, ponieważ jego moduł odśmiecania pamięci nie został zbyt mocno zoptymalizowany (np. W porównaniu z maszyną JVM). Ponadto domyślny .NET GC (stacja robocza) nie został jeszcze zrównoleglony. W rezultacie równoległe programy, które używają krotek, zatrzymują się, ponieważ wszystkie rdzenie walczą o współdzielony moduł odśmiecania pamięci, niszcząc skalowalność. To nie tylko dominujący problem, ale, AFAIK, został całkowicie zaniedbany przez Microsoft, gdy badał ten problem.

Kolejnym problemem jest wirtualna wysyłka. Typy referencyjne obsługują podtypy, dlatego ich elementy członkowskie są zwykle wywoływane za pośrednictwem wirtualnej wysyłki. W przeciwieństwie do tego typy wartości nie mogą obsługiwać podtypów, więc wywołanie elementu członkowskiego jest całkowicie jednoznaczne i zawsze można je wykonać jako bezpośrednie wywołanie funkcji. Wirtualna wysyłka jest niezwykle kosztowna w przypadku nowoczesnego sprzętu, ponieważ procesor nie może przewidzieć, gdzie skończy się licznik programu. JVM dokłada wszelkich starań, aby zoptymalizować wysyłanie wirtualne, ale .NET tego nie robi. Jednak .NET zapewnia ucieczkę przed wirtualną wysyłką w postaci typów wartości. Zatem reprezentowanie krotek jako typów wartości mogłoby tutaj ponownie znacznie poprawić wydajność. Na przykład dzwonienieGetHashCode na dwukrotnej krotce milion razy zajmuje 0,17 s, ale wywołanie jej na równoważnej strukturze zajmuje tylko 0,008 s, tj. typ wartości jest 20 razy szybszy niż typ referencyjny.

Prawdziwą sytuacją, w której często występują problemy z wydajnością krotek, jest użycie krotek jako kluczy w słownikach. Właściwie natknąłem się na ten wątek, podążając za linkiem z pytania przepełnienia stosu. F # uruchamia mój algorytm wolniej niż Python! gdzie autorski program F # okazał się wolniejszy niż jego Python właśnie dlatego, że używał krotek pudełkowych. Ręczne rozpakowywanie przy użyciu odręcznego structtypu sprawia, że ​​jego program F # jest kilka razy szybszy i szybszy niż Python. Te problemy nigdy by się nie pojawiły, gdyby krotki były reprezentowane przez typy wartości, a nie typy referencyjne na początku ...

JD
źródło
2
@Bent: Tak, dokładnie to robię, kiedy napotykam krotki na gorącej ścieżce w F #. Byłoby miło, gdyby dostarczyli zarówno krotki w pudełkach, jak i bez pudełek w .NET Framework ...
JD
18
Jeśli chodzi o wysyłkę wirtualną, myślę, że twoja wina jest niesłuszna: Tuple<_,...,_>typy mogły zostać przypieczętowane, w takim przypadku żadna wirtualna wysyłka nie byłaby potrzebna, mimo że są typami referencyjnymi. Bardziej ciekawi mnie, dlaczego nie są zapieczętowane, niż dlaczego są typami referencyjnymi.
kvb
2
Z moich testów wynika, że ​​dla scenariusza, w którym krotka byłaby generowana w jednej funkcji i zwracana do innej funkcji, a następnie nigdy więcej nie byłaby używana, struktury pola odsłoniętego wydają się oferować lepszą wydajność dla dowolnego rozmiaru elementu danych, który nie jest tak duży, aby wybuchnąć stos. Niezmienne klasy są lepsze tylko wtedy, gdy referencje zostaną przekazane na tyle, aby uzasadnić ich koszt budowy (im większy element danych, tym mniej trzeba je omijać, aby kompromis mógł je faworyzować). Ponieważ krotka ma reprezentować po prostu zbiór sklejonych ze sobą zmiennych, struktura wydaje się idealna.
supercat
2
„Rozpakowane krotki o wielkości do 512 bajtów nadal mogą być szybsze niż w pudełkach” - który to scenariusz? Państwo może być w stanie przeznaczyć struct 512B szybciej niż instancji klasy gospodarstwa 512B danych, ale przechodząc wokół będzie więcej niż 100 razy wolniej (zakładając, x86). Czy jest coś, co przeoczam?
Groo
45

Powodem jest najprawdopodobniej to, że tylko mniejsze krotki miałyby sens jako typy wartości, ponieważ miałyby niewielki ślad pamięci. Większe krotki (tj. Te z większą liczbą właściwości) faktycznie ucierpiałyby na wydajności, ponieważ byłyby większe niż 16 bajtów.

Zamiast tego, aby niektóre krotki były typami wartości, a inne były typami referencyjnymi, i zmuszają programistów, aby wiedzieli, które z nich są, jak sobie wyobrażam, ludzie w firmie Microsoft myśleli, że uczynienie ich wszystkich typów referencyjnych było prostsze.

Ach, podejrzenia się potwierdziły! Zobacz sekcję Budowanie :

Pierwsza ważna decyzja dotyczyła traktowania krotek jako referencji lub typu wartości. Ponieważ są one niezmienne za każdym razem, gdy chcesz zmienić wartości krotki, musisz utworzyć nową. Jeśli są to typy referencyjne, oznacza to, że może powstać wiele śmieci, jeśli zmieniasz elementy w krotce w ciasnej pętli. Krotki F # były typami referencyjnymi, ale zespół miał wrażenie, że mógłby uzyskać poprawę wydajności, gdyby zamiast tego dwie, a może trzy krotki elementów były typami wartości. Niektóre zespoły, które utworzyły wewnętrzne krotki, używały wartości zamiast typów referencyjnych, ponieważ ich scenariusze były bardzo wrażliwe na tworzenie wielu zarządzanych obiektów. Odkryli, że użycie typu wartości zapewniło im lepszą wydajność. W naszej pierwszej wersji roboczej specyfikacji krotki Zachowaliśmy dwu-, trzy- i czteroelementowe krotki jako typy wartości, a reszta to typy referencyjne. Jednak podczas spotkania projektowego, które obejmowało przedstawicieli innych języków, zdecydowano, że ten „podzielony” projekt będzie mylący ze względu na nieco inną semantykę obu typów. Stwierdzono, że spójność w zachowaniu i projekcie ma wyższy priorytet niż potencjalny wzrost wydajności. Na podstawie tych danych wejściowych zmieniliśmy projekt tak, aby wszystkie krotki były typami referencyjnymi, chociaż poprosiliśmy zespół F # o przeprowadzenie analizy wydajności, aby sprawdzić, czy wystąpiło przyspieszenie podczas używania typu wartości dla niektórych rozmiarów krotek. Miał dobry sposób na przetestowanie tego, ponieważ jego kompilator, napisany w języku F #, był dobrym przykładem dużego programu, który używał krotek w różnych scenariuszach. W końcu zespół F # stwierdził, że nie uzyskał poprawy wydajności, gdy niektóre krotki były typami wartości zamiast typów referencyjnych. To sprawiło, że poczuliśmy się lepiej, gdy zdecydowaliśmy się na użycie typów referencyjnych dla krotki.

Andrew Hare
źródło
3
Świetna dyskusja tutaj: blogs.msdn.com/bclteam/archive/2009/07/07/…
Keith Adler
Ahh rozumiem. Nadal jestem trochę zdezorientowany, że typy wartości nic nie znaczą w praktyce: P
Bent Rasmussen
Właśnie przeczytałem komentarz o braku ogólnych interfejsów i kiedy patrzyłem na kod wcześniej, to była dokładnie kolejna rzecz, która mnie uderzyła. To naprawdę mało inspirujące, jak nietypowe są typy Tuple. Ale myślę, że zawsze możesz stworzyć własne ... W C # i tak nie ma wsparcia syntaktycznego. Ale przynajmniej ... Mimo to, użycie leków generycznych i ograniczeń, które są one nadal ograniczone w .Net, wydaje się ograniczone. Istnieje znaczny potencjał dla bardzo ogólnych, bardzo abstrakcyjnych bibliotek, ale generyczne prawdopodobnie potrzebują dodatkowych rzeczy, takich jak kowariantne typy zwracane.
Bent Rasmussen
7
Twój limit „16 bajtów” jest fałszywy. Kiedy testowałem to na .NET 4, odkryłem, że GC jest tak powolny, że rozpakowane krotki do 512 bajtów mogą nadal być szybsze. Kwestionowałbym również wyniki testów porównawczych Microsoftu. Założę się, że zignorowali równoległość (kompilator F # nie jest równoległy) i właśnie tam unikanie GC naprawdę się opłaca, ponieważ GC stacji roboczej .NET również nie jest równoległe.
JD
Z ciekawości zastanawiam się, czy zespół kompilatorów przetestował pomysł tworzenia krotek jako struktur EXPOSED-FIELD ? Jeśli ktoś ma instancję typu z różnymi cechami i potrzebuje instancji, która jest identyczna z wyjątkiem jednej cechy, która jest inna, struktura ujawnionego pola może osiągnąć o wiele szybciej niż jakikolwiek inny typ, a przewaga rośnie tylko wtedy, gdy struktury uzyskują większy.
supercat
7

Gdyby typy .NET System.Tuple <...> zostały zdefiniowane jako struktury, nie byłyby skalowalne. Na przykład trójskładnikowa krotka długich liczb całkowitych jest obecnie skalowana w następujący sposób:

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

Gdyby trójskładnikowa krotka została zdefiniowana jako struktura, wynik byłby następujący (na podstawie przykładu testowego, który zaimplementowałem):

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

Ponieważ krotki mają wbudowaną obsługę składni w języku F # i są niezwykle często używane w tym języku, krotki „struct” narażałyby programistów F # na ryzyko pisania nieefektywnych programów, nawet o tym nie wiedząc. Stałoby się to tak łatwo:

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

Moim zdaniem krotki „strukturalne” spowodowałyby duże prawdopodobieństwo powstania znacznych nieefektywności w codziennym programowaniu. Z drugiej strony, istniejące obecnie krotki „klasowe” również powodują pewne nieefektywności, o czym wspomniał @Jon. Myślę jednak, że iloczyn „prawdopodobieństwa wystąpienia” razy „potencjalne uszkodzenie” byłby znacznie wyższy w przypadku struktur niż obecnie w przypadku klas. Dlatego obecne wdrożenie jest mniejszym złem.

Idealnie byłoby, gdyby istniały zarówno krotki „class”, jak i „struct”, obie z obsługą składni w języku F #!

Edycja (2017-10-07)

Krotki struktury są teraz w pełni obsługiwane w następujący sposób:

Marc Sigrist
źródło
2
Jeśli unika się niepotrzebnego kopiowania, struktura ujawnionego pola o dowolnym rozmiarze będzie bardziej wydajna niż niezmienna klasa o tym samym rozmiarze, chyba że każda instancja zostanie skopiowana na tyle razy, że koszt takiego kopiowania przewyższy koszt tworzenia obiektu sterty ( nieparzysta liczba kopii zależy od rozmiaru obiektu). Takie kopiowanie może być nieuniknione, jeśli ktoś chce struct który udaje się niezmienne, ale elemencie, który przeznaczony jest do stawienia się w zbiorach zmiennych (co jest co struct ) mogą być wykorzystane efektywnie nawet gdy są one ogromne.
supercat
2
Może się zdarzyć, że F # nie pasuje do idei przekazywania struktur obok ref, lub może nie lubić faktu, że tak zwane „niezmienne struktury” nie są, zwłaszcza gdy są opakowane. Szkoda, że ​​.net nigdy nie zaimplementował koncepcji przekazywania parametrów przez element egzekwowalny const ref, ponieważ w wielu przypadkach taka semantyka jest naprawdę wymagana.
supercat
1
Nawiasem mówiąc, uważam zamortyzowany koszt GC za część kosztu alokacji obiektów; jeśli L0 GC byłby konieczny po każdym megabajcie alokacji, wówczas koszt przydzielenia 64 bajtów wynosi około 1/16 000 kosztu L0 GC plus ułamek kosztu jakichkolwiek L1 lub L2 GC, które stają się konieczne jako konsekwencja tego.
supercat
4
„Myślę, że iloczyn prawdopodobieństwa wystąpienia i potencjalnej szkody byłby znacznie wyższy w przypadku struktur niż obecnie w przypadku klas”. FWIW, bardzo rzadko widziałem krotki krotek na wolności i uważam je za wadę projektową, ale bardzo często widzę ludzi borykających się z okropną wydajnością, gdy używają krotek (ref) jako kluczy w a Dictionary, np. Tutaj: stackoverflow.com/questions/5850243 /…
JD
3
@Jon Minęły dwa lata, odkąd napisałem tę odpowiedź, a teraz zgadzam się z tobą, że byłoby lepiej, gdyby co najmniej 2- i 3-krotki były strukturami. W związku z tym złożono sugestię dotyczącą głosu użytkownika w języku F # . Problem jest pilny, ponieważ w ostatnich latach nastąpił ogromny wzrost zastosowań w dużych zbiorach danych, finansach ilościowych i grach.
Marc Sigrist
4

W przypadku krotek 2 nadal można zawsze używać KeyValuePair <TKey, TValue> z wcześniejszych wersji systemu Common Type. To typ wartości.

Drobnym wyjaśnieniem artykułu Matta Ellisa byłoby to, że różnica w semantyce użycia między typami referencyjnymi i wartościowymi jest tylko „niewielka”, gdy obowiązuje niezmienność (co oczywiście miałoby miejsce w tym przypadku). Niemniej jednak myślę, że byłoby najlepiej w projekcie BCL nie wprowadzać zamieszania związanego z przejściem Tuple do typu odniesienia na pewnym progu.

Glenn Slayden
źródło
Jeśli wartość zostanie użyta raz po jej zwróceniu, struktura ujawnionego pola o dowolnym rozmiarze będzie działać lepiej niż jakikolwiek inny typ, pod warunkiem, że nie jest tak potwornie duża, aby zniszczyć stos. Koszt budowy obiektu klasy zwróci się tylko wtedy, gdy odniesienie zostanie udostępnione wiele razy. Czasami przydaje się, aby typ heterogeniczny ogólnego przeznaczenia o stałym rozmiarze był klasą, ale są też sytuacje, w których struktura byłaby lepsza - nawet w przypadku „dużych” rzeczy.
supercat
Dziękujemy za dodanie tej praktycznej reguły. Mam jednak nadzieję, że nie zrozumiałeś źle mojego stanowiska: jestem ćpunem typu wartościowego. ( stackoverflow.com/a/14277068 nie powinno pozostawiać wątpliwości).
Glenn Slayden,
Typy wartości są jedną z najlepszych cech .net, ale niestety osoba, która napisała msdn dox, nie zauważyła, że ​​istnieje dla nich wiele rozłącznych przypadków użycia i że różne przypadki użycia powinny mieć różne wytyczne. Styl polecany przez struct msdn powinien być używany tylko ze strukturami, które reprezentują jednorodną wartość, ale jeśli trzeba przedstawić jakieś niezależne wartości połączone taśmą klejącą, nie należy używać tego stylu struktury - należy użyć struktury odsłonięte pola publiczne.
supercat
0

Nie wiem, ale jeśli kiedykolwiek używałeś F # krotki są częścią języka. Jeśli utworzyłem plik .dll i zwróciłem typ krotek, byłoby miło mieć typ do umieszczenia go. Podejrzewam teraz, że F # jest częścią języka (.Net 4) wprowadzono pewne modyfikacje w CLR w celu dostosowania niektórych wspólnych struktur w F #

Z 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)
Bioniczny Cyborg
źródło