Tło
Odwiedziłem starą (ale świetną) stronę, na której nie byłem od wieków - Alioth Language Shootout ( http://benchmarksgame.alioth.debian.org/ ).
Zacząłem programować w C / C ++ kilka lat temu, ale od tego czasu pracuję prawie wyłącznie w Javie z powodu ograniczeń językowych w projektach, w których byłem zaangażowany. Nie pamiętając liczb, chciałem zobaczyć, w przybliżeniu, jak dobrze Java poradził sobie z C / C ++ pod względem zużycia zasobów.
Te czasy wykonania były nadal stosunkowo dobra, z Java w najgorszym wykonywania 4x wolniej niż C / C ++, ale średnio około (lub poniżej) 2x. Z uwagi na naturę implementacji samej Javy nie było to zaskoczeniem, a jej wydajność była w rzeczywistości niższa niż się spodziewałem.
Prawdziwą cegłą był przydział pamięci - w najgorszym przypadku Java przydzieliła:
- aż 52 razy więcej pamięci niż C.
- i 25 razy więcej niż C ++.
52x pamięć ... Absolutnie paskudna, prawda? ... Albo to jest? Pamięć jest teraz stosunkowo tania.
Pytanie:
Jeśli nie mówimy o platformach docelowych z surowymi ograniczeniami pamięci operacyjnej (tj. Systemy wbudowane i tym podobne), czy użycie pamięci powinno być problemem przy dzisiejszym wyborze języka ogólnego?
Pytam częściowo, ponieważ rozważam migrację do Scali jako mojego głównego języka. Bardzo podoba mi się jego funkcjonalny aspekt, ale z tego, co widzę, jest jeszcze droższy pod względem pamięci niż Java. Ponieważ jednak z roku na rok pamięć wydaje się coraz szybsza, tańsza i obfitsza (wydaje się, że coraz trudniej jest znaleźć laptopa konsumenckiego bez co najmniej 4 GB pamięci RAM DDR3), czy nie można argumentować, że zarządzanie zasobami staje się coraz bardziej nieistotne w porównaniu z (prawdopodobnie kosztownymi pod względem implementacji) funkcjami języka wysokiego poziomu, które pozwalają na szybszą budowę bardziej czytelnych rozwiązań?
Odpowiedzi:
Zarządzanie pamięcią jest niezwykle istotne, ponieważ decyduje o tym, jak szybko coś się pojawia, nawet jeśli coś ma dużo pamięci. Najlepszym i najbardziej kanonicznym przykładem są gry z tytułem AAA, takie jak Call of Duty lub Bioshock. Są to aplikacje działające w czasie rzeczywistym, wymagające ogromnej kontroli w zakresie optymalizacji i użytkowania. Problemem nie jest samo użycie, ale zarządzanie.
Wszystko sprowadza się do dwóch słów: Garbage Collection. Algorytmy wyrzucania elementów bezużytecznych mogą powodować niewielkie problemy z wydajnością, a nawet powodować zawieszanie się aplikacji przez sekundę lub dwie. Głównie nieszkodliwy w aplikacji księgowej, ale potencjalnie rujnujący pod względem doświadczenia użytkownika w grze Call of Duty. Dlatego w aplikacjach, w których liczy się czas, języki odśmiecane mogą być bardzo problematyczne. Jest to na przykład jeden z celów projektowych Squirrel, który stara się zaradzić problemowi, jaki ma Lua z GC, stosując zamiast tego liczenie referencji.
Czy to bardziej ból głowy? Jasne, ale jeśli potrzebujesz precyzyjnej kontroli, godzisz się z tym.
źródło
Czy rozumiesz liczby, na których opierasz swoje pytanie?
Kiedy występuje duża rozbieżność między tymi programami Java i C, jest to głównie domyślny przydział pamięci JVM w porównaniu do wszystkiego, czego potrzebuje libc:
Program Java n-body 13 996 KB :: program C 320 KB :: Darmowy Pascal 8 KB
Spójrz na zadania wymagające przydzielenia pamięci (lub użyj dodatkowych buforów do gromadzenia wyników z programów wielordzeniowych):
program Java mandelbrot 67 , 880KB :: program C 30 , 444 KB
k-nukleotydowy
program Java 494 , 040KB :: program C 153 , 452KB
program Java 511 , 484KB z komplementacją wsteczną :: program C 248 , 632KB
regex-dna
program Java 557 , 080KB :: program C 289 , 088KB
drzewa binarne
program Java 506 , 592KB :: program C 99 , 448KB
Zależy to od tego, czy określone użycie, dla twojego konkretnego podejścia do rozwiązania określonych problemów, które musisz rozwiązać, będzie ograniczone przez określone limity dostępnej pamięci na konkretnej platformie, która będzie używana.
źródło
Jak w przypadku wszystkich rzeczy, jest to kompromis.
Jeśli budujesz aplikację, która będzie działała na pulpicie jednego użytkownika i można zasadnie oczekiwać, że będzie kontrolować dużą część pamięci RAM na tym komputerze, warto poświęcić użycie pamięci dla szybkości implementacji. Jeśli celujesz w tę samą maszynę, ale budujesz małe narzędzie, które będzie konkurować z wieloma innymi wymagającymi pamięci aplikacjami, które działają jednocześnie, możesz być bardziej ostrożny w kwestii tego kompromisu. Użytkownikowi może się spodobać gra, która potrzebuje całej pamięci, gdy jest uruchomiona (choć, jak zauważa World Engineer, „ Będę się martwił, jeśli śmieciarz zdecyduje się okresowo wstrzymać akcję, aby wykonać zamiatanie) - prawdopodobnie będą o wiele mniej entuzjastycznie nastawieni, jeśli odtwarzacz muzyczny uruchamiany w tle podczas wykonywania innych czynności zdecyduje się pożreć masę pamięci i zakłóca ich zdolność do pracy. Jeśli budujesz aplikację internetową, dowolna pamięć używana na serwerach ogranicza twoją skalowalność, zmuszając Cię do wydawania większych pieniędzy na więcej serwerów aplikacji w celu obsługi tego samego zestawu użytkowników. Może to mieć duży wpływ na ekonomikę firmy, więc możesz być bardzo ostrożny przy podejmowaniu tego kompromisu. dowolna pamięć używana na serwerach ogranicza twoją skalowalność, zmuszając Cię do wydawania większej ilości pieniędzy na więcej serwerów aplikacji w celu obsługi tego samego zestawu użytkowników. Może to mieć duży wpływ na ekonomikę firmy, więc możesz być bardzo ostrożny przy podejmowaniu tego kompromisu. dowolna pamięć używana na serwerach ogranicza twoją skalowalność, zmuszając Cię do wydawania większej ilości pieniędzy na więcej serwerów aplikacji w celu obsługi tego samego zestawu użytkowników. Może to mieć duży wpływ na ekonomikę firmy, więc możesz być bardzo ostrożny przy podejmowaniu tego kompromisu.
źródło
To zależy od wielu czynników, zwłaszcza od skali, w jakiej pracujesz.
Dla celów argumentu załóżmy 30-krotną różnicę w pamięci i 2x w wykorzystaniu procesora.
Jeśli masz do czynienia z programem interaktywnym, który zajmuje 10 megabajtów pamięci i 1 milisekundę procesora, jeśli jest napisany w C, jest to prawie nieistotne - 300 megabajtów pamięci i 2 milisekundy do wykonania jest zwykle zupełnie nieistotne na typowym pulpicie, i mało prawdopodobne, by miało to znaczenie nawet na telefonie lub tablecie.
Różnica między potrzebowaniem około połowy zasobów 1 serwera a potrzebowaniem 15 serwerów jest jednak znacznie większym krokiem - zwłaszcza, że skalowanie do 15 serwerów prawdopodobnie będzie wymagało dużo dodatkowej pracy, a nie mniej. Jeśli chodzi o przyszłą ekspansję, te same czynniki, o których wspominasz, sugerują, że jeśli twoja baza klientów nie ulegnie znacznemu wzrostowi, że jeśli będzie działać na jednym serwerze, istnieje spora szansa, że kiedy wyrosniesz z tego serwera, będziesz w stanie zastąpić to jednym nowym serwerem bez żadnego problemu.
Innym czynnikiem, który naprawdę musisz wziąć pod uwagę, jest dokładnie to, ile różnic w kosztach rozwoju zobaczysz dla swojego konkretnego zadania. W tej chwili zasadniczo patrzysz na jedną stronę równania. Aby dobrze zorientować się w kosztach i korzyściach, (oczywiście) musisz przyjrzeć się zarówno kosztom, jak i korzyściom, a nie pojedynczo. Prawdziwe pytanie brzmi: „czy x jest większy niż y?” - ale nie można tego ustalić, patrząc tylko na x. Oczywiście musisz również spojrzeć na y.
źródło
Zarządzanie pamięcią jest absolutnie istotne w dzisiejszym świecie. Jednak nie tak, jak można się spodziewać. Nawet w językach, w których zbierane są śmieci, musisz upewnić się, że nie wyciek referencji
Robisz coś złego, jeśli to jest twój kod:
Odśmiecanie nie może magicznie wiedzieć, że nigdy nie użyjesz jakiegoś odniesienia, chyba że go stworzysz, więc nie będziesz mógł go użyć ponownie, tzn. Robiąc
Cache=null
to skutecznie ostrzegasz śmieciarza, że „hej, nie będę mógł uzyskaj do niej dostęp. Zrób z tym, co chcesz ”Jest to bardziej skomplikowane, ale wycieki referencyjne są tak samo, jeśli nie bardziej, szkodliwe niż tradycyjne wycieki pamięci.
Istnieją również miejsca, w których nie można zmieścić pojemnika na śmieci. Na przykład ATTiny84 jest mikrokontrolerem z 512 bajtami kodu ROM i 32 bajtami RAM. Powodzenia! To ekstremalne zjawisko i prawdopodobnie nie byłoby programowane w niczym innym niż w asemblerze, ale nadal. W innych przypadkach możesz mieć 1M pamięci. Jasne, możesz zmieścić śmietnik, ale jeśli procesor jest bardzo wolny (albo z powodu ograniczeń, albo w celu oszczędzania baterii), to nie będziesz chciał używać śmieciarza, ponieważ śledzenie tego, co programista może wiedzieć, jest zbyt drogie .
Staje się również znacznie trudniejsze w używaniu funkcji wyrzucania elementów bezużytecznych, gdy potrzebujesz gwarantowanych czasów odpowiedzi. Na przykład, jeśli masz monitor pracy serca lub coś takiego, a gdy odbiera on sygnał
1
z jakiegoś portu, musisz zagwarantować, że możesz zareagować na niego odpowiednim sygnałem lub czymś w ciągu 10ms. Jeśli w trakcie rutynowej odpowiedzi moduł odśmiecający musi wykonać przepustkę, a odpowiedź zajmuje 100 ms, może to oznaczać, że ktoś nie żyje. Zbieranie śmieci jest bardzo trudne, jeśli nie niemożliwe, do wykorzystania, gdy należy zagwarantować wymagania dotyczące czasu.I oczywiście, nawet na nowoczesnym sprzęcie, są przypadki, w których potrzebujesz dodatkowych 2% wydajności, nie martwiąc się o narzut śmieciarki.
źródło
Jak powiedział Donald Knuth, przedwczesna optymalizacja jest źródłem wszelkiego zła. Jeśli nie masz powodu, aby sądzić, że pamięć będzie wąskim gardłem, nie martw się o to. Biorąc pod uwagę, że prawo Moore'a wciąż zapewnia zwiększoną pojemność pamięci (chociaż nie uzyskujemy z niej szybszego kodu jednowątkowego), istnieją wszelkie powody, by sądzić, że w przyszłości będziemy jeszcze mniej ograniczeni w zakresie pamięci niż my są dzisiaj.
To powiedziawszy, jeśli optymalizacja nie jest przedwczesna, zrób to. Obecnie osobiście pracuję nad projektem, w którym bardzo szczegółowo rozumiem wykorzystanie pamięci, tak naprawdę potrzebuję precyzyjnej kontroli, a zamiatanie śmieci by mnie zabiło. Dlatego robię ten projekt w C ++. Ale wybór ten wydaje mi się wydarzeniem raz na kilka lat. (Mam nadzieję, że za kilka tygodni nie będę więcej dotykać C ++ przez kilka kolejnych lat.)
źródło
Dla osób zajmujących się „big data” zarządzanie pamięcią jest nadal ogromnym problemem. Programy astronomiczne, fizyki, bioinformatyki, uczenia maszynowego itp. Muszą radzić sobie z wielobajtowymi zestawami danych, a programy działają o wiele szybciej, jeśli odpowiednie części można zachować w pamięci. Nawet uruchomienie na komputerze z 128 GB pamięci RAM nie rozwiązuje problemu.
Jest także kwestia wykorzystania GPU, choć być może sklasyfikowałbyś to jako system wbudowany. Większość trudnego myślenia przy użyciu CUDA lub OpenCL sprowadza się do problemów z zarządzaniem pamięcią podczas przesyłania danych z pamięci głównej do pamięci GPU.
źródło
Szczerze mówiąc, wiele Javy pozwala sobie na pewne prawdziwie i bezcelowe, klasowo wybuchowe wzorce, które po prostu mordują wydajność i pamięć świni, ale zastanawiam się, ile z tej pamięci to tylko JVM, która teoretycznie (heh) pozwala ci uruchomić ta sama aplikacja w wielu środowiskach bez konieczności przepisywania nowych. Stąd pytanie o kompromis w zakresie projektowania sprowadza się do: „Ile pamięci twoich użytkowników jest dla Ciebie warte takiego rozwoju?”
Jest to, IMO, warte rozważenia i warte rozważenia. Wkurza mnie jednak to, że ponieważ współczesne komputery PC są tak potężne, a pamięć tak tania, możemy całkowicie zignorować takie obawy i nadmuchać funkcje i nadęty kod oraz być leniwym w kwestii wyborów do tego stopnia, że wydaje się, że jest dużo Robię teraz na komputerze z systemem Windows, zajmuje to tyle samo czasu, co w Windows '95. Poważnie, Word? Ile nowych bzdur, których 80% ich bazy użytkowników faktycznie potrzebuje, mogliby dodać w ciągu 18 lat? Jesteś pewien, że mieliśmy już okna sprawdzania pisowni, prawda? Ale rozmawialiśmy o pamięci, która niekoniecznie jest szybka, jeśli masz jej dużo, więc dygresuję.
Ale oczywiście, jeśli uda Ci się zrobić aplikację w ciągu 2 tygodni kosztem może kilku dodatkowych megabajtów zamiast 2 lat, aby uzyskać wersję wymagającą tylko kilku K, warto zastanowić się, jak wygląda kilka megabajtów ( Zgaduję) 4-12 koncertów na przeciętnym komputerze użytkownika, zanim wyśmiewam się z tego, że jestem taki niechlujny.
Ale co to ma wspólnego ze Scalą poza kwestią kompromisu? Tylko dlatego, że jest to zbieranie śmieci, nie oznacza, że nie zawsze powinieneś myśleć o przepływie danych w kategoriach tego, co jest w zakresach i zamknięciach, oraz czy należy je zostawić siedząc lub używać w taki sposób, aby zwolniony przez GC, gdy nie jest już potrzebny. Jest to coś, o czym nawet my, twórcy stron WWW JavaScript, musieliśmy pomyśleć i mam nadzieję, że będzie to kontynuowane, gdy będziemy się rozprzestrzeniać w innych problematycznych domenach, takich jak rak doświadczony w perfekcji (że wszyscy powinniście byli zabić za pomocą Flasha lub apletów lub czegoś, gdy mieliście okazję) tym jesteśmy.
źródło
Zarządzanie pamięcią (lub sterowanie) jest właściwie głównym powodem, dla którego używam C i C ++.
Nie szybka pamięć. Nadal patrzymy na niewielką liczbę rejestrów, coś w rodzaju pamięci podręcznej 32 KB dla L1 na i7, 256 KB dla L2 i 2 MB dla L3 / rdzenia. To mówi:
Zużycie pamięci na poziomie ogólnym, może nie. Jestem trochę niepraktyczny, ponieważ nie podoba mi się pomysł notatnika, który zabiera, powiedzmy, 50 megabajtów pamięci DRAM i setki megabajtów miejsca na dysku twardym, mimo że mam to do stracenia i więcej. Jestem tu od dłuższego czasu i wydaje mi się to dziwne i trochę obrzydliwe, gdy widzę, że tak prosta aplikacja zajmuje stosunkowo dużo pamięci na to, co powinno być możliwe w przypadku kilobajtów. To powiedziawszy, mógłbym być w stanie żyć ze sobą, gdybym spotkał się z taką rzeczą, jeśli nadal byłaby miła i wrażliwa.
Powodem, dla którego zarządzanie pamięcią jest dla mnie ważne w mojej dziedzinie, nie jest ogólne zmniejszenie zużycia pamięci. Setki megabajtów pamięci nie musi spowalniać aplikacji w żaden nietrywialny sposób, jeśli żadna z tych pamięci nie jest często uzyskiwana (np. Tylko po kliknięciu przycisku lub innej formie wprowadzania danych przez użytkownika, co jest niezwykle rzadkie, chyba że użytkownik mówią o koreańskich graczach Starcraft, którzy mogą kliknąć przycisk milion razy na sekundę).
Powodem, dla którego jest to ważne w mojej dziedzinie, jest ścisłe połączenie pamięci, które jest bardzo często dostępne (np. Zapętlanie każdej ramki) w tych krytycznych ścieżkach. Nie chcemy przegapić pamięci podręcznej za każdym razem, gdy uzyskujemy dostęp tylko do jednego z miliona elementów, do których wszystkie muszą być dostępne w pętli przy każdej klatce. Kiedy przenosimy pamięć w dół hierarchii od wolnej pamięci do szybkiej pamięci w dużych porcjach, powiedzmy 64 bajtowe linie pamięci podręcznej, naprawdę pomocne jest, jeśli te 64 bajty zawierają odpowiednie dane, jeśli możemy zmieścić wiele elementów o wartości danych w tych 64 bajtach, i jeśli nasze wzorce dostępu są takie, że wykorzystamy to wszystko przed eksmisją danych.
Te często używane dane dla miliona elementów mogą zajmować jedynie 20 megabajtów, mimo że mamy gigabajty. Nadal robi różnicę w liczbie klatek na sekundę zapętlającą się wokół tych danych, co każdą narysowaną klatkę, jeśli pamięć jest szczelna i blisko siebie, aby zminimalizować straty pamięci podręcznej, i tam właśnie zarządzanie / kontrola pamięci jest tak przydatna. Prosty przykład wizualny na kuli z kilkoma milionami wierzchołków:
Powyższe jest w rzeczywistości wolniejsze niż moja zmienna wersja, ponieważ testuje trwałą reprezentację struktury danych siatki, ale z tym bokiem starałem się osiągnąć takie szybkości klatek nawet przy połowie tych danych (co prawda sprzęt był szybszy od moich zmagań ), ponieważ nie rozumiem, jak minimalizować błędy pamięci podręcznej i zużycie pamięci dla danych siatki. Siatki to jedne z najtrudniejszych struktur danych, z którymi miałem do czynienia w tym względzie, ponieważ przechowują tak wiele współzależnych danych, że muszą być zsynchronizowane, takie jak wielokąty, krawędzie, wierzchołki, tyle map tekstur, ile użytkownik chce dołączyć, masy kości, mapy kolorów, zestawy wyboru, cele przekształcenia, grubości krawędzi, materiały wielokątów itp. itd. itd.
W ciągu ostatnich kilku dekad zaprojektowałem i wdrożyłem wiele systemów siatkowych, a ich prędkość była często bardzo proporcjonalna do wykorzystania pamięci. Mimo że pracuję z tym, o wiele więcej pamięci niż na początku, moje nowe systemy siatki są ponad 10 razy szybsze niż mój pierwszy projekt (prawie 20 lat temu) i w dużym stopniu, ponieważ zużywają około 1/10 pamięć. Najnowsza wersja wykorzystuje nawet indeksowaną kompresję do wtłoczenia jak największej ilości danych, i pomimo narzutu związanego z przetwarzaniem dekompresji, kompresja faktycznie poprawiła wydajność, ponieważ znowu mamy tak mało cennej szybkiej pamięci. Mogę teraz dopasować milion siatki wielokątów ze współrzędnymi tekstury, bigowaniem krawędzi, przypisaniami materiałów itp. Wraz z indeksem przestrzennym dla tego w około 30 megabajtach.
Oto modyfikowalny prototyp z ponad 8 milionami czworokątów i wielozadaniowym schematem podziału na i3 z GF 8400 (to było kilka lat temu). Jest szybszy niż moja niezmienna wersja, ale nie jest używana w produkcji, ponieważ uważam, że wersja niezmienna jest o wiele łatwiejsza w utrzymaniu, a wydajność nie jest taka zła. Zauważ, że szkielet nie wskazuje faset, ale łatki (druty są w rzeczywistości krzywymi, w przeciwnym razie cała siatka byłaby jednolita czarna), chociaż wszystkie punkty w elemencie są modyfikowane przez pędzel.
Tak czy inaczej, chciałem tylko pokazać niektóre z powyższych powyżej, aby pokazać konkretne przykłady i obszary, w których zarządzanie pamięcią jest tak pomocne, a także mam nadzieję, że ludzie nie myślą, że po prostu mówię poza moim tyłkiem. Często denerwuję się, gdy ludzie mówią, że pamięć jest tak obfita i tania, ponieważ chodzi o wolną pamięć, taką jak DRAM i dyski twarde. Jest wciąż tak mały i tak cenny, gdy mówimy o szybkiej pamięci, a wydajność dla naprawdę krytycznych (tj. Często spotykanych przypadków, nie dla wszystkiego) ścieżek odnosi się do grania z tak małą ilością szybkiej pamięci i wykorzystywania jej tak skutecznie, jak to możliwe .
W przypadku tego rodzaju rzeczy bardzo pomocna jest praca z językiem, który pozwala na przykład projektować obiekty wysokiego poziomu, takie jak C ++, jednocześnie zachowując te obiekty w jednej lub kilku sąsiadujących tablicach z gwarancją, że pamięć wszystkie takie obiekty będą reprezentowane w sposób ciągły i bez niepotrzebnego narzutu pamięci na obiekt (np. nie wszystkie obiekty wymagają refleksji lub wirtualnej wysyłki). Kiedy faktycznie przenosisz się do obszarów krytycznych pod względem wydajności, staje się to wzrostem wydajności, aby mieć taką kontrolę nad pamięcią, powiedzmy, majstrowanie przy pulach obiektów i używanie prymitywnych typów danych, aby uniknąć narzutu obiektu, kosztów GC i aby często uzyskiwać dostęp do pamięci razem przylegające.
Tak więc zarządzanie pamięcią / kontrola (lub jej brak) jest w moim przypadku dominującym powodem wyboru języka, który najbardziej produktywnie pozwala mi rozwiązywać problemy. Zdecydowanie piszę swoją część kodu, który nie jest krytyczny pod względem wydajności, i do tego zwykle używam Lua, którą dość łatwo można osadzić w C.
źródło