Struktury a klasy

95

Mam zamiar stworzyć 100 000 obiektów w kodzie. Są małe, tylko z 2 lub 3 właściwościami. Umieszczę je na ogólnej liście, a kiedy już są, zapętlę je i sprawdzę wartość, aa może zaktualizuję wartość b.

Czy szybciej / lepiej jest tworzyć te obiekty jako klasę czy strukturę?

EDYTOWAĆ

za. Właściwości są typami wartości (poza ciągiem, jak sądzę?)

b. Mogą (nie jesteśmy jeszcze pewni) mieć metodę walidacji

EDYCJA 2

Zastanawiałem się: czy obiekty na stercie i stosie są przetwarzane równo przez garbage collectora, czy to działa inaczej?

Michel
źródło
2
Czy będą mieć tylko pola publiczne, czy też będą mieli metody? Czy typy to typy pierwotne, takie jak liczby całkowite? Czy będą zawarte w tablicy, czy w czymś takim jak List <T>?
JeffFerguson
14
Lista zmiennych struktur? Uważaj na velociraptora.
Anthony Pegram
1
@Anthony: Obawiam się, że brakuje mi żartu o Velociraptor: -s
Michel
5
Żart velociraptora pochodzi z XKCD. Ale kiedy przerzucasz błędne przekonanie / szczegóły implementacji `` typy wartości są przydzielane na stosie '' (usuń jeśli to konieczne), to Eric Lippert musisz uważać na ...
Greg Beech
4
velociraptor
WernerCD

Odpowiedzi:

138

Czy szybsze jest tworzenie tych obiektów jako klasy czy struktury?

Jesteś jedyną osobą, która może określić odpowiedź na to pytanie. Wypróbuj oba sposoby, zmierz znaczący, zorientowany na użytkownika i odpowiedni miernik wydajności, a dowiesz się, czy zmiana ma znaczący wpływ na prawdziwych użytkowników w odpowiednich scenariuszach.

Struktury zużywają mniej pamięci sterty (ponieważ są mniejsze i łatwiejsze do zagęszczenia, a nie dlatego, że znajdują się „na stosie”). Ale kopiowanie trwa dłużej niż kopia referencyjna. Nie wiem, jakie są wskaźniki wydajności dotyczące wykorzystania pamięci lub szybkości; jest tu kompromis i jesteś osobą, która wie, co to jest.

Czy lepiej jest tworzyć te obiekty jako klasę czy strukturę?

Może klasa, może struktura. Ogólna reguła: Jeśli obiekt jest:
1. Mały
2. Logicznie niezmienna wartość
3. Jest ich dużo.
Wtedy rozważę zrobienie z niego struktury. W przeciwnym razie trzymałbym się typu referencyjnego.

Jeśli chcesz zmutować jakieś pole struktury, zwykle lepiej jest zbudować konstruktora, który zwraca całą nową strukturę z prawidłowo ustawionym polem. To może być trochę wolniejsze (zmierz to!), Ale logicznie dużo łatwiejsze do rozważenia.

Czy obiekty na stercie i stosie są przetwarzane jednakowo przez moduł odśmiecania pamięci?

Nie , nie są takie same, ponieważ obiekty na stosie są podstawowymi elementami kolekcji . Odśmiecacz nie musi nigdy pytać „czy ta rzecz na stosie żyje?” ponieważ odpowiedź na to pytanie zawsze brzmi „Tak, jest na stosie”. (Teraz nie możesz na tym polegać, aby utrzymać obiekt przy życiu, ponieważ stos jest szczegółem implementacji. Jitter może wprowadzać optymalizacje, które, powiedzmy, rejestrują to, co normalnie byłoby wartością stosu, a następnie nigdy nie jest na stosie więc GC nie wie, że wciąż żyje. Zarejestrowany obiekt może agresywnie zbierać swoich potomków, gdy tylko rejestr, który go posiada, nie będzie ponownie odczytywany.)

Ale garbage collector nie trzeba traktować obiektów na stosie jako żywa, w taki sam sposób, że traktuje dowolny obiekt znany, że żyje jak żyje. Obiekt na stosie może odnosić się do obiektów przydzielonych na stosie, które muszą być utrzymywane przy życiu, więc GC musi traktować obiekty stosu jak żywe obiekty przydzielone na stosie w celu określenia aktywnego zestawu. Ale oczywiście nie są one traktowane jako „żywe obiekty” na potrzeby kompaktowania pryzmy, ponieważ w ogóle nie znajdują się na stercie.

Czy to jasne?

Eric Lippert
źródło
Eric, czy wiesz, czy kompilator lub jitter wykorzystuje niezmienność (być może jeśli jest wymuszona readonly), aby umożliwić optymalizacje. Nie pozwoliłbym, żeby to wpłynęło na wybór zmienności (teoretycznie jestem wariatem na temat szczegółów wydajności, ale w praktyce moim pierwszym krokiem w kierunku wydajności jest zawsze próba uzyskania tak prostej gwarancji poprawności, jak tylko mogę i dlatego marnować cykle procesora i cykle mózgowe na kontrolach i przypadkach skrajnych, a bycie odpowiednio zmiennym lub niezmiennym pomaga tam), ale przeciwdziałałoby to jakiejkolwiek odruchowej reakcji na twoje twierdzenie, że niezmienność może być wolniejsza.
Jon Hanna
@Jon: kompilator C # optymalizuje dane const, ale nie dane tylko do odczytu . Nie wiem, czy kompilator jit przeprowadza jakiekolwiek optymalizacje buforowania w polach tylko do odczytu.
Eric Lippert
Szkoda, bo wiem, że znajomość niezmienności pozwala na pewne optymalizacje, ale w tym momencie przekroczyłem granice mojej wiedzy teoretycznej, ale są to granice, które chciałbym rozciągnąć. W międzyczasie "może być szybciej w obie strony, oto dlaczego, teraz przetestuj i dowiedz się, co ma zastosowanie w tym przypadku" jest przydatne, aby móc powiedzieć :)
Jon Hanna
Polecam przeczytanie simple-talk.com/dotnet/.net-framework/… i własnego artykułu (@Eric): blogs.msdn.com/b/ericlippert/archive/2010/09/30/…, aby rozpocząć nurkowanie w szczegóły. Istnieje wiele innych dobrych artykułów. A tak przy okazji, różnica w przetwarzaniu 100 000 małych obiektów w pamięci jest prawie niezauważalna ze względu na pewne obciążenie pamięci (~ 2,3 MB) dla klasy. Można to łatwo sprawdzić za pomocą prostego testu.
Nick Martyshchenko
23

Czasami structnie musisz wywoływać konstruktora new () i bezpośrednio przypisywać pola, dzięki czemu jest znacznie szybszy niż zwykle.

Przykład:

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

jest około 2 do 3 razy szybszy niż

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

gdzie Valuejest structz dwoma polami ( idi isValid).

struct Value
{
    int id;
    bool isValid;

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

Z drugiej strony, elementy muszą zostać przeniesione lub wybrane typy wartości, wszystko to kopiowanie spowolni. Podejrzewam, że aby uzyskać dokładną odpowiedź, musisz sprofilować swój kod i przetestować go.

John Alexiou
źródło
Oczywiście rzeczy stają się znacznie szybsze, gdy kierujesz wartości również poza natywne granice.
leppie
Sugerowałbym użycie nazwy innej niż list, biorąc pod uwagę, że wskazany kod nie będzie działał z List<Value>.
supercat
7

Struktury mogą wydawać się podobne do klas, ale istnieją istotne różnice, o których powinieneś wiedzieć. Przede wszystkim klasy są typami referencyjnymi, a struktury są typami wartości. Używając struktur, możesz tworzyć obiekty, które zachowują się jak typy wbudowane i cieszyć się ich zaletami.

Kiedy wywołujesz operatora New w klasie, zostanie ona przydzielona na stercie. Jednak po utworzeniu wystąpienia struktury zostanie utworzona na stosie. Przyniesie to wzrost wydajności. Ponadto nie będziesz mieć do czynienia z odwołaniami do instancji struktury, tak jak w przypadku klas. Będziesz pracować bezpośrednio z instancją struct. Z tego powodu podczas przekazywania struktury do metody jest ona przekazywana przez wartość zamiast jako odwołanie.

Więcej tutaj:

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

kyndigs
źródło
4
Wiem, że mówi to w MSDN, ale MSDN nie opowiada całej historii. Stos kontra sterta to szczegół implementacji, a struktury nie zawsze trafiają na stos. Tylko jeden niedawny blog na ten temat można znaleźć na stronie: blogs.msdn.com/b/ericlippert/archive/2010/09/30/…
Anthony Pegram
„... jest przekazywana przez wartość ...” zarówno odwołania, jak i struktury są przekazywane przez wartość (chyba że używa się „ref”) - różni się to, czy przekazywana jest wartość lub odwołanie, tj. struktury są przekazywane wartość po wartości , obiekty klas są przekazywane jako odniesienie do wartości, a parametry oznaczone przez odniesienie przekazują odniesienie do odniesienia.
Paul Ruane
10
Ten artykuł jest mylący w kilku kluczowych punktach i poprosiłem zespół MSDN o poprawienie go lub usunięcie.
Eric Lippert
2
@supercat: aby zająć się pierwszym punktem: większym punktem jest ten w kodzie zarządzanym, w którym przechowywana jest wartość lub odniesienie do wartości, jest w dużej mierze nieistotne . Ciężko pracowaliśmy, aby stworzyć model pamięci, który przez większość czasu pozwala programistom zezwalać środowisku wykonawczemu na podejmowanie inteligentnych decyzji dotyczących pamięci masowej w ich imieniu. Te rozróżnienia mają ogromne znaczenie, gdy niezrozumienie ich ma katastrofalne konsekwencje, jak ma to miejsce w C; nie tak bardzo w C #.
Eric Lippert
1
@supercat: aby zająć się drugą kwestią, żadne zmienne struktury nie są w większości złe. Na przykład void M () {S s = new S (); s.Blah (); N (s); }. Refaktoryzuj do: void DoBlah (S) {s.Blah (); } void M (S s = new S (); DoBlah (s); N (s);}. To właśnie wprowadziło błąd, ponieważ S jest zmienną strukturą. Czy od razu zauważyłeś błąd? A może fakt, że S jest zmienna struktura ukrywa przed tobą błąd?
Eric Lippert
6

Tablice struktur są reprezentowane na stercie w ciągłym bloku pamięci, podczas gdy tablica obiektów jest reprezentowana jako ciągły blok odniesień z rzeczywistymi obiektami w innym miejscu na stercie, co wymaga pamięci zarówno dla obiektów, jak i dla ich odwołań do tablic .

W tym przypadku, gdy umieszczasz je w a List<>(a a List<>jest zapisywane w tablicy), byłoby bardziej wydajne, jeśli chodzi o wykorzystanie pamięci, aby użyć struktur.

(Uważaj jednak, że duże tablice trafią na stertę dużych obiektów, gdzie, jeśli ich żywotność jest długa, może mieć niekorzystny wpływ na zarządzanie pamięcią twojego procesu. Pamiętaj też, że pamięć nie jest jedynym czynnikiem).

Paul Ruane
źródło
Możesz użyć refsłowa kluczowego, aby sobie z tym poradzić.
leppie
„Uważaj jednak, że duże tablice trafią na stertę dużych obiektów, gdzie, jeśli ich żywotność jest długa, może mieć niekorzystny wpływ na zarządzanie pamięcią w procesie”. - Nie jestem całkiem pewien, dlaczego tak myślisz? Przydzielenie do LOH nie spowoduje żadnych negatywnych skutków dla zarządzania pamięcią, chyba że (prawdopodobnie) jest to obiekt krótkotrwały i chcesz szybko odzyskać pamięć bez czekania na kolekcję Gen 2.
Jon Artus
@Jon Artus: LOH nie zagęszcza się. Każdy długowieczny obiekt podzieli LOH na obszar wolnej pamięci przed i obszar po. Do alokacji wymagana jest ciągła pamięć, a jeśli te obszary nie są wystarczająco duże do alokacji, wówczas więcej pamięci jest przydzielane do LOH (tj. Uzyskasz fragmentację LOH).
Paul Ruane
4

Jeśli mają semantykę wartości, prawdopodobnie powinieneś użyć struktury. Jeśli mają semantykę odwołań, prawdopodobnie powinieneś użyć klasy. Istnieją wyjątki, które w większości skłaniają się do tworzenia klasy, nawet jeśli istnieje semantyka wartości, ale zacznij od tego.

Jeśli chodzi o twoją drugą edycję, GC zajmuje się tylko stertą, ale jest o wiele więcej miejsca na stosie niż miejsce na stosie, więc umieszczanie rzeczy na stosie nie zawsze jest wygraną. Poza tym lista typów struktur i lista typów klas będzie na stercie w obie strony, więc nie ma to znaczenia w tym przypadku.

Edytować:

Zaczynam uważać określenie zło za szkodliwe. W końcu zrobienie mutowalnej klasy jest złym pomysłem, jeśli nie jest aktywnie potrzebne, i nie wykluczałbym kiedykolwiek używania mutowalnej struktury. Jest to kiepski pomysł tak często, że prawie zawsze jest złym pomysłem, ale przeważnie po prostu nie pokrywa się z semantyką wartości, więc po prostu nie ma sensu używać struktury w danym przypadku.

Mogą istnieć rozsądne wyjątki w przypadku prywatnych zagnieżdżonych struktur, w przypadku których wszystkie zastosowania tej struktury są zatem ograniczone do bardzo ograniczonego zakresu. Nie dotyczy to jednak tutaj.

Naprawdę myślę, że „mutuje, więc jest złym układem” nie jest dużo lepsze niż gadanie o stosie i stosie (co przynajmniej ma pewien wpływ na wydajność, nawet jeśli jest często błędnie przedstawiany). „To mutuje, więc prawdopodobnie nie ma sensu uważać go za mającego semantykę wartości, więc jest to zła struktura” jest tylko trochę inny, ale co ważne, tak myślę.

Jon Hanna
źródło
3

Najlepszym rozwiązaniem jest zmierzenie, ponowne zmierzenie, a następnie jeszcze większy pomiar. Mogą istnieć szczegóły tego, co robisz, co może utrudniać uproszczoną, łatwą odpowiedź, taką jak „użyj struktur” lub „użyj klas”.

FMM
źródło
zgadzam się z częścią dotyczącą miar, ale moim zdaniem był to prosty i jasny przykład i pomyślałem, że może można o nim powiedzieć kilka ogólnych rzeczy. I jak się okazało, niektórzy tak zrobili.
Michel
3

Struktura jest w swej istocie niczym więcej, ani mniej niż skupiskiem pól. W .NET możliwe jest, aby struktura "udawała" obiekt, a dla każdego typu struktury .NET domyślnie definiuje typ obiektu sterty z tymi samymi polami i metodami, które - będąc obiektem sterty - będą zachowywać się jak obiekt . Zmienna, która zawiera odniesienie do takiego obiektu sterty (struktura „pudełkowa”) będzie wykazywać semantykę referencyjną, ale ta, która przechowuje bezpośrednio strukturę, jest po prostu agregacją zmiennych.

Myślę, że wiele zamieszania między strukturami a klasami wynika z faktu, że struktury mają dwa bardzo różne przypadki użycia, które powinny mieć bardzo różne wytyczne projektowe, ale wytyczne MS nie rozróżniają między nimi. Czasami istnieje potrzeba czegoś, co zachowuje się jak przedmiot; w takim przypadku wytyczne MS są całkiem rozsądne, chociaż „limit 16 bajtów” powinien prawdopodobnie być bardziej zbliżony do 24-32. Czasami jednak potrzebna jest agregacja zmiennych. Struktura używana w tym celu powinna po prostu składać się z kilku pól publicznych i prawdopodobnie plikuEquals przesłonięcia, ToStringprzesłonięcia iIEquatable(itsType).Equalsrealizacja. Struktury używane jako agregacje pól nie są obiektami i nie powinny udawać, że są. Z punktu widzenia struktury pole nie powinno oznaczać nic więcej ani mniej niż „ostatnia rzecz zapisana w tym polu”. Każde dodatkowe znaczenie powinno być określone przez kod klienta.

Na przykład, jeśli struktura agregująca zmienne ma członków Minimumi Maximum, sama struktura nie powinna tego obiecać Minimum <= Maximum. Kod, który otrzymuje taką strukturę jako parametr, powinien zachowywać się tak, jakby był przekazywany oddzielnie Minimumi Maximumwartości. Wymaganie, które Minimumnie jest większe niż Maximumpowinno być traktowane jako wymaganie, aby Minimumparametr nie był większy niż parametr przekazywany oddzielnie Maximum.

Przydatnym wzorcem do rozważenia jest czasami ExposedHolder<T>zdefiniowanie klasy, na przykład:

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

Jeśli ktoś ma List<ExposedHolder<someStruct>>, gdzie someStructjest strukturą agregującą zmienne, można zrobić takie rzeczy, jak myList[3].Value.someField += 7;, ale przekazanie myList[3].Valueinnemu kodowi da mu zawartość, Valuea nie da mu możliwości zmiany. Z drugiej strony, gdyby ktoś użył a List<someStruct>, należałoby użyć var temp=myList[3]; temp.someField += 7; myList[3] = temp;. Gdyby ktoś użył zmiennego typu klasy, ujawnienie zawartości myList[3]kodu z zewnątrz wymagałoby skopiowania wszystkich pól do innego obiektu. Gdyby ktoś użył niezmiennego typu klasy lub struktury „w stylu obiektowym”, należałoby skonstruować nową instancję, która byłaby podobna, myList[3]z someFieldtą różnicą, że była inna, a następnie zapisać tę nową instancję na liście.

Jedna dodatkowa uwaga: jeśli przechowujesz dużą liczbę podobnych rzeczy, dobrym rozwiązaniem może być przechowywanie ich w potencjalnie zagnieżdżonych tablicach struktur, najlepiej starając się zachować rozmiar każdej tablicy w przedziale od 1 KB do 64 KB lub więcej. Tablice struktur są szczególne, ponieważ indeksowanie da bezpośrednie odniesienie do struktury wewnątrz, więc można powiedzieć „a [12] .x = 5;”. Chociaż można zdefiniować obiekty podobne do tablic, C # nie pozwala im na współdzielenie takiej składni z tablicami.

supercat
źródło
1

Użyj klas.

Ogólnie rzecz biorąc. Dlaczego nie zaktualizować wartości b podczas ich tworzenia?

Preet Sangha
źródło
1

Z punktu widzenia c ++ zgadzam się, że modyfikowanie właściwości struktury będzie wolniejsze w porównaniu z klasą. Ale myślę, że odczytanie ich będzie szybsze, ponieważ struktura zostanie przydzielona na stosie zamiast na stercie. Odczytywanie danych ze stosu wymaga większej liczby sprawdzeń niż ze stosu.

Robert
źródło
0

Cóż, jeśli wybierzesz struct afterall, pozbądź się stringów i użyj buforu char lub bajt o stałym rozmiarze.

To jest re: wydajność.

Daniel Mošmondor
źródło