Pytanie inżyniera na poziomie podstawowym dotyczące zarządzania pamięcią

9

Minęło kilka miesięcy, odkąd zacząłem pracę jako programista na poziomie podstawowym. Teraz, kiedy minęło już kilka krzywych uczenia się (np. Język, żargon, składnia VB i C #), zaczynam skupiać się na bardziej ezoterycznych tematach, jak pisać lepsze oprogramowanie.

Odpowiedzi na proste pytanie, które zadałem współpracownikowi, brzmiało: „Skupiam się na niewłaściwych rzeczach”. Chociaż szanuję tego współpracownika, nie zgadzam się, że jest to „niewłaściwa rzecz”, na której można się skupić.

Oto kod (w VB), a po nim pytanie.

Uwaga: Funkcja GenerateAlert () zwraca liczbę całkowitą.

Dim alertID as Integer = GenerateAlert()
_errorDictionary.Add(argErrorID, NewErrorInfo(Now(), alertID))    

vs...

 _errorDictionary.Add(argErrorID, New ErrorInfo(Now(), GenerateAlert()))

Pierwotnie napisałem to drugie i przepisałem go z „Dim alertID”, aby ktoś inny mógł łatwiej go odczytać. Ale oto moja troska i pytanie:

Gdyby napisać to przy pomocy Dim AlertID, faktycznie zajęłoby to więcej pamięci; skończone, ale więcej i czy tę metodę należy nazywać wiele razy, czy może to prowadzić do problemu? Jak .NET obsłuży ten obiekt AlertID. Poza platformą .NET należy ręcznie pozbyć się obiektu po użyciu (pod koniec części podrzędnej).

Chcę się upewnić, że zostanę doświadczonym programistą, który nie polega wyłącznie na wyrzucaniu elementów bezużytecznych. Czy ja o tym myślę? Czy skupiam się na niewłaściwych rzeczach?

Sean Hobbs
źródło
1
Mógłbym z łatwością stwierdzić, że jest w 100%, ponieważ pierwsza wersja jest bardziej czytelna. Założę się, że kompilator może nawet zająć się tym, czym się zajmujesz. Nawet jeśli nie, optymalizujesz przedwcześnie.
Przypon
6
Wcale nie jestem pewien, czy to naprawdę zużyłoby więcej pamięci z anonimową liczbą całkowitą vs. W każdym razie tak naprawdę jest to przedwczesna optymalizacja. Jeśli musisz się martwić wydajnością na tym poziomie (jestem prawie pewien, że nie), możesz potrzebować C ++ zamiast C #. Dobrze jest zrozumieć problemy z wydajnością i to, co dzieje się pod maską, ale to małe drzewo w dużym lesie.
psr
5
Nazwana vs anonimowa liczba całkowita nie zużyłaby więcej pamięci, zwłaszcza, że ​​anonimowa liczba całkowita jest tylko nazwaną liczbą całkowitą, której TY NIE nazwałeś (kompilator wciąż musi ją nazwać). Co najwyżej nazwana liczba całkowita miałaby inny zakres, aby mogła żyć dłużej. Anonimowa liczba całkowita będzie żyła tylko tak długo, jak potrzebna będzie jej metoda, a nazwana będzie żyła tak długo, jak jej rodzic jej potrzebował.
Joel Etherton,
Zobaczmy ... Jeśli liczba całkowita jest klasą, zostanie przydzielona na stercie. Zmienna lokalna (najprawdopodobniej na stosie) będzie zawierała odniesienie do niej. Odniesienie zostanie przekazane do obiektu errorDictionary. Jeśli środowisko wykonawcze wykonuje liczenie referencji (lub takie), to gdy nie będzie już żadnych referencji, to (obiekt) zostanie zwolniony ze sterty. Wszystko na stosie jest automatycznie „zwolnione” po wyjściu metody. Jeśli jest to prymitywne, (najprawdopodobniej) trafi na stos.
Paul
Twój kolega miał rację: kwestia podniesiona przez twoje pytanie nie powinna dotyczyć optymalizacji, ale czytelności .
haylem,

Odpowiedzi:

25

„Przedwczesna optymalizacja jest źródłem wszelkiego zła (a przynajmniej większości) w programowaniu”. - Donald Knuth

Jeśli chodzi o pierwsze przejście, po prostu napisz kod, aby był poprawny i czysty. Jeśli później zostanie ustalone, że Twój kod ma krytyczne znaczenie dla wydajności (istnieją narzędzia do określania tego mianem profilerów), można go ponownie zapisać. Jeśli kod nie ma decydującego znaczenia dla wydajności, czytelność jest o wiele ważniejsza.

Czy warto zagłębić się w te tematy dotyczące wydajności i optymalizacji? Oczywiście, ale nie na dolara twojej firmy, jeśli jest to niepotrzebne.

Christopher Berman
źródło
1
Na kim jeszcze powinien być dolar? Twój pracodawca korzysta bardziej ze wzrostu umiejętności niż ty.
Marcin
Tematy, które nie przyczyniają się bezpośrednio do bieżącego zadania? Powinieneś realizować te rzeczy we własnym czasie. Gdybym usiadł i zbadał każdy przedmiot CompSci, który wzbudził moją ciekawość w ciągu dnia, nic bym nie zrobił. Po to są moje wieczory.
Christopher Berman
2
Dziwne. Niektórzy z nas mają życie osobiste i, jak mówię, pracodawca korzysta przede wszystkim z badań. Kluczem do sukcesu jest nie spędzanie na tym całego dnia.
Marcin
6
Dobrze dla ciebie. Jednak tak naprawdę nie jest to reguła uniwersalna. Ponadto, jeśli zniechęcasz swoich pracowników do nauki w pracy, wszystko, co robisz, to zniechęcanie ich do nauki i zachęcasz ich do znalezienia innego pracodawcy, który faktycznie płaci za rozwój pracowników.
Marcin
2
Rozumiem opinie odnotowane w powyższych komentarzach; Chciałbym zauważyć, że zapytałem podczas przerwy na lunch. :) Jeszcze raz dziękuję wszystkim za wkład tutaj i na stronie Stack Exchange; to jest bezcenne!
Sean Hobbs,
5

Dla przeciętnego programu .NET tak, to jest przesadne myślenie. Może się zdarzyć, że będziesz chciał przejść do sedna tego, co dzieje się w .NET, ale jest to stosunkowo rzadkie.

Jedną z trudnych zmian, jakie miałem, było przejście z używania C i MASM na programowanie w klasycznym VB w latach 90-tych. Przyzwyczaiłem się do optymalizacji wszystkiego pod kątem wielkości i prędkości. Musiałem w większości zrezygnować z tego myślenia i pozwolić VB robić to, aby było skuteczne.

jfrankcarr
źródło
5

Jak mawiał mój współpracownik:

  1. Niech to zadziała
  2. Napraw wszystkie błędy, aby działały bezbłędnie
  3. Zrób to SOLIDNYM
  4. Zastosuj optymalizację, jeśli działa wolno

Innymi słowy, zawsze pamiętaj o KISS (niech to będzie głupie). Ponieważ nadmierna inżynieria, nadmierne myślenie pewnej logiki kodu może stanowić problem przy zmianie logiki następnym razem. Jednak utrzymywanie kodu w czystości i prostocie jest zawsze dobrą praktyką .

Jednak z czasem i doświadczeniem lepiej wiesz, który kod pachnie i wkrótce będziesz potrzebował optymalizacji.

Jusubow
źródło
3

Należy napisać to z Dim AlertID

Czytelność jest ważna. W przykładzie, choć nie jestem pewien, że jesteś naprawdę czyni rzeczy , które stają się bardziej czytelne. GenerateAlert () ma dobrą nazwę i nie powoduje dużego hałasu. Prawdopodobnie są lepsze sposoby wykorzystania twojego czasu.

w rzeczywistości zajmowałoby więcej pamięci;

Podejrzewam, że nie. Jest to stosunkowo prosta optymalizacja dla kompilatora.

czy tę metodę należy wywoływać wiele razy, czy może to prowadzić do problemów?

Użycie zmiennej lokalnej jako pośrednika nie ma wpływu na moduł wyrzucania elementów bezużytecznych. Jeśli pamięć generowana przez GenerateAlert () new, będzie miała znaczenie. Ale to będzie miało znaczenie niezależnie od lokalnej zmiennej, czy nie.

Jak .NET obsłuży ten obiekt AlertID.

AlertID nie jest obiektem. Wynikiem GenerateAlert () jest obiekt. AlertID to zmienna, która jeśli jest zmienną lokalną, jest po prostu przestrzenią powiązaną z metodą śledzenia rzeczy.

Poza platformą .NET należy ręcznie pozbyć się obiektu po użyciu

Jest to trudniejsze pytanie, które zależy od kontekstu i semantyki własności instancji podanej przez GenerateAlert (). Ogólnie rzecz biorąc, cokolwiek utworzyło instancję, należy ją usunąć. Twój program prawdopodobnie wyglądałby znacznie inaczej, gdyby został zaprojektowany z myślą o ręcznym zarządzaniu pamięcią.

Chcę się upewnić, że zostanę doświadczonym programistą, który nie polega tylko na usuwaniu śmieci. Czy ja o tym myślę? Czy skupiam się na niewłaściwych rzeczach?

Dobry programista wykorzystuje dostępne dla nich narzędzia, w tym śmietnik. Lepiej jest przemyśleć rzeczy, niż żyć nieświadomym. Być może skupiasz się na niewłaściwych rzeczach, ale skoro tu jesteśmy, równie dobrze możesz się o tym dowiedzieć.

Telastyn
źródło
2

Spraw, aby działał, czyść go, uczyń go SOLIDNYM, A NASTĘPNIE spraw, aby działał tak szybko, jak powinien .

To powinna być normalna kolejność rzeczy. Twoim pierwszym priorytetem jest stworzenie czegoś, co przejdzie testy akceptacyjne, które wstrząsną wymaganiami. To jest twój pierwszy priorytet, ponieważ jest to pierwszy priorytet twojego klienta; spełnianie wymagań funkcjonalnych w terminach rozwoju. Kolejnym priorytetem jest napisanie czystego, czytelnego kodu, który jest łatwy do zrozumienia, a zatem może być utrzymywany przez potomność bez żadnych WTF, gdy stanie się to konieczne (prawie nigdy nie jest to kwestia „jeśli”; ty lub ktoś po tobie będzie musiał odejść wróć i zmień / napraw coś). Trzecim priorytetem jest dostosowanie kodu do metodologii SOLID (lub GRASP, jeśli wolisz), która umieszcza kod w modułowych, wielokrotnego użytku, wymiennych częściach, które ponownie pomagają w utrzymaniu (nie tylko mogą zrozumieć, co zrobiłeś i dlaczego, ale są czyste linie, wzdłuż których mogę chirurgicznie usunąć i wymienić fragmenty kodu). Ostatnim priorytetem jest wydajność; jeśli kod jest na tyle ważny, że musi być zgodny ze specyfikacjami wydajności, prawie na pewno jest wystarczająco ważny, aby najpierw był poprawny, czysty i SOLIDNY.

Nawiązując do Christophera (i Donalda Knutha), „przedwczesna optymalizacja jest źródłem wszelkiego zła”. Ponadto rozważane rodzaje optymalizacji są niewielkie (odniesienie do nowego obiektu zostanie utworzone na stosie, niezależnie od tego, czy nadasz mu nazwę w kodzie źródłowym, czy nie), a także typu, który nie może powodować żadnych różnic w kompilacji IL. Nazwy zmiennych nie są przenoszone do IL, więc ponieważ deklarujesz zmienną tuż przed jej pierwszym (i prawdopodobnie jedynym) użyciem, postawiłbym trochę pieniędzy na piwo, że IL jest identyczna między Twoimi dwoma przykładami. Twój współpracownik ma więc 100% racji; patrzysz w niewłaściwym miejscu, jeśli patrzysz na zmienną nazwaną vs instancję wbudowaną, aby coś zoptymalizować.

Mikrooptymalizacje w .NET prawie nigdy nie są tego warte (mówię o 99,99% przypadków). Być może w C / C ++ JEŚLI wiesz, co robisz. Pracując w środowisku .NET, jesteś już wystarczająco daleko od metalu sprzętu, co powoduje znaczne obciążenie podczas wykonywania kodu. Biorąc pod uwagę, że jesteś już w środowisku, które wskazuje, że zrezygnowałeś z błyskawicznej prędkości i zamiast tego chcesz napisać „poprawny” kod, jeśli coś w środowisku .NET naprawdę nie działa wystarczająco szybko, albo jego złożoność jest zbyt wysoko, lub powinieneś rozważyć równoległe ustawienie. Oto kilka podstawowych wskazówek dotyczących optymalizacji; Gwarantuję Ci, że Twoja optymalizacja wydajności (prędkość uzyskana z poświęconego czasu) gwałtownie wzrośnie:

  • Zmiana kształtu funkcji ma znaczenie bardziej niż zmiana współczynników - złożoność WRT Big-Oh, można zmniejszyć o połowę liczbę kroków, które należy wykonać w algorytmie N 2 , a mimo to algorytm złożoności kwadratowej jest wykonywany nawet w o połowę mniej niż kiedyś. Jeśli jest to dolna granica złożoności dla tego rodzaju problemu, niech tak będzie, ale jeśli istnieje NlogN, liniowe lub logarytmiczne rozwiązanie tego samego problemu, zyskasz więcej, zmieniając algorytmy w celu zmniejszenia złożoności, niż optymalizując ten, który masz.
  • To, że nie widzisz złożoności, nie oznacza, że ​​to Cię nie kosztuje - wiele z najbardziej eleganckich jedno-liniowych słów tego słowa działa strasznie (na przykład sprawdzanie liczb pierwszych Regex jest funkcją złożoności wykładniczej, a jednocześnie wydajną pierwsza ocena polegająca na podzieleniu liczby przez wszystkie liczby pierwsze mniejsze niż pierwiastek kwadratowy jest rzędu O (Nlog (sqrt (N))). Linq to świetna biblioteka, ponieważ upraszcza kod, ale w przeciwieństwie do silnika SQL, .Net kompilator nie będzie próbował znaleźć najbardziej wydajnego sposobu wykonania zapytania. Musisz wiedzieć, co się stanie, gdy użyjesz metody, a zatem dlaczego metoda może być szybsza, jeśli zostanie umieszczona wcześniej (lub później) w łańcuchu, podczas tworzenia te same wyniki.
  • OTOH, prawie zawsze istnieje kompromis między złożonością źródła a złożonością środowiska wykonawczego - SelectionSort jest bardzo łatwy do wdrożenia; prawdopodobnie możesz to zrobić w 10LOC lub mniej. MergeSort jest nieco bardziej złożony, Quicksort bardziej, a RadixSort jeszcze bardziej. Jednak wraz ze wzrostem złożoności algorytmów (a tym samym z czasem programowania „z góry”) zmniejsza się złożoność środowiska wykonawczego; MergeSort i QuickSort to NlogN, a RadixSort jest ogólnie uważany za liniowy (technicznie jest to NlogM, gdzie M jest największą liczbą w N).
  • Przerwij szybko - jeśli istnieje czek, który można wykonać niedrogo, który najprawdopodobniej będzie prawdziwy i oznacza, że ​​możesz przejść dalej, wykonaj go najpierw. Jeśli na przykład twój algorytm dba tylko o liczby kończące się na 1, 2 lub 3, najbardziej prawdopodobnym przypadkiem (w przypadku danych całkowicie losowych) jest liczba, która kończy się na innej cyfrze, więc sprawdź, czy liczba NIE kończy się na 1, 2 lub 3 przed sprawdzeniem, czy liczba kończy się na 1, 2 lub 3. Jeśli element logiczny wymaga A i B, a P (A) = 0,9, a P (B) = 0,1, to sprawdź Najpierw B, chyba że jeśli! A, to! B (jak if(myObject != null && myObject.someProperty == 1)) lub B zajmuje więcej niż 9 razy dłużej niż A do oceny ( if(myObject != null && some10SecondMethodReturningBool())).
  • Nie zadawaj żadnych pytań, na które znasz już odpowiedź - jeśli masz serię warunków awaryjnych, a jeden lub więcej z tych warunków zależy od prostszego warunku, który również należy sprawdzić, nigdy nie sprawdzaj obu te niezależnie. Na przykład, jeśli masz czek, który wymaga A, i czek, który wymaga A i B, powinieneś sprawdzić A, a jeśli to prawda, powinieneś sprawdzić B. Jeśli! A, to! A i& B, więc nawet nie zawracaj sobie tym głowy.
  • Im więcej razy coś robisz, tym bardziej powinieneś zwracać uwagę na to, jak to się robi - Jest to wspólny motyw w rozwoju, na wielu poziomach; w ogólnym sensie rozwoju: „jeśli wspólne zadanie jest czasochłonne lub kłopotliwe, rób to, dopóki nie będziesz zarówno sfrustrowany, jak i posiadasz wystarczającą wiedzę, aby znaleźć lepszy sposób”. W kategoriach kodowych, im więcej razy uruchamiany jest nieefektywny algorytm, tym więcej zyskasz na ogólnej wydajności dzięki optymalizacji. Istnieją narzędzia do profilowania, które mogą wziąć zestaw binarny i jego symbole debugowania i pokazać, po przejrzeniu niektórych przypadków użycia, jakie wiersze kodu zostały uruchomione najbardziej. Te linie i linie, które je prowadzą, są tym, na co powinieneś zwrócić największą uwagę, ponieważ każdy wzrost wydajności, który tam osiągniesz, zostanie zwielokrotniony.
  • Bardziej złożony algorytm wygląda jak mniej złożony algorytm, jeśli wrzuci się w niego wystarczającą ilość sprzętu . Są chwile, w których musisz sobie uświadomić, że twój algorytm zbliża się do technicznych ograniczeń systemu (lub jego części), na którym go uruchamiasz; od tego momentu, jeśli musi być szybszy, zyskasz więcej, po prostu uruchamiając go na lepszym sprzęcie. Dotyczy to również równoległości; N 2 algorytm -complexity, kiedy uruchamiane na N rdzeni, wygląda liniowych. Jeśli więc masz pewność, że osiągnąłeś niższy poziom złożoności dla rodzaju pisanego algorytmu, poszukaj sposobów na „dzielenie i podbijanie”.
  • Jest szybki, gdy jest wystarczająco szybki - o ile nie pakujesz ręcznie zestawu do celowania w konkretny układ, zawsze można coś zyskać. Jednakże, chyba że CHCESZ być ręcznym pakowaniem, zawsze musisz pamiętać, co klient nazwałby „wystarczająco dobrym”. Ponownie „przedwczesna optymalizacja jest źródłem wszelkiego zła”; kiedy twój klient nazywa to wystarczająco szybko, robisz to, dopóki on nie uważa, że ​​to wystarczająco szybko.
KeithS
źródło
0

Jedynym czasem, gdy trzeba się wcześnie martwić optymalizacją, jest to, że wiesz, że masz do czynienia z czymś, co jest albo ogromne, albo że coś, co wiesz, wykona się ogromną liczbę razy.

Definicja „ogromnego” oczywiście różni się w zależności od tego, jakie są twoje systemy docelowe.

Loren Pechtel
źródło
0

Wolałbym wersję dwuliniową po prostu dlatego, że łatwiej jest przejść przez debugger. Linia z kilkoma wbudowanymi połączeniami utrudnia.

Slapout
źródło