Późno przyszedł do tego pytania i odpowiedzi z już świetnymi odpowiedziami, ale chciałem się wtrącić jako obcokrajowiec przyzwyczajony do patrzenia na rzeczy z niższego poziomu bitów i bajtów w pamięci.
Jestem bardzo podekscytowany niezmiennymi projektami, nawet pochodzącymi z perspektywy C oraz z perspektywy znalezienia nowych sposobów skutecznego programowania tego bestialskiego sprzętu, jaki mamy obecnie.
Wolniej / szybciej
Jeśli chodzi o pytanie, czy to spowalnia sytuację, odpowiedzią byłaby robot yes
. Na tym bardzo technicznym poziomie koncepcyjnym niezmienność może tylko spowolnić sytuację. Sprzęt działa najlepiej, gdy nie sporadycznie przydziela pamięć i może po prostu zmodyfikować istniejącą pamięć (dlaczego mamy takie pojęcia, jak lokalizacja czasowa).
Jednak praktyczna odpowiedź brzmi maybe
. Wydajność nadal jest w dużej mierze wskaźnikiem wydajności w każdej nietrywialnej bazie kodu. Zazwyczaj okropne w utrzymaniu bazy kodów potykające się o warunki wyścigowe nie są najbardziej wydajne, nawet jeśli pomijamy błędy. Wydajność jest często funkcją elegancji i prostoty. Szczyt mikrooptymalizacji może nieco konfliktować, ale zwykle są one zarezerwowane dla najmniejszych i najbardziej krytycznych sekcji kodu.
Przekształcanie niezmiennych bitów i bajtów
Z punktu widzenia niskiego poziomu, jeśli prześwietlamy koncepcje rentgenowskie takie jak objects
i strings
tak dalej, w centrum tego są tylko bity i bajty w różnych formach pamięci o różnych charakterystykach prędkości / rozmiaru (szybkość i rozmiar pamięci zwykle są wzajemnie się wykluczające).
Hierarchia pamięci komputera podoba się, gdy wielokrotnie uzyskujemy dostęp do tego samego fragmentu pamięci, jak na powyższym schemacie, ponieważ utrzyma ten często używany fragment pamięci w najszybszej formie pamięci (pamięć podręczna L1, np. jest prawie tak szybki jak rejestr). Możemy wielokrotnie uzyskiwać dostęp do dokładnie tej samej pamięci (wielokrotnie ją wykorzystywać) lub wielokrotnie uzyskiwać dostęp do różnych sekcji fragmentu (np. Zapętlanie elementów w ciągłym fragmencie, który wielokrotnie uzyskuje dostęp do różnych części tego fragmentu pamięci).
W rezultacie rzucamy kluczem w tym procesie, jeśli modyfikacja tej pamięci kończy się na chęci utworzenia z boku zupełnie nowego bloku pamięci, w ten sposób:
... w tym przypadku dostęp do nowego bloku pamięci może wymagać obowiązkowych błędów strony i braków pamięci podręcznej, aby przenieść ją z powrotem do najszybszych form pamięci (aż do rejestru). To może być prawdziwy zabójca wydajności.
Istnieją jednak sposoby na złagodzenie tego, używając już zarezerwowanej puli wstępnie przydzielonej pamięci.
Duże kruszywa
Innym zagadnieniem koncepcyjnym, które wynika z nieco wyższego poziomu, jest po prostu tworzenie niepotrzebnych kopii naprawdę dużych agregatów.
Aby uniknąć zbyt skomplikowanego schematu, wyobraźmy sobie, że ten prosty blok pamięci był w jakiś sposób drogi (być może znaki UTF-32 na niewiarygodnie ograniczonym sprzęcie).
W takim przypadku, jeśli chcielibyśmy zastąpić „HELP” słowem „KILL”, a ten blok pamięci był niezmienny, musielibyśmy stworzyć cały nowy blok w całości, aby stworzyć unikalny nowy obiekt, nawet jeśli zmieniły się tylko jego części :
Rozciągając nieco naszą wyobraźnię, ten rodzaj głębokiej kopii wszystkiego innego, aby uczynić jedną małą część wyjątkową, może być dość kosztowny (w rzeczywistych przypadkach ten blok pamięci byłby o wiele, wiele większy, aby stanowić problem).
Jednak pomimo takiego kosztu, ten rodzaj konstrukcji będzie znacznie mniej podatny na błędy ludzkie. Każdy, kto pracował w funkcjonalnym języku z czystymi funkcjami, może to docenić, szczególnie w przypadku wielowątkowości, w których możemy wielowątkowość takiego kodu bez opieki na świecie. Ogólnie rzecz biorąc, programiści-ludzie mają tendencję do potykania się o zmiany stanu, szczególnie te, które powodują zewnętrzne skutki uboczne dla stanów poza zakresem bieżącej funkcji. Nawet odzyskanie po zewnętrznym błędzie (wyjątku) w takim przypadku może być niezwykle trudne ze zmiennymi zmianami stanu zewnętrznego w miksie.
Jednym ze sposobów złagodzenia tego zbędnego kopiowania jest przekształcenie tych bloków pamięci w kolekcję wskaźników (lub odniesień) do znaków, takich jak:
Przepraszam, nie zdawałem sobie sprawy, że nie musimy robić L
wyjątkowych rzeczy podczas tworzenia diagramu.
Niebieski oznacza płytkie skopiowane dane.
... niestety, byłoby niezwykle kosztownie zapłacić wskaźnik / koszt odniesienia za znak. Co więcej, możemy rozproszyć zawartość znaków w całej przestrzeni adresowej i ostatecznie zapłacić za nią w postaci mnóstwa błędów stron i braków w pamięci podręcznej, co z łatwością czyni to rozwiązanie jeszcze gorszym niż kopiowanie całości w całości.
Nawet jeśli staraliśmy się przydzielić te znaki w sposób ciągły, powiedzmy, że maszyna może załadować 8 znaków i 8 wskaźników do znaku w linii pamięci podręcznej. W końcu ładujemy pamięć w taki sposób, aby przejść przez nowy ciąg:
W tym przypadku wymagamy załadowania 7 różnych linii pamięci podręcznej ciągłej pamięci, aby przejść przez ten ciąg, gdy idealnie potrzebujemy tylko 3.
Chunk Up The Data
Aby złagodzić powyższy problem, możemy zastosować tę samą podstawową strategię, ale na grubszym poziomie 8 znaków, np
Wynik wymaga załadowania danych o wartości 4 linii pamięci podręcznej (1 dla 3 wskaźników i 3 dla znaków), aby przejść przez ten ciąg, który jest tylko o 1 od teoretycznego optymalnego.
Więc to wcale nie jest takie złe. Trochę marnotrawstwa pamięci, ale pamięć jest obfita, a zużywanie jej więcej nie spowalnia rzeczy, jeśli dodatkowa pamięć będzie po prostu zimnymi danymi, do których często nie ma dostępu. To tylko dla gorących, ciągłych danych, w których zmniejszone zużycie pamięci i szybkość często idą w parze, gdzie chcemy zmieścić więcej pamięci w jednej stronie lub linii pamięci podręcznej i uzyskać dostęp do wszystkich przed eksmisją. Ta reprezentacja jest dość przyjazna dla pamięci podręcznej.
Prędkość
Tak więc użycie takiej reprezentacji jak wyżej może dać całkiem przyzwoitą równowagę wydajności. Prawdopodobnie najbardziej krytyczne z punktu widzenia wydajności zastosowania niezmiennych struktur danych przyjmą ten charakter modyfikacji masywnych fragmentów danych i uczynienia ich wyjątkowymi w tym procesie, przy jednoczesnym płytkim kopiowaniu niezmodyfikowanych fragmentów. Wymaga to również narzutu operacji atomowych, aby bezpiecznie odwoływać się do płytko skopiowanych fragmentów w kontekście wielowątkowym (być może przy trwającym pewnym zliczaniu atomowym).
Jednak dopóki te masywne fragmenty danych są reprezentowane na wystarczająco grubym poziomie, duża część tego narzutu zmniejsza się, a być może nawet jest trywializowana, a jednocześnie zapewnia nam bezpieczeństwo i łatwość kodowania i wielowątkowości większej liczby funkcji w czystej postaci bez strony zewnętrznej efekty.
Przechowywanie nowych i starych danych
Moim zdaniem niezmienność jest potencjalnie najbardziej pomocna z punktu widzenia wydajności (w sensie praktycznym), kiedy możemy ulec pokusie tworzenia całych kopii dużych danych, aby uczynić je wyjątkowymi w zmiennym kontekście, w którym celem jest stworzenie czegoś nowego z coś, co już istnieje w taki sposób, że chcemy zachować zarówno nowe, jak i stare, kiedy moglibyśmy po prostu robić małe kawałki tego wyjątkowego dzięki starannemu niezmiennemu projektowi.
Przykład: Cofnij system
Przykładem tego jest system cofania. Możemy zmienić niewielką część struktury danych i chcieć zachować zarówno pierwotną formę, którą możemy cofnąć, jak i nową formę. Dzięki tego rodzaju niezmiennemu projektowi, który sprawia, że małe, zmodyfikowane sekcje struktury danych są unikalne, możemy po prostu przechowywać kopię starych danych w pozycji cofania, płacąc jedynie koszt pamięci dodanych danych unikatowych części. Zapewnia to bardzo skuteczną równowagę wydajności (sprawienie, że wdrożenie systemu cofania jest dziecinnie proste) i wydajności.
Interfejsy wysokiego poziomu
Jednak w powyższej sprawie powstaje coś niezręcznego. W lokalnym kontekście funkcji zmienne dane są często najłatwiejsze i najłatwiejsze do zmodyfikowania. W końcu najłatwiejszym sposobem modyfikacji tablicy jest po prostu zapętlenie jej i zmodyfikowanie jednego elementu na raz. Możemy skończyć ze zwiększeniem narzutu intelektualnego, gdybyśmy mieli do wyboru dużą liczbę algorytmów wysokiego poziomu do transformacji tablicy i musieliśmy wybrać odpowiedni, aby upewnić się, że wszystkie te masywne płytkie kopie zostaną wykonane, a zmodyfikowane części są uczyniony wyjątkowym.
Prawdopodobnie najłatwiejszym sposobem w takich przypadkach jest użycie zmiennych buforów lokalnie w kontekście funkcji (gdzie zwykle nas nie wyzwalają), które dokonują atomowych zmian w strukturze danych, aby uzyskać nową niezmienną kopię (wierzę, że niektóre języki nazywają te „stany przejściowe”) ...
... lub możemy po prostu modelować funkcje transformacji wyższego i wyższego poziomu na danych, abyśmy mogli ukryć proces modyfikowania modyfikowalnego bufora i przypisywania go do struktury bez włączania logiki zmiennej. W każdym razie nie jest to jeszcze obszar szeroko badany, a nasza praca zostanie przerwana, jeśli przyjmiemy bardziej niezmienne projekty, aby wymyślić sensowne interfejsy do przekształcania tych struktur danych.
Struktury danych
Inną rzeczą, która się tutaj pojawia, jest to, że niezmienność stosowana w kontekście krytycznym dla wydajności prawdopodobnie spowoduje, że struktury danych rozpadną się na masywne dane, w których fragmenty nie są zbyt małe, ale też nie są zbyt duże.
Listy połączone mogą chcieć nieco zmienić, aby to dostosować i zamienić w listy rozwijane. Duże, ciągłe tablice mogą przekształcić się w tablicę wskaźników w ciągłe fragmenty z indeksowaniem modulo dla losowego dostępu.
Potencjalnie zmienia to sposób, w jaki patrzymy na struktury danych w ciekawy sposób, jednocześnie popychając funkcje modyfikujące te struktury danych, aby przypominały nieporęczną naturę, aby ukryć dodatkową złożoność w płytkim kopiowaniu niektórych bitów tutaj i czyniąc inne bity wyjątkowymi.
Występ
W każdym razie to mój mały pogląd na ten temat na niższym poziomie. Teoretycznie niezmienność może kosztować od bardzo dużych do mniejszych. Ale bardzo teoretyczne podejście nie zawsze przyspiesza działanie aplikacji. Może to uczynić je skalowalnymi, ale rzeczywista prędkość często wymaga przyjęcia bardziej praktycznego sposobu myślenia.
Z praktycznego punktu widzenia takie cechy, jak wydajność, łatwość konserwacji i bezpieczeństwo zwykle zamieniają się w jedno duże rozmycie, szczególnie w przypadku bardzo dużej bazy kodu. Podczas gdy wydajność w pewnym sensie absolutnym jest obniżana z niezmiennością, trudno jest argumentować o korzyściach, jakie ma ona w zakresie wydajności i bezpieczeństwa (w tym bezpieczeństwa wątków). Wraz z ich wzrostem często może nastąpić wzrost praktycznej wydajności, choćby dlatego, że programiści mają więcej czasu na dostrojenie i optymalizację kodu bez roju błędów.
Sądzę więc, że z tego praktycznego punktu widzenia niezmienne struktury danych mogą w rzeczywistości pomóc w wydajności w wielu przypadkach, jakkolwiek dziwnie to brzmi. Idealny świat może poszukiwać kombinacji tych dwóch: niezmiennych struktur danych i zmiennych, przy czym zmienne zwykle są bardzo bezpieczne w użyciu w bardzo lokalnym zasięgu (np. Lokalne dla funkcji), podczas gdy niezmienne mogą uniknąć strony zewnętrznej efekty wprost i zmień wszystkie zmiany w strukturę danych w operację atomową, tworząc nową wersję bez ryzyka warunków wyścigowych.