Mam nadzieję, że to pytanie nie jest uważane za zbyt podstawowe dla tego forum, ale zobaczymy. Zastanawiam się, jak refaktoryzować kod, aby uzyskać lepszą wydajność, która jest uruchamiana kilka razy.
Załóżmy, że tworzę listę częstotliwości słów, korzystając z mapy (prawdopodobnie HashMap), w której każdy klucz jest ciągiem ze słowem, które jest zliczane, a wartością jest liczba całkowita, która jest zwiększana za każdym razem, gdy zostanie znaleziony token słowa.
W Perlu zwiększenie takiej wartości byłoby banalnie proste:
$map{$word}++;
Ale w Javie jest to o wiele bardziej skomplikowane. Oto sposób, w jaki obecnie to robię:
int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);
Co oczywiście zależy od funkcji autoboxowania w nowszych wersjach Java. Zastanawiam się, czy możesz zasugerować bardziej skuteczny sposób na zwiększenie takiej wartości. Czy istnieją nawet dobre powody, dla których warto unikać ramek Kolekcje i używać czegoś innego?
Aktualizacja: Zrobiłem test kilku odpowiedzi. Patrz poniżej.
źródło
Odpowiedzi:
Niektóre wyniki testu
Otrzymałem wiele dobrych odpowiedzi na to pytanie - dzięki, ludzie - więc postanowiłem przeprowadzić kilka testów i dowiedzieć się, która metoda jest rzeczywiście najszybsza. Pięć metod, które przetestowałem, to:
metoda
Oto co zrobiłem ...
Wyniki
Najpierw przedstawię wyniki, a poniżej kod dla zainteresowanych.
Metoda ContainsKey była, zgodnie z oczekiwaniami, najwolniejsza, dlatego podam prędkość każdej metody w porównaniu do prędkości tej metody.
Wnioski
Wydaje się, że tylko metoda MutableInt i metoda Trove są znacznie szybsze, ponieważ tylko one dają wzrost wydajności o ponad 10%. Jeśli jednak wątek stanowi problem, AtomicLong może być bardziej atrakcyjny niż inne (nie jestem do końca pewien). Uruchomiłem także TestForNull ze
final
zmiennymi, ale różnica była znikoma.Pamiętaj, że nie profilowałem użycia pamięci w różnych scenariuszach. Z przyjemnością dowiem się od każdego, kto ma dobry wgląd w to, w jaki sposób metody MutableInt i Trove mogłyby wpłynąć na wykorzystanie pamięci.
Osobiście uważam, że metoda MutableInt jest najbardziej atrakcyjna, ponieważ nie wymaga ładowania żadnych klas stron trzecich. Więc jeśli nie odkryję problemów z tym, najprawdopodobniej pójdę.
Kod
Oto kluczowy kod z każdej metody.
Zawiera klucz
TestForNull
AtomicLong
Trove
MutableInt
źródło
freq.compute(word, (key, count) -> count == null ? 1 : count + 1)
? Wewnętrznie robi to o jeden mniej zaszyfrowany przegląd niżcontainsKey
, byłoby interesujące zobaczyć, jak wypada w porównaniu z innymi, z powodu lambda.Teraz jest krótszy sposób korzystania z Java 8
Map::merge
.Co to robi:
Więcej informacji tutaj .
źródło
map.merge(key, 1, (a, b) -> a + b);
nieInteger::sum
jako BiFunkcji i nie podoba się @russter odpowiedzieć w sposób, w jaki został napisany. To zadziałało dla mnieMap.merge(key, 1, { a, b -> a + b})
Trochę badań w 2016 r .: https://github.com/leventov/java-word-count , kod źródłowy testu porównawczego
Najlepsze wyniki na metodę (im mniejsza, tym lepsza):
Wyniki dla czasu \ przestrzeni:
źródło
Google Guava jest twoim przyjacielem ...
... przynajmniej w niektórych przypadkach. Mają fajną AtomicLongMap . Szczególnie miłe, bo masz do czynienia z długo jak wartość w mapie.
Na przykład
Możliwe jest również dodanie do wartości więcej niż 1:
źródło
AtomicLongMap#getAndAdd
przyjmuje prymitywną,long
a nie klasę opakowania; nie ma sensu tego robićnew Long()
. IAtomicLongMap
jest sparametryzowanym typem; powinieneś był to zadeklarowaćAtomicLongMap<String>
.@Hank Gay
Jako kontynuacja mojego (raczej bezużytecznego) komentarza: Trove wygląda jak droga. Jeżeli z jakiegoś powodu chcesz trzymać się standardowego JDK, ConcurrentMap i AtomicLong może sprawić, że kodują malutki nieco ładniejszy, chociaż YMMV.
pozostawi
1
jako wartość na mapie dlafoo
. Realistycznie, zwiększona łatwość nawlekania wątków to wszystko, co takie podejście musi polecić.źródło
I w ten sposób zwiększasz wartość za pomocą prostego kodu.
Zasiłek:
Minusem:
Teoretycznie po wywołaniu get () wiesz już, gdzie umieścić (), więc nie powinieneś ponownie szukać. Ale wyszukiwanie mapy skrótów zajmuje zwykle bardzo minimalny czas, który można zignorować ten problem z wydajnością.
Ale jeśli poważnie podchodzisz do problemu, jesteś perfekcjonistą, innym sposobem jest użycie metody scalania, jest to (prawdopodobnie) bardziej wydajne niż poprzedni fragment kodu, ponieważ (teoretycznie) przeszukujesz mapę tylko raz: (chociaż ten kod nie jest oczywisty od pierwszego wejrzenia, jest krótki i wydajny)
Sugestia: przez większość czasu powinieneś dbać o czytelność kodu, a nie o niewielki wzrost wydajności. Jeśli łatwiej jest zrozumieć pierwszy fragment kodu, użyj go. Ale jeśli jesteś w stanie zrozumieć drugą grzywnę, możesz też ją wybrać!
źródło
Zawsze warto poszukać czegoś takiego w Bibliotece kolekcji Google . W takim przypadku Multiset wykona lewę:
Istnieją metody przypominające mapę do iteracji kluczy / wpisów itp. Wewnętrznie implementacja obecnie używa a
HashMap<E, AtomicInteger>
, więc nie poniesiesz kosztów boksu.źródło
count()
metoda na wielu zestawach działa w czasie O (1) lub O (n) (w najgorszym przypadku)? Dokumenty nie są jasne w tej kwestii.Powinieneś być świadomy faktu, że Twoja pierwotna próba
zawiera dwie potencjalnie drogie operacje na mapie, a mianowicie
containsKey
iget
. Pierwsza wykonuje operację potencjalnie podobną do drugiej, więc wykonuje się tę samą pracę dwa razy !Jeśli spojrzysz na API dla mapy,
get
operacje zwykle powracająnull
gdy mapa nie zawiera żądanego elementu.Zauważ, że dzięki temu rozwiążesz takie rozwiązanie
niebezpieczne, ponieważ może dać
NullPointerException
s. Powinieneśnull
najpierw sprawdzić .Należy również pamiętać , a to jest bardzo ważne, że
HashMap
ów może zawieraćnulls
definicji. Więc nie każdy wróciłnull
mówi „nie ma takiego elementu”. Pod tym względemcontainsKey
zachowuje się inaczej niżget
w rzeczywistości mówiąc, czy istnieje taki element. Szczegółowe informacje można znaleźć w interfejsie API.Jednak w twoim przypadku możesz nie chcieć rozróżniać między przechowywanym
null
a „noSuchElement”. Jeśli nie chcesz zezwalać,null
możesz preferowaćHashtable
. Korzystanie z biblioteki opakowań, jak już zaproponowano w innych odpowiedziach, może być lepszym rozwiązaniem do ręcznego leczenia, w zależności od złożoności aplikacji.Aby ukończyć odpowiedź (i zapomniałem na początku to wstawić, dzięki funkcji edycji!), Najlepszym sposobem na zrobienie tego natywnie, jest
get
przejście dofinal
zmiennej, sprawdzenienull
i ponowne sprawdzenie zaput
pomocą1
. Zmienna powinna być,final
ponieważ i tak jest niezmienna. Kompilator może nie potrzebować tej wskazówki, ale w ten sposób jest jaśniejszy.Jeśli nie chcesz polegać na autoboxowaniu, powinieneś powiedzieć coś takiego
map.put(new Integer(1 + i.getValue()));
.źródło
Innym sposobem byłoby utworzenie zmiennej liczby całkowitej:
oczywiście oznacza to utworzenie dodatkowego obiektu, ale narzut w porównaniu z tworzeniem liczby całkowitej (nawet z Integer.valueOf) nie powinien być aż tak duży.
źródło
Możesz skorzystać z metody computeIfAbsent w
Map
interfejsie udostępnionym w Javie 8 .Metoda
computeIfAbsent
sprawdza, czy określony klucz jest już powiązany z wartością, czy nie? Jeśli nie ma powiązanej wartości, wówczas próbuje obliczyć swoją wartość przy użyciu danej funkcji odwzorowania. W każdym przypadku zwraca bieżącą (istniejącą lub obliczoną) wartość powiązaną z określonym kluczem lub null, jeśli obliczona wartość jest null.Na marginesie, jeśli masz sytuację, w której wiele wątków aktualizuje wspólną sumę, możesz spojrzeć na klasę LongAdder. Przy dużej rywalizacji oczekiwana przepustowość tej klasy jest znacznie wyższa niż
AtomicLong
kosztem większego zużycia miejsca.źródło
Problemem może być rotacja pamięci, ponieważ każde boksowanie liczby całkowitej większej lub równej 128 powoduje przydział obiektu (patrz Integer.valueOf (int)). Chociaż moduł wyrzucania śmieci bardzo skutecznie radzi sobie z obiektami krótkotrwałymi, wydajność do pewnego stopnia ucierpi.
Jeśli wiesz, że liczba dokonanych przyrostów znacznie przewyższy liczbę kluczy (= w tym przypadku słów), rozważ użycie int int. Phax już przedstawił kod do tego. Oto znowu, z dwiema zmianami (klasa posiadacza stała się statyczna, a wartość początkowa ustawiona na 1):
Jeśli potrzebujesz ekstremalnej wydajności, poszukaj implementacji mapy, która jest bezpośrednio dostosowana do pierwotnych typów wartości. jrudolph wspomniał o GNU Trove .
Nawiasem mówiąc, dobrym wyszukiwanym terminem dla tego tematu jest „histogram”.
źródło
Zamiast wywoływać metodę includeKey (), szybciej jest wywołać map.get i sprawdzić, czy zwracana wartość jest pusta, czy nie.
źródło
Czy na pewno jest to wąskie gardło? Czy przeprowadziłeś jakąś analizę wydajności?
Spróbuj użyć profilera NetBeans (darmowego i wbudowanego w NB 6.1), aby spojrzeć na hotspoty.
Wreszcie, aktualizacja JVM (powiedzmy z 1.5-> 1.6) jest często tanim wzmacniaczem wydajności. Nawet aktualizacja numeru kompilacji może zapewnić dobre zwiększenie wydajności. Jeśli korzystasz z systemu Windows i jest to aplikacja klasy serwera, użyj -server w wierszu polecenia, aby użyć JVM serwera Hotspot. W maszynach z systemem Linux i Solaris jest to wykrywane automatycznie.
źródło
Istnieje kilka podejść:
Użyj alorithm Bag, jak zestawy zawarte w kolekcjach Google.
Utwórz zmienny pojemnik, którego możesz użyć na mapie:
I użyj put („słowo”, nowe Moje („Słowo”)); Następnie możesz sprawdzić, czy istnieje i zwiększyć wartość podczas dodawania.
Unikaj rzucania własnym rozwiązaniem za pomocą list, ponieważ jeśli pojawi się wyszukiwanie i sortowanie w pętli wewnętrznej, wydajność będzie śmierdzieć. Pierwsze rozwiązanie HashMap jest w rzeczywistości dość szybkie, ale poprawne jest takie samo, jak w kolekcjach Google.
Liczenie słów za pomocą Kolekcji Google wygląda mniej więcej tak:
Korzystanie z HashMultiset jest dość eleganckie, ponieważ algorytm workowy jest właśnie tym, czego potrzebujesz do liczenia słów.
źródło
Myślę, że twoje rozwiązanie byłoby standardowe, ale - jak sam zauważyłeś - prawdopodobnie nie jest to najszybszy możliwy sposób.
Możesz spojrzeć na GNU Trove . Jest to biblioteka zawierająca wszelkiego rodzaju szybkie prymitywne kolekcje. Twój przykład użyłby TObjectIntHashMap, który ma metodę adjustOrPutValue, która robi dokładnie to, co chcesz.
źródło
Odmianą podejścia MutableInt, które może być jeszcze szybsze, jeśli jest trochę włamaniem, jest użycie tablicy intelementowej z jednym elementem:
Byłoby interesujące, gdybyś mógł ponownie uruchomić testy wydajności z tą odmianą. To może być najszybsze.
Edycja: Powyższy wzór działał dla mnie dobrze, ale ostatecznie zmieniłem użycie kolekcji Trove, aby zmniejszyć rozmiar pamięci na niektórych bardzo dużych mapach, które tworzyłem - i jako bonus było również szybsze.
Jedną naprawdę fajną cechą jest to, że
TObjectIntHashMap
klasa ma pojedynczeadjustOrPutValue
wywołanie, które w zależności od tego, czy wartość ma już ten klucz, albo wprowadzi wartość początkową, albo zwiększy istniejącą wartość. Jest to idealne rozwiązanie do zwiększania:źródło
Kolekcje Google HashMultiset:
- dość elegancki w użyciu
- ale zużywa procesor i pamięć
Najlepiej byłoby mieć taką metodę jak:
Entry<K,V> getOrPut(K);
(elegancki i tani)Taka metoda oblicza skrót i indeks tylko raz, a następnie moglibyśmy zrobić to, co chcemy z wpisem (zastąpić lub zaktualizować wartość).
Bardziej elegancki:
- weź
HashSet<Entry>
- przedłuż go, aby
get(K)
w razie potrzeby wstawić nowy wpis- wpis może być twoim własnym obiektem.
->
(new MyHashSet()).get(k).increment();
źródło
Po prostu użyj wbudowanej funkcji w
Map.java
następujący sposóbźródło
++
... OMG, to takie proste. @siegi++
w tym wyrażeniu nie działa nigdzie, ponieważ zmienna jest potrzebna jako argument, ale są tylko wartości. Twój dodatek+ 1
działa. Teraz twoje rozwiązanie jest takie samo jak w odpowiedzi off99555 .„put” need „get” (aby zapewnić brak duplikatu klucza).
Więc bezpośrednio zrób „put”,
a jeśli była poprzednia wartość, to dodaj:
Jeśli liczenie zaczyna się od 0, dodaj 1: (lub dowolne inne wartości ...)
Uwaga: ten kod nie jest bezpieczny dla wątków. Użyj go, aby zbudować, a następnie użyj mapy, a nie jej jednocześnie aktualizować.
Optymalizacja: w pętli zachowaj starą wartość, aby stała się nową wartością następnej pętli.
źródło
Różne prymitywne opakowania, na przykład,
Integer
są niezmienne, więc naprawdę nie ma bardziej zwięzłego sposobu robienia tego, o co prosisz, chyba że możesz to zrobić za pomocą czegoś takiego jak AtomicLong . Mogę spróbować za chwilę i zaktualizować. BTW, Hashtable jest częścią kolekcji Framework .źródło
Użyłbym Leniwej mapy kolekcji Apache (aby zainicjować wartości do 0) i użyłem MutableIntegers z Apache Lang jako wartości na tej mapie.
Największym kosztem jest dwukrotne przeszukanie mapy w twojej metodzie. W moim musisz to zrobić tylko raz. Po prostu pobierz wartość (zostanie zainicjowana, jeśli jej nie ma) i zwiększ ją.
źródło
Struktura danych biblioteki Functional Java
TreeMap
zawieraupdate
metodę w najnowszym nagłówku trunk:Przykładowe użycie:
Ten program wypisuje „2”.
źródło
@Vilmantas Baranauskas: Jeśli chodzi o tę odpowiedź, chciałbym skomentować, gdybym miał punkty rep, ale nie mam. Chciałem zauważyć, że zdefiniowana tam klasa Counter NIE jest bezpieczna dla wątków, ponieważ nie wystarczy synchronizacja inc () bez synchronizacji wartości (). Inne wątki wywołujące wartość () nie mają gwarancji, że zobaczą tę wartość, chyba że zostanie ustanowiony związek przed aktualizacją.
źródło
Nie wiem, jak to jest wydajne, ale działa również poniższy kod. Musisz zdefiniować
BiFunction
na początku. Co więcej, dzięki tej metodzie możesz zrobić coś więcej niż tylko przyrost.wyjście jest
źródło
Jeśli używasz kolekcji Eclipse , możesz użyć
HashBag
. Będzie to najbardziej wydajne podejście pod względem wykorzystania pamięci, a także będzie dobrze działać pod względem szybkości wykonywania.HashBag
jest wspierany przez,MutableObjectIntMap
który przechowuje prymitywne ints zamiastCounter
obiektów. Zmniejsza to obciążenie pamięci i poprawia szybkość wykonywania.HashBag
zapewnia interfejs API, którego potrzebujesz, ponieważ jest toCollection
umożliwia on również sprawdzenie liczby wystąpień elementu.Oto przykład z Eclipse Collections Kata .
Uwaga: jestem osobą odpowiedzialną za kolekcje Eclipse.
źródło
Sugeruję użycie Java 8 Map :: compute (). Rozważa również przypadek, gdy klucz nie istnieje.
źródło
mymap.merge(key, 1, Integer::sum)
?Ponieważ wiele osób szuka w języku Java odpowiedzi na Groovy, oto jak to zrobić w Groovy:
źródło
Prosty i łatwy sposób w java 8 jest następujący:
źródło
Mam nadzieję, że rozumiem twoje pytanie poprawnie, idę do Javy z Pythona, aby móc wczuć się w twoją walkę.
Jeśli masz
ty byś zrobił
Mam nadzieję że to pomoże!
źródło