Savjeti za izvedbu gudača

1. Uvod

U ovom vodiču, usredotočit ćemo se na aspekt izvedbe Java String API-ja.

Kopat ćemo Niz operacije kreiranja, pretvorbe i modifikacije za analizu dostupnih opcija i usporedbu njihove učinkovitosti.

Prijedlozi koje ćemo dati neće nužno odgovarati svakoj prijavi. Ali svakako, pokazat ćemo kako pobijediti na performansama kada je vrijeme rada aplikacije kritično.

2. Konstruiranje novog niza

Kao što znate, u Javi su žice nepromjenjive. Dakle, svaki put kad konstruiramo ili spojimo a Niz objekt, Java stvara novi String - to bi moglo biti posebno skupo ako se radi u petlji.

2.1. Korištenje konstruktora

U većini slučajeva, trebali bismo izbjegavati stvaranje Žice koristeći konstruktor, osim ako ne znamo što radimo.

Stvorimo a newString prvi objekt unutar petlje, koristeći novi niz () konstruktor, a zatim = operater.

Da bismo napisali našu referentnu vrijednost, poslužit ćemo se alatom JMH (Java Microbenchmark Harness).

Naša konfiguracija:

@BenchmarkMode (Mode.SingleShotTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) @Measurement (batchSize = 10000, iterations = 10) @Warmup (batchSize = 10000, iterations = 10) javna klasa StringPerformance {}

Ovdje koristimo SingeShotTime način koji metodu pokreće samo jednom. Kao što želimo mjeriti performanse Niz operacije unutar petlje, postoji a @Mjerenje za to dostupna napomena.

Važno je znati to Petlje benchmarkinga izravno u našim testovima mogu iskriviti rezultate zbog različitih optimizacija koje je primijenio JVM.

Dakle, izračunavamo samo jednu operaciju i prepuštamo JMH-u da se pobrine za petlju. Ukratko rečeno, JMH izvodi iteracije koristeći batchSize parametar.

Sad, dodajmo prvo mikro-mjerilo:

@Benchmark public String benchmarkStringConstructor () {return new String ("baeldung"); } @Benchmark javni niz benchmarkStringLiteral () {return "baeldung"; }

U prvom se testu stvara novi objekt u svakoj iteraciji. U drugom testu objekt se stvara samo jednom. Za preostale iteracije isti se objekt vraća iz Gudački stalni bazen.

Pokrenimo testove s brojanjem ponavljajućih ponavljanja = 1,000,000 i pogledajte rezultate:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringConstructor ss 10 16.089 ± 3.355 ms / op benchmarkStringLiteral ss 10 9.523 ± 3.331 ms / op

Od Postići vrijednosti, možemo jasno vidjeti da je razlika značajna.

2.2. + Operater

Pogledajmo dinamiku Niz primjer spajanja:

@State (Scope.Thread) javna statička klasa StringPerformanceHints {Rezultat niza = ""; Niz baeldung = "baeldung"; } @Benchmark javni niz benchmarkStringDynamicConcat () {return rezultat + baeldung; } 

U našim rezultatima želimo vidjeti prosječno vrijeme izvršenja. Format izlaznog broja postavljen je na milisekunde:

Benchmark 1000 10.000 benchmarkStringDynamicConcat 47.331 4370.411

Sada, analizirajmo rezultate. Kao što vidimo, dodavanje 1000 predmeta do država.rezultat uzima 47.331 milisekundi. Posljedično tome, povećavajući broj ponavljanja u 10 puta, vrijeme izvođenja raste do 4370.441 milisekundi.

Ukratko, vrijeme izvršenja raste kvadratno. Stoga je složenost dinamičkog spajanja u petlji od n iteracija O (n ^ 2).

2.3. String.concat ()

Još jedan način spajanja Žice je pomoću concat () metoda:

@Benchmark public String benchmarkStringConcat () {return result.concat (baeldung); } 

Izlazna jedinica vremena je milisekunda, broj ponavljanja je 100 000. Tablica rezultata izgleda ovako:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringConcat ss 10 3403.146 ± 852.520 ms / op

2.4. String.format ()

Drugi način stvaranja nizova je pomoću String.format () metoda. Ispod napa koristi regularne izraze za raščlanjivanje ulaza.

Napišimo JMH test slučaj:

String formatString = "bok% s, drago mi je što smo se upoznali"; @Benchmark public String benchmarkStringFormat_s () {return String.format (formatString, baeldung); }

Nakon toga pokrećemo ga i vidimo rezultate:

Broj ponavljanja 10 000 100 000 1 000 000 benchmarkStringFormat_s 17.181 140.456 1636.279 ms / op

Iako je kod sa String.format () izgleda čistije i čitljivije, ovdje ne pobjeđujemo u smislu izvedbe.

2.5. StringBuilder i StringBuffer

Već imamo objašnjenje s objašnjenjem StringBuffer i StringBuilder. Dakle, ovdje ćemo prikazati samo dodatne informacije o njihovoj izvedbi. StringBuilder koristi promjenjivi niz i indeks koji označava položaj posljednje ćelije korištene u polju. Kad je niz pun, proširuje se dvostruko veće veličine i kopira sve znakove u novi niz.

Uzimajući u obzir da se promjena veličine ne događa često, možemo razmotriti svaku dodati() operacija kao O (1) stalno vrijeme. Uzimajući to u obzir, cijeli je proces imao Na) složenost.

Nakon izmjene i pokretanja testa dinamičkog spajanja za StringBuffer i StringBuilder, dobivamo:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringBuffer ss 10 1.409 ± 1.665 ms / op benchmarkStringBuilder ss 10 1.200 ± 0.648 ms / op

Iako razlika u bodovima nije velika, možemo primijetiti da StringBuilder radi brže.

Srećom, u jednostavnim slučajevima ne trebamo StringBuilder staviti jedan Niz s drugom. Ponekad, statičko spajanje s + zapravo može zamijeniti StringBuilder. Ispod haube, najnoviji Java kompajleri nazvat će StringBuilder.append () za spajanje nizova.

To znači značajnu pobjedu u izvedbi.

3. Komunalne operacije

3.1. StringUtils.replace () nasuprot String.replace ()

Zanimljivo je znati to Verzija Apache Commons za zamjenu Niz čini se bolje od vlastitog Gudača zamijeniti() metoda. Odgovor na tu razliku leži u njihovoj provedbi. String.replace () koristi obrazac regularnog izraza kako bi se podudarao s Niz.

U kontrastu, StringUtils.replace () se široko koristi indexOf (), što je brže.

Sada je vrijeme za referentne testove:

@Benchmark javni niz benchmarkStringReplace () {return longString.replace ("prosjek", "prosjek !!!"); } @Benchmark javni niz benchmarkStringUtilsReplace () {return StringUtils.replace (longString, "prosjek", "prosjek !!!"); }

Postavljanje batchSize do 100.000, predstavljamo rezultate:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringReplace ss 10 6.233 ± 2.922 ms / op benchmarkStringUtilsReplace ss 10 5.355 ± 2.497 ms / op

Iako razlika između brojeva nije prevelika, StringUtils.replace () ima bolji rezultat. Naravno, brojevi i jaz između njih mogu se razlikovati ovisno o parametrima poput broja iteracija, duljine niza, pa čak i JDK verzije.

S najnovijim verzijama JDK 9+ (naši testovi rade na JDK 10), obje implementacije imaju prilično jednake rezultate. Sada vratimo JDK verziju na 8 i ponovo testove:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringReplace ss 10 48.061 ± 17.157 ms / op benchmarkStringUtilsReplace ss 10 14.478 ± 5.752 ms / op

Razlika u izvedbi je sada ogromna i potvrđuje teoriju o kojoj smo raspravljali na početku.

3.2. podjela()

Prije nego što započnemo, bilo bi korisno provjeriti metode dijeljenja nizova dostupne u Javi.

Kada postoji potreba za razdvajanjem niza s graničnikom, prva nam funkcija obično padne na pamet String.split (regex). Međutim, donosi ozbiljne probleme s izvedbom jer prihvaća argument regularnog izraza. Alternativno, možemo koristiti StringTokenizer razreda za razbijanje niza u žetone.

Druga opcija je Guavina Cjepidlaka API. Napokon, dobro staro indexOf () je također dostupan za poboljšanje performansi naše aplikacije ako nam nije potrebna funkcionalnost regularnih izraza.

Sada je vrijeme da napišemo referentne testove za String.split () opcija:

String emptyString = ""; @Benchmark public String [] benchmarkStringSplit () {return longString.split (emptyString); }

Pattern.split () :

@Benchmark public String [] benchmarkStringSplitPattern () {return spacePattern.split (longString, 0); }

StringTokenizer :

Popis stringTokenizer = novi ArrayList (); @Benchmark javni popis benchmarkStringTokenizer () {StringTokenizer st = novi StringTokenizer (longString); while (st.hasMoreTokens ()) {stringTokenizer.add (st.nextToken ()); } return stringTokenizer; }

String.indexOf () :

Popis stringSplit = novi ArrayList (); @Benchmark javni popis benchmarkStringIndexOf () {int pos = 0, end; while ((end = longString.indexOf ('', pos))> = 0) {stringSplit.add (longString.substring (poz, kraj)); pos = kraj + 1; } return stringSplit; }

Guava Cjepidlaka :

@Benchmark javni popis benchmarkGuavaSplitter () {return Splitter.on ("") .trimResults () .omitEmptyStrings () .splitToList (longString); }

Na kraju pokrećemo i uspoređujemo rezultate za batchSize = 100.000:

Benchmark Mode Cnt Score Greške Jedinice benchmarkGuavaSplitter ss 10 4.008 ± 1.836 ms / op benchmarkStringIndexOf ss 10 1.144 ± 0.322 ms / op benchmarkStringSplit ss 10 1.983 ± 1.075 ms / op benchmarkStringSplitPattern ss 10 14.891 ± 5.677zer s / s op

Kao što vidimo, najlošije performanse imaju benchmarkStringSplitPattern metodu, gdje koristimo Uzorak razred. Kao rezultat toga, možemo naučiti da korištenjem klase regularnih izraza s podjela() metoda može više puta prouzročiti gubitak performansi.

Također, primjećujemo da najbrži rezultati pružaju primjere s upotrebom indexOf () i split ().

3.3. Pretvaranje u Niz

U ovom ćemo odjeljku izmjeriti vrijeme izvođenja pretvorbe niza. Da bismo bili konkretniji, ispitat ćemo Integer.toString () metoda spajanja:

int sampleNumber = 100; @Benchmark javni niz benchmarkIntegerToString () {return Integer.toString (sampleNumber); }

String.valueOf () :

@Benchmark javni niz benchmarkStringValueOf () {return String.valueOf (sampleNumber); }

[neka cjelobrojna vrijednost] + “” :

@Benchmark javni niz benchmarkStringConvertPlus () {return sampleNumber + ""; }

String.format () :

String formatDigit = "% d"; @Benchmark javni niz benchmarkStringFormat_d () {return String.format (formatDigit, sampleNumber); }

Nakon pokretanja testova, vidjet ćemo izlaz za batchSize = 10.000:

Benchmark Mode Cnt Score Greške Jedinice benchmarkIntegerToString ss 10 0,953 ± 0,707 ms / op benchmarkStringConvertPlus ss 10 1,464 ± 1,670 ms / op benchmarkStringFormat_d ss 10 15,656 ± 8,896 ms / op benchmarkStringValueOf ss 10 2,847 ± 11,153 ms

Nakon analize rezultata, to vidimo test za Integer.toString () ima najbolji rezultat od 0.953 milisekundi. Nasuprot tome, pretvorba koja uključuje String.format ("% d") ima najlošiju izvedbu.

To je logično jer raščlanjivanje formata Niz je skupa operacija.

3.4. Uspoređujući žice

Procijenimo različite načine usporedbe Žice. Broj ponavljanja je 100,000.

Evo naših referentnih testova za String.equals () operacija:

@Benchmark public boolean benchmarkStringEquals () {return longString.equals (baeldung); }

String.equalsIgnoreCase () :

@Benchmark javni logički benchmarkStringEqualsIgnoreCase () {return longString.equalsIgnoreCase (baeldung); }

String.matches () :

@Benchmark public boolean benchmarkStringMatches () {return longString.matches (baeldung); } 

String.compareTo () :

@Benchmark public int benchmarkStringCompareTo () {return longString.compareTo (baeldung); }

Nakon toga pokrećemo testove i prikazujemo rezultate:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringCompareTo ss 10 2.561 ± 0.899 ms / op benchmarkStringEquals ss 10 1.712 ± 0.839 ms / op benchmarkStringEqualsIgnoreCase ss 10 2.081 ± 1.221 ms / op benchmarkStringMatches ss 10 118.364 ± 43.203 ms

Kao i uvijek, brojke govore same za sebe. The podudaranja () traje najduže jer koristi regularni izraz za usporedbu jednakosti.

U kontrastu, the jednako () i equalsIgnoreCase() su najbolji izbor.

3.5. String.matches () nasuprot Predkompilirani obrazac

Pogledajmo sada zasebno String.matches () i Matcher.matches () uzorci. Prvi uzima regexp kao argument i sastavlja ga prije izvođenja.

Pa svaki put kad nazovemo String.matches (), on kompajlira Uzorak:

@Benchmark public boolean benchmarkStringMatches () {return longString.matches (baeldung); }

Druga metoda ponovno koristi Uzorak objekt:

Uzorak longPattern = Uzorak.compile (longString); @Benchmark public boolean benchmarkPrecompiledMatches () {return longPattern.matcher (baeldung) .matches (); }

A sada rezultati:

Benchmark Mode Cnt Score Greške Jedinice benchmarkPrecompiledMatches ss 10 29.594 ± 12.784 ms / op benchmarkStringMatches ss 10 106.821 ± 46.963 ms / op

Kao što vidimo, podudaranje s unaprijed sastavljenim regularnim izrazom djeluje otprilike tri puta brže.

3.6. Provjeravanje duljine

Napokon, usporedimo String.isEmpty () metoda:

@Benchmark javni logički benchmarkStringIsEmpty () {return longString.isEmpty (); }

i String.length () metoda:

@Benchmark javni logički benchmarkStringLengthZero () {return emptyString.length () == 0; }

Prvo ih zovemo preko longString = "Pozdrav baeldung, u prosjeku sam nešto duži od ostalih žica". The batchSize je 10,000:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringIsEmpty ss 10 0,295 ± 0,277 ms / op benchmarkStringLengthZero ss 10 0,472 ± 0,840 ms / op

Nakon toga, postavimo longString = "" prazan niz i ponovo pokrenite testove:

Benchmark Mode Cnt Score Greške Jedinice benchmarkStringIsEmpty ss 10 0,245 ± 0,362 ms / op benchmarkStringLengthZero ss 10 0,351 ± 0,473 ms / op

Kao što primjećujemo, benchmarkStringLengthZero () i benchmarkStringIsEmpty () metode u oba slučaja imaju približno isti rezultat. Međutim, zvanje prazno je() radi brže od provjere je li duljina niza jednaka nuli.

4. Deduplikacija niza

Od JDK 8, značajka deduplikacije niza dostupna je kako bi se eliminirala potrošnja memorije. Jednostavno rečeno, ovaj alat traži nizove s istim ili dupliciranim sadržajem za spremanje jedne kopije svake zasebne vrijednosti niza u spremište nizova.

Trenutno postoje dva načina rješavanja Niz duplikati:

  • koristiti String.intern () ručno
  • omogućujući deduplikaciju niza

Pogledajmo detaljnije svaku opciju.

4.1. String.intern ()

Prije skoka naprijed, bilo bi korisno pročitati o ručnom interniranju u našem zapisu. S String.intern () možemo ručno postaviti referencu na Niz objekt unutar globalnog Niz bazen.

Tada JVM može koristiti povrat reference kad je to potrebno. Sa stajališta izvedbe, naša aplikacija može imati veliku korist ponovnom upotrebom referenci na niz iz konstantnog spremišta.

Važno je znati to JVM Niz spremište nije lokalno za nit. Svaki Niz koje dodajemo u bazen, dostupno je i drugim nitima.

Međutim, postoje i ozbiljni nedostaci:

  • da bismo pravilno održavali svoju aplikaciju, možda ćemo morati postaviti a -XX: StringTableSize JVM parametar za povećanje veličine bazena. JVM treba ponovno pokretanje kako bi proširio veličinu spremišta
  • pozivajući String.intern () ručno oduzima vrijeme. Raste u linearnom vremenskom algoritmu sa Na) složenost
  • dodatno, česti pozivi na duge Niz predmeti mogu uzrokovati probleme s memorijom

Da bismo imali neke dokazane brojke, pokrenimo benchmark test:

@Benchmark javni niz benchmarkStringIntern () {return baeldung.intern (); }

Dodatno, izlazni rezultati su u milisekundama:

Mjerilo 1000 10 000 100 000 1 000 000 mjeriloStringIntern 0,433 2,243 19,996 204,373

Zaglavlja stupaca ovdje predstavljaju drugačije ponavljanja računa od 1000 do 1,000,000. Za svaki iteracijski broj imamo ocjenu izvedbe testa. Kao što primjećujemo, rezultat se dramatično povećava pored broja ponavljanja.

4.2. Omogući automatsko uklanjanje duplikata

Kao prvo, ova je opcija dio G1 sakupljača smeća. Prema zadanim postavkama ova je značajka onemogućena. Stoga ga moramo omogućiti sljedećom naredbom:

 -XX: + UseG1GC -XX: + UseStringDeduplication

Važno je napomenuti da omogućavanje ove opcije to ne garantira Niz dogodit će se deduplikacija. Također, ne obrađuje mlade Žice. Kako bi se upravljalo minimalnom dobi obrade Žice, XX: StringDeduplicationAgeThreshold = 3 Dostupna je JVM opcija. Ovdje, 3 je zadani parametar.

5. Sažetak

U ovom uputstvu pokušavamo dati neke savjete za učinkovitiju upotrebu nizova u našem svakodnevnom životu kodiranja.

Kao rezultat, možemo istaknuti neke prijedloge kako bismo poboljšali izvedbu naše aplikacije:

  • pri spajanju nizova, StringBuilder je najprikladnija opcija to mi padne na pamet. Međutim, s malim žicama, + operacija ima gotovo iste performanse. Ispod poklopca Java kompajler može koristiti StringBuilder klasa za smanjenje broja objekata u nizu
  • za pretvaranje vrijednosti u niz, [neka vrsta] .toString () (Integer.toString () na primjer) tada radi brže String.valueOf (). Budući da ta razlika nije značajna, možemo je slobodno koristiti String.valueOf () da nema ovisnosti o vrsti ulazne vrijednosti
  • što se tiče usporedbe žica, ništa nije bolje od String.equals () daleko
  • Niz deduplikacija poboljšava performanse u velikim aplikacijama s više niti. Ali pretjerano String.intern () može uzrokovati ozbiljno curenje memorije, usporavajući aplikaciju
  • za razdvajanje žica koje bismo trebali koristiti indexOf () pobijediti u izvedbi. Međutim, u nekim nekritičnim slučajevima String.split () funkcija može dobro odgovarati
  • Koristeći Pattern.match () niz značajno poboljšava izvedbu
  • String.isEmpty () je brži od Stringa.dužina () == 0

Također, imajte na umu da su brojevi koje ovdje predstavljamo samo rezultati mjerenja JMH - tako da biste uvijek trebali testirati opseg vlastitog sustava i vremena izvođenja kako biste utvrdili utjecaj ove vrste optimizacija.

Napokon, kao i uvijek, kod korišten tijekom rasprave možete pronaći na GitHubu.


$config[zx-auto] not found$config[zx-overlay] not found