Czy zalecenie Microsoft dotyczące używania właściwości C # ma zastosowanie do tworzenia gier?


26

Rozumiem, że czasami potrzebujesz właściwości, takich jak:

public int[] Transitions { get; set; }

lub:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

Ale mam wrażenie, że przy tworzeniu gier, chyba że masz powód, aby zrobić inaczej, wszystko powinno być tak szybkie, jak to możliwe. Mam wrażenie, że z rzeczy, które przeczytałem, Unity 3D nie ma pewności, aby uzyskać bezpośredni dostęp za pośrednictwem właściwości, więc użycie właściwości spowoduje wywołanie dodatkowej funkcji.

Czy mam prawo stale ignorować sugestie Visual Studio dotyczące nieużywania pól publicznych? (Należy pamiętać, że korzystamy z int Foo { get; private set; }niego, gdy wykazanie zamierzonego użycia ma przewagę).

Wiem, że przedwczesna optymalizacja jest zła, ale jak powiedziałem, w rozwoju gier nie spowalniasz czegoś, gdy podobny wysiłek może przyspieszyć.


34
Zawsze sprawdzaj za pomocą narzędzia do profilowania, zanim uważasz, że coś jest „wolne”. Powiedziano mi, że coś jest „wolne”, a profilista powiedział mi, że musiał wykonać 500 000 000 razy, aby stracić pół sekundy. Ponieważ nigdy nie wykona się więcej niż kilkaset razy w ramce, zdecydowałem, że to nie ma znaczenia.
Almo

5
Odkryłem, że odrobina abstrakcji tutaj i tam pomaga szerzej stosować optymalizacje niskiego poziomu. Na przykład widziałem wiele zwykłych projektów C korzystających z różnego rodzaju list połączonych, które są strasznie wolne w porównaniu do przyległych tablic (np. Backing for ArrayList). Wynika to z faktu, że C nie ma generycznych, a ci programiści C prawdopodobnie łatwiej było wielokrotnie zaimplementować połączone listy niż robić to samo dla ArrayListsalgorytmu zmiany rozmiaru. Jeśli wpadniesz na dobrą optymalizację niskiego poziomu, obuduj ją!
hegel5000

3
@Almo Czy stosujesz tę samą logikę do operacji, które występują w 10 000 różnych miejscach? Ponieważ jest to dostęp członka klasy. Logicznie powinieneś pomnożyć koszt wydajności przez 10 000, zanim zdecydujesz, że klasa optymalizacji nie ma znaczenia. A 0,5 ms to lepsza zasada, gdy wydajność twojej gry zostanie zrujnowana, a nie pół sekundy. Twój punkt jest nadal aktualny, choć nie sądzę, aby właściwe działanie było tak jasne, jak się wydaje.
piojo

5
Masz 2 konie. Ścigaj się z nimi.
Maszt

@piojo nie. Pewna myśl musi zawsze dotyczyć tych rzeczy. W moim przypadku było to dla pętli w kodzie interfejsu użytkownika. Jedna instancja interfejsu użytkownika, kilka pętli, kilka iteracji. Dla przypomnienia skomentowałem twój komentarz. :)
Almo

Odpowiedzi:


44

Ale mam wrażenie, że przy tworzeniu gier, chyba że masz powód, aby zrobić inaczej, wszystko powinno być tak szybkie, jak to możliwe.

Niekoniecznie. Podobnie jak w oprogramowaniu aplikacyjnym, w grze znajduje się kod, który ma krytyczne znaczenie dla wydajności, a kod nie.

Jeśli kod jest wykonywany kilka tysięcy razy na ramkę, taka optymalizacja niskiego poziomu może mieć sens. (Chociaż możesz najpierw chcieć najpierw sprawdzić, czy rzeczywiście trzeba tak często go wywoływać. Najszybszym kodem jest kod, którego nie uruchamiasz)

Jeśli kod jest wykonywany raz na kilka sekund, to czytelność i łatwość konserwacji są znacznie ważniejsze niż wydajność.

Przeczytałem, że Unity 3D nie ma pewności, aby uzyskać bezpośredni dostęp za pośrednictwem właściwości, więc użycie właściwości spowoduje dodatkowe wywołanie funkcji.

Dzięki trywialnym właściwościom w stylu int Foo { get; private set; }możesz być pewien, że kompilator zoptymalizuje opakowanie właściwości. Ale:

  • jeśli faktycznie masz implementację, nie możesz być już tego taki pewien. Im bardziej złożona implementacja, tym mniejsze prawdopodobieństwo, że kompilator będzie w stanie ją wstawić.
  • Właściwości mogą ukrywać złożoność. To jest obosieczny miecz. Z jednej strony sprawia, że ​​kod jest bardziej czytelny i zmienny. Ale z drugiej strony inny programista, który uzyskuje dostęp do właściwości twojej klasy, może pomyśleć, że uzyskuje dostęp do jednej zmiennej i nie zdaje sobie sprawy, ile kodu faktycznie ukryłeś za tą właściwością. Kiedy właściwość zaczyna być kosztownie obliczeniowa, mam tendencję do przekształcania jej w jawną GetFoo()/ SetFoo(value)metodę: sugerować, że za kulisami dzieje się znacznie więcej. (lub jeśli muszę być jeszcze bardziej pewien, że inni dostaną podpowiedź: CalculateFoo()/ SwitchFoo(value))

Dostęp do pola w polu publicznym typu „nie tylko do odczytu” typu konstrukcji jest szybki, nawet jeśli pole to znajduje się w strukturze znajdującej się w strukturze itp., Pod warunkiem, że obiekt najwyższego poziomu jest klasą „nie tylko do odczytu” pole lub zmienna wolnostojąca nie tylko do odczytu. Konstrukcje mogą być dość duże bez negatywnego wpływu na wydajność, jeśli te warunki mają zastosowanie. Przerwanie tego wzoru spowoduje spowolnienie proporcjonalne do wielkości struktury.
supercat

5
„potem mam tendencję do przekształcania go w jawną metodę GetFoo () / SetFoo (wartość):” - Dobra uwaga, zwykle przenoszę ciężkie operacje z właściwości na metody.
Mark Rogers,

Czy istnieje sposób, aby potwierdzić, że kompilator Unity 3D dokonuje wspomnianej optymalizacji (w wersjach IL2CPP i Mono dla Androida)?
piojo

9
@piojo Podobnie jak w przypadku większości pytań dotyczących wydajności, najprostszą odpowiedzią jest „po prostu zrób to”. Masz gotowy kod - sprawdź różnicę między posiadaniem pola a posiadaniem właściwości. Szczegóły wdrożenia są warte zbadania tylko wtedy, gdy można zmierzyć istotną różnicę między tymi dwoma podejściami. Z mojego doświadczenia wynika, że ​​jeśli piszesz wszystko tak szybko, jak to możliwe, ograniczasz dostępne optymalizacje wysokiego poziomu i po prostu próbujesz ścigać się z częściami niskiego poziomu („nie widzę lasu dla drzew”). Np. Znalezienie sposobu na Updatecałkowite pominięcie pozwoli zaoszczędzić więcej czasu niż Updateszybkie.
Luaan

9

W razie wątpliwości skorzystaj z najlepszych praktyk.

Masz wątpliwości


To była łatwa odpowiedź. Rzeczywistość jest oczywiście bardziej złożona.

Po pierwsze, istnieje mit, że programowanie gier jest programowaniem o ultra wysokiej wydajności i wszystko musi być tak szybkie, jak to możliwe. Programowanie gier klasyfikuję jako programowanie zorientowane na wydajność . Deweloper musi być świadomy ograniczeń wydajności i pracować nad nimi.

Po drugie, istnieje mit, że sprawienie, by każda rzecz była tak szybka, jak to możliwe, powoduje, że program działa szybko. W rzeczywistości identyfikacja i optymalizacja wąskich gardeł sprawia, że ​​program jest szybki i jest to o wiele łatwiejsze / tańsze / szybsze do zrobienia, jeśli kod jest łatwy w utrzymaniu i modyfikacji; bardzo zoptymalizowany kod jest trudny do utrzymania i modyfikacji. Wynika z tego, że aby pisać wyjątkowo zoptymalizowane programy, musisz używać ekstremalnej optymalizacji kodu tak często, jak to konieczne i tak rzadko, jak to możliwe.

Po trzecie, zaletą właściwości i akcesoriów jest to, że są odporne na zmiany, a tym samym ułatwiają konserwację i modyfikację kodu - czego potrzebujesz do pisania szybkich programów. Chcesz zgłosić wyjątek, gdy ktoś chce ustawić wartość na zero? Użyj setera. Chcesz wysłać powiadomienie za każdym razem, gdy zmienia się wartość? Użyj setera. Chcesz zalogować nową wartość? Użyj setera. Chcesz zrobić leniwą inicjalizację? Użyj gettera.

Po czwarte, osobiście nie przestrzegam najlepszych praktyk Microsoftu w tym przypadku. Jeśli potrzebuję nieruchomości z trywialnymi i publicznymi obiektami pobierającymi i ustawiającymi, po prostu używam pola i po prostu zmieniam go na właściwość, kiedy jest to konieczne. Jednak bardzo rzadko potrzebuję tak trywialnej właściwości - o wiele bardziej powszechne jest to, że chcę sprawdzić warunki brzegowe lub ustawić prywatność setera.


2

Środowisko wykonawcze .net bardzo łatwo wstawia proste właściwości i często tak jest. Ale to wstawianie jest zwykle wyłączone przez profilerów, dlatego łatwo jest zostać wprowadzonym w błąd i myśleć, że zmiana własności publicznej na publiczną przyspieszy oprogramowanie.

W kodzie wywoływanym przez inny kod, który nie zostanie ponownie skompilowany, gdy kod zostanie ponownie skompilowany, uzasadnione jest użycie właściwości, ponieważ zmiana z pola na właściwość jest przełomową zmianą. Ale dla większości kodów, poza tym, że mogę ustawić punkt przerwania w get / set, nigdy nie widziałem korzyści z używania prostej właściwości w porównaniu z polem publicznym.

Głównym powodem, dla którego korzystałem z właściwości przez 10 lat jako profesjonalny programista C # było powstrzymanie innych programistów od mówienia mi, że nie przestrzegam standardu kodowania. To był dobry kompromis, ponieważ prawie nigdy nie istniał dobry powód, aby nie korzystać z nieruchomości.


2

Struktury i puste pola ułatwią współdziałanie z niektórymi niezarządzanymi interfejsami API. Często okazuje się, że interfejs API niskiego poziomu chce uzyskać dostęp do wartości przez odniesienie, co jest dobre dla wydajności (ponieważ unikamy niepotrzebnej kopii). Korzystanie z właściwości jest przeszkodą w tym i często biblioteki otoki wykonują kopie dla ułatwienia użytkowania, a czasem dla bezpieczeństwa.

Z tego powodu często uzyskuje się lepszą wydajność dzięki typom wektorów i macierzy, które nie mają właściwości, ale puste pola.


Najlepsze praktyki nie powstają w próżni. Pomimo pewnego kultu ładunków, ogólnie dobre praktyki istnieją z dobrego powodu.

W tym przypadku mamy kilka:

  • Właściwość umożliwia zmianę implementacji bez zmiany kodu klienta (na poziomie binarnym można zmienić pole na właściwość bez zmiany kodu klienta na poziomie źródłowym, jednak po zmianie kompiluje się do czegoś innego) . Oznacza to, że przy użyciu właściwości od samego początku kod, który odwołuje się do twojego, nie będzie musiał zostać ponownie skompilowany, aby zmienić to, co ta właściwość robi wewnętrznie.

  • Jeśli nie wszystkie możliwe wartości pól twojego typu są poprawnymi stanami, nie chcesz narażać ich na kod klienta, który kod go modyfikuje. Dlatego jeśli niektóre kombinacje wartości są nieprawidłowe, chcesz zachować pola prywatne (lub wewnętrzne).

Mówiłem kod klienta. To oznacza kod, który woła do twojego. Jeśli nie tworzysz biblioteki (lub nawet tworzysz bibliotekę, ale używasz wewnętrznej zamiast publicznej), zwykle możesz uciec od niej i trochę dobrej dyscypliny. W takiej sytuacji najlepsza praktyka korzystania z właściwości ma na celu zapobieganie strzelaniu sobie w stopę. Co więcej, o wiele łatwiej jest wnioskować o kodzie, jeśli widzisz wszystkie miejsca, w których pole może się zmienić w jednym pliku, zamiast martwić się czymkolwiek, czy nie jest modyfikowane gdzie indziej. W rzeczywistości właściwości są również dobre do ustalenia punktów przerwania, gdy zastanawiasz się, co poszło nie tak.


Tak, warto zobaczyć, co dzieje się w branży. Czy jednak masz motywację, by przeciwstawić się najlepszym praktykom? czy po prostu przeciwstawiasz się najlepszym praktykom - utrudniając zrozumienie kodu - tylko dlatego, że zrobił to ktoś inny? Ach, nawiasem mówiąc, „inni to robią” to sposób na rozpoczęcie kultu ładunków.


Więc ... Czy twoja gra działa wolno? Lepiej poświęć czas na znalezienie wąskiego gardła i naprawienie go, zamiast spekulacji, co to może być. Możesz być pewny, że kompilator dokona wielu optymalizacji, dlatego istnieje szansa, że ​​patrzysz na niewłaściwy problem.

Z drugiej strony, jeśli decydujesz, od czego zacząć, powinieneś martwić się najpierw o algorytmy i struktury danych, zamiast martwić się o mniejsze szczegóły, takie jak pola vs właściwości.


Wreszcie, czy zarabiasz coś, postępując wbrew najlepszym praktykom?

Czy w przypadku konkretnego przypadku (Unity i Mono dla Androida) Unity przyjmuje wartości przez odniesienie? Jeśli nie, skopiuje wartości i tak, nie ma tam wzrostu wydajności.

Jeśli tak, jeśli przekazujesz te dane do interfejsu API, który zajmuje ref. Czy ma sens upublicznienie pola, czy można sprawić, by typ mógł bezpośrednio wywoływać interfejs API?

Tak, oczywiście, mogą istnieć optymalizacje, które można wykonać, używając struktur z nagimi polami. Na przykład, uzyskujesz do nich dostęp za pomocą wskaźników Span<T> lub podobnych. Są również kompaktowe w pamięci, co ułatwia ich serializację w celu przesłania przez sieć lub umieszczenia w stałej pamięci (i tak, to są kopie).

A teraz, czy wybrałeś odpowiednie algorytmy i struktury, jeśli okażą się one wąskim gardłem, wtedy decydujesz, jaki jest najlepszy sposób, aby to naprawić ... które mogą być strukturami z nagimi polami lub nie. Będziesz mógł się tym martwić, jeśli i kiedy to się stanie. Tymczasem możesz martwić się ważniejszymi sprawami, takimi jak stworzenie dobrej lub fajnej gry, w którą warto grać.


1

... mam wrażenie, że przy tworzeniu gier, chyba że masz powód, aby zrobić inaczej, wszystko powinno być tak szybkie jak to możliwe.

:

Wiem, że przedwczesna optymalizacja jest zła, ale jak powiedziałem, w rozwoju gier nie spowalniasz czegoś, gdy podobny wysiłek może przyspieszyć.

Masz rację, wiedząc, że przedwczesna optymalizacja jest zła, ale to tylko połowa historii (patrz pełny cytat poniżej). Nawet przy tworzeniu gier nie wszystko musi być tak szybkie, jak to możliwe.

Najpierw spraw, by działało. Tylko wtedy zoptymalizuj bity, które tego potrzebują. Nie oznacza to, że pierwszy projekt może być bałaganiarski lub niepotrzebnie nieefektywny, ale optymalizacja nie jest priorytetem na tym etapie.

Prawdziwy problem polega na tym, że programiści spędzili zbyt wiele czasu martwiąc się o wydajność w niewłaściwych miejscach i w niewłaściwych momentach ; przedwczesna optymalizacja jest źródłem wszelkiego zła (a przynajmniej większości) w programowaniu”. Donald Knuth, 1974.


1

Jeśli chodzi o takie podstawowe rzeczy, optymalizator C # i tak to zrobi. Jeśli martwisz się o to (i martwisz się o to przez ponad 1% swojego kodu, robisz to źle ), atrybut został stworzony dla Ciebie. Ten atrybut [MethodImpl(MethodImplOptions.AggressiveInlining)]znacznie zwiększa szansę na wprowadzenie metody (metody pobierania i ustawiania są metodami).


0

Ujawnienie elementów typu struktury raczej jako właściwości niż pól często przyniesie słabą wydajność i semantykę, szczególnie w przypadkach, w których struktury są faktycznie traktowane jako struktury, a nie jako „dziwaczne obiekty”.

Struktura w .NET jest zbiorem pól połączonych taśmą razem i które dla niektórych celów mogą być traktowane jako całość. .NET Framework został zaprojektowany tak, aby zezwalać na używanie struktur w taki sam sposób, jak obiektów klasowych. Czasami może to być wygodne.

Wiele rad dotyczących struktur opiera się na przekonaniu, że programiści będą chcieli używać struktur jako obiektów, a nie jako połączonych ze sobą zbiorów pól. Z drugiej strony istnieje wiele przypadków, w których użyteczne może być posiadanie czegoś, co zachowuje się jak skupiona razem grupa pól. Jeśli tego właśnie potrzebujesz, porady dotyczące platformy .NET dodadzą niepotrzebnej pracy zarówno programistom, jak i kompilatorom, zapewniając gorszą wydajność i semantykę niż bezpośrednie używanie struktur.

Najważniejsze zasady, o których należy pamiętać podczas korzystania ze struktur, to:

  1. Nie przekazuj ani nie zwracaj struktur według wartości ani w inny sposób nie powoduj ich niepotrzebnego kopiowania.

  2. Jeśli zasada 1 zostanie zachowana, duże struktury będą działać tak samo dobrze, jak małe.

Jeśli Alphablobjest strukturą zawierającą 26 publicznych intpól nazwanych az, a nieruchomość getter abktóra zwraca sumę swoich aand bpól, a następnie podany Alphablob[] arr; List<Alphablob> list;kod int foo = arr[0].ab + list[0].ab;będzie trzeba czytać pól ai bna arr[0], ale trzeba czytać wszystkie 26 pola list[0], mimo że będzie zignoruj ​​wszystkie oprócz dwóch. Jeśli ktoś chciałby mieć ogólną kolekcję podobną do listy, która mogłaby wydajnie współpracować z podobną strukturą alphaBlob, należy zastąpić indeksowany getter metodą:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

który wtedy by się przywołał proc(ref backingArray[index], ref param);. Biorąc pod uwagę taki zbiór, jeśli jeden zastępuje sum = myCollection[0].ab;z

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

uniknęłoby to konieczności kopiowania części alphaBlob, które nie będą używane w module pobierania właściwości. Ponieważ przekazany delegat nie ma dostępu do niczego poza jego refparametrami, kompilator może przekazać statycznego delegata.

Niestety, .NET Framework nie definiuje żadnego rodzaju delegatów potrzebnych do poprawnego działania tej funkcji, a składnia jest w pewnym sensie bałaganem. Z drugiej strony takie podejście pozwala uniknąć niepotrzebnego kopiowania struktur, co z kolei sprawi, że praktyczne będzie wykonywanie czynności w miejscu na dużych strukturach przechowywanych w tablicach, co jest najbardziej wydajnym sposobem dostępu do pamięci.


Cześć, czy opublikowałeś swoją odpowiedź na niewłaściwej stronie?
piojo

@pojo: Być może powinienem był wyjaśnić, że celem odpowiedzi jest zidentyfikowanie sytuacji, w której użycie właściwości zamiast pól jest złe, i określenie, w jaki sposób można osiągnąć wydajność i zalety semantyczne pól, jednocześnie zachowując dostęp.
supercat

@piojo: Czy moja edycja wyjaśnia wszystko?
supercat

„będzie musiał przeczytać wszystkie 26 pól listy [0], nawet jeśli zignoruje wszystkie oprócz dwóch.” co? Jesteś pewny? Czy sprawdziłeś MSIL? C # prawie zawsze przekazuje typy klas przez referencje.
pjc50,

@ pjc50: Odczyt właściwości daje wynik według wartości. Jeśli zarówno indeksowany moduł pobierający, jak i moduł listpobierający właściwości absą wbudowane, może być możliwe, że JITter rozpozna tylko członków ai bsą potrzebne, ale nie jestem świadomy, że jakakolwiek wersja JITtera faktycznie robi takie rzeczy.
supercat
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.