Tipy na výkon reťazca

1. Úvod

V tomto návode zameriame sa na výkonový aspekt rozhrania Java String API.

Zahĺbime sa String operácie vytvárania, konverzie a modifikácie s cieľom analyzovať dostupné možnosti a porovnať ich efektívnosť.

Návrhy, ktoré navrhneme, nebudú pre každú aplikáciu nevyhnutne vhodné. Ale určite si ukážeme, ako zvíťaziť vo výkone, keď je kritická doba chodu aplikácie.

2. Konštrukcia nového reťazca

Ako viete, v Jave sú reťazce nemenné. Takže zakaždým, keď zostrojíme alebo zreťazíme a String objekt, Java vytvorí nový Reťazec - to môže byť obzvlášť nákladné, ak sa to deje v slučke.

2.1. Pomocou konštruktora

Väčšinou, mali by sme sa vyhnúť vytváraniu Struny pomocou konštruktora, pokiaľ nevieme, čo robíme.

Vytvorme a novýString objekt vo vnútri slučky ako prvý, pomocou nový reťazec () konštruktér, potom = operátor.

Na napísanie našej referenčnej hodnoty použijeme nástroj JMH (Java Microbenchmark Harness).

Naša konfigurácia:

@BenchmarkMode (Mode.SingleShotTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) @Measurement (batchSize = 10 000, iterácie = 10) @ Warmup (batchSize = 10 000, iterácie = 10) verejná trieda StringPerformance {}

Tu používame SingeShotTime režim, ktorý spustí metódu iba raz. Pretože chceme merať výkonnosť String vo vnútri slučky sú operácie @ Meranie k tomu dostupná anotácia.

Je dôležité vedieť, že testovacie cykly priamo v našich testoch môžu skresliť výsledky kvôli rôznym optimalizáciám aplikovaným JVM.

Takže vypočítame iba jednu operáciu a necháme JMH, aby sa postaralo o opakovanie. Stručne povedané, JMH vykonáva iterácie pomocou batchSize parameter.

Teraz pridajme prvý mikro-benchmark:

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

V prvom teste sa v každej iterácii vytvorí nový objekt. V druhom teste je objekt vytvorený iba raz. Pre zostávajúce iterácie sa z objektu vráti ten istý objekt Reťazec stály bazén.

Poďme spustiť testy s počtom iterácií opakovania = 1,000,000 a pozrite si výsledky:

Benchmark Mode Cnt Score Error Jednotky benchmarkStringConstructor ss 10 16,089 ± 3,355 ms / op benchmarkStringLiteral ss 10 9,523 ± 3,331 ms / op

Z Skóre jasne vidíme, že rozdiel je značný.

2.2. + Operátor

Poďme sa pozrieť na dynamiku String príklad zreťazenia:

@State (Scope.Thread) verejná statická trieda StringPerformanceHints {Výsledok reťazca = ""; Reťazec baeldung = "baeldung"; } @Benchmark public String benchmarkStringDynamicConcat () {return result + baeldung; } 

V našich výsledkoch chceme vidieť priemerný čas vykonania. Formát výstupného čísla je nastavený na milisekundy:

Benchmark 1 000 10 000 benchmarkStringDynamicConcat 47,331 4370,411

Poďme si teraz analyzovať výsledky. Ako vidíme, pridávanie 1000 položky do výsledný stav berie 47.331 milisekundy. V dôsledku toho sa zvyšuje počet iterácií na 10-násobok, prevádzková doba rastie 4370.441 milisekundy.

Stručne povedané, čas popravy rastie kvadraticky. Preto je zložitosť dynamického zreťazenia v slučke n iterácií O (n ^ 2).

2.3. String.concat ()

Ešte jeden spôsob zreťazenia Struny je pomocou concat () metóda:

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

Jednotka výstupného času je milisekunda, počet iterácií je 100 000. Výsledková tabuľka vyzerá takto:

Benchmark Mode Cnt Score Error Jednotky benchmarkStringConcat ss 10 3403,146 ± 852,520 ms / op

2.4. String.format ()

Ďalším spôsobom, ako vytvoriť reťazce, je použitie String.format () metóda. Pod kapotou analyzuje vstup pomocou regulárnych výrazov.

Poďme napísať testovací prípad JMH:

String formatString = "ahoj% s, rád ťa spoznávam"; @Benchmark public String benchmarkStringFormat_s () {return String.format (formatString, baeldung); }

Potom ho spustíme a uvidíme výsledky:

Počet iterácií 10 000 100 000 1 000 000 referenčná hodnota StringFormat_s 17,181 140 456 1636 279 ms / operácia

Aj keď kód s String.format () vyzerá čistejšie a čitateľnejšie, nevyhrávame tu z hľadiska výkonu.

2.5. StringBuilder a StringBuffer

Už máme vysvetlenie týkajúce sa zápisu StringBuffer a StringBuilder. Takže tu ukážeme iba ďalšie informácie o ich výkone. StringBuilder používa zmeniteľné pole a index, ktorý označuje pozíciu poslednej bunky použitej v poli. Keď je pole plné, rozšíri sa na dvojnásobok svojej veľkosti a skopíruje všetky znaky do nového poľa.

Berúc do úvahy, že zmena veľkosti sa nevyskytuje veľmi často, môžeme zvážiť každého pridať () prevádzka ako O (1) konštantný čas. Ak vezmeme do úvahy túto skutočnosť, celý proces má O (n) zložitosť.

Po úprave a spustení testu dynamického zreťazenia pre StringBuffer a StringBuilder, dostaneme:

Benchmark Mode Cnt Score Error Jednotky benchmarkStringBuffer ss 10 1,409 ± 1,665 ms / op benchmarkStringBuilder ss 10 1,200 ± 0,648 ms / op

Aj keď rozdiel v skóre nie je veľa, môžeme si všimnúť že StringBuilder funguje rýchlejšie.

Našťastie, v jednoduchých prípadoch to nepotrebujeme StringBuilder dať jeden String s inou. Niekedy statické zreťazenie s + môže skutočne nahradiť StringBuilder. Pod kapotou budú najnovšie kompilátory Java volať StringBuilder.append () zreťaziť reťazce.

To znamená výrazné víťazstvo vo výkone.

3. Prevádzkové služby

3.1. StringUtils.replace () vs String.replace ()

Je zaujímavé vedieť, že Verzia Apache Commons na nahradenie String je oveľa lepší ako vlastný reťazec nahradiť () metóda. Odpoveď na tento rozdiel spočíva v ich implementácii. String.replace () používa regexový vzor na zosúladenie s String.

Naproti tomu StringUtils.replace () je široko používaný indexOf (), ktorá je rýchlejšia.

Teraz je čas na testovacie testy:

@Benchmark public String benchmarkStringReplace () {return longString.replace ("average", "average !!!"); } @Benchmark public String benchmarkStringUtilsReplace () {return StringUtils.replace (longString, "average", "average !!!"); }

Nastavenie batchSize na 100 000 uvádzame výsledky:

Benchmark Mode Cnt Score Error Jednotky benchmarkStringReplace ss 10 6,233 ± 2,922 ms / op benchmarkStringUtilsReplace ss 10 5,355 ± 2,497 ms / op

Aj keď rozdiel medzi číslami nie je príliš veľký, StringUtils.replace () má lepšie skóre. Čísla a rozdiel medzi nimi sa samozrejme môžu líšiť v závislosti od parametrov, ako je počet iterácií, dĺžka reťazca a dokonca aj verzia JDK.

S najnovšou verziou JDK 9+ (naše testy prebiehajú na JDK 10) majú obe implementácie pomerne rovnaké výsledky. Teraz poďme downgrade verzie JDK na 8 a testy znova:

Benchmark Mode Cnt Score Error Jednotky benchmarkStringReplace ss 10 48,061 ± 17,157 ms / op benchmarkStringUtilsReplace ss 10 14,478 ± 5,752 ms / op

Výkonový rozdiel je teraz obrovský a potvrdzuje teóriu, o ktorej sme hovorili na začiatku.

3.2. split ()

Než začneme, bude užitočné vyskúšať si metódy rozdelenia reťazcov dostupné v prostredí Java.

Ak je potrebné rozdeliť reťazec pomocou oddeľovača, prvá funkcia, ktorá nás napadne, je zvyčajne String.split (regex). Prináša však vážne problémy s výkonom, pretože prijíma argument regulárneho výrazu. Prípadne môžeme použiť StringTokenizer triedy na rozdelenie reťazca na tokeny.

Ďalšou možnosťou je Guava Rozdeľovač API. Konečne staré dobré indexOf () je tiež k dispozícii na zvýšenie výkonu našej aplikácie, ak nepotrebujeme funkčnosť regulárnych výrazov.

Teraz je čas napísať testovacie testy pre String.split () možnosť:

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

Pattern.split () :

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

StringTokenizer :

Zoznam stringTokenizer = nový ArrayList (); @Benchmark public List benchmarkStringTokenizer () {StringTokenizer st = nový StringTokenizer (longString); while (st.hasMoreTokens ()) {stringTokenizer.add (st.nextToken ()); } návrat stringTokenizer; }

String.indexOf () :

List stringSplit = new ArrayList (); @Benchmark public List benchmarkStringIndexOf () {int pos = 0, koniec; while ((end = longString.indexOf ('', pos))> = 0) {stringSplit.add (longString.substring (pos, end)); pos = koniec + 1; } vratit retazecSplit; }

Guava Rozdeľovač :

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

Nakoniec spustíme a porovnáme výsledky pre batchSize = 100 000:

Benchmark Mode Cnt Score Error Units benchmark GuavaSplitter 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,678 ms / op benchmark op

Ako vidíme, najhorší výkon má benchmarkStringSplitPattern metóda, kde používame Vzor trieda. Vo výsledku sa môžeme dozvedieť, že použitie triedy regex s split () Táto metóda môže spôsobiť stratu výkonu viackrát.

Podobne, všimli sme si, že najrýchlejšími výsledkami sú príklady použitia indexOf () a split ().

3.3. Konvertuje sa na String

V tejto časti budeme merať runtime skóre konverzie reťazcov. Aby sme boli konkrétnejší, preskúmame to Integer.toString () metóda zreťazenia:

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

String.valueOf () :

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

[nejaká celočíselná hodnota] + „“ :

@Benchmark public String benchmarkStringConvertPlus () {návrat sampleNumber + ""; }

String.format () :

Formát reťazcaDigit = "% d"; @Benchmark public String benchmarkStringFormat_d () {return String.format (formatDigit, sampleNumber); }

Po vykonaní testov uvidíme výstup pre batchSize = 10 000:

Benchmark Mode Cnt Score Error Jednotky 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

Po analýze výsledkov to vidíme test pre Integer.toString () má najlepšie skóre z 0.953 milisekundy. Naproti tomu konverzia, ktorá zahŕňa String.format („% d“) má najhorší výkon.

Je to logické, pretože analýza formátu String je drahá operácia.

3.4. Porovnávanie reťazcov

Zhodnoťme rôzne spôsoby porovnávania Struny. Počet iterácií je 100,000.

Tu sú naše testovacie testy pre String.equals () prevádzka:

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

String.equalsIgnoreCase () :

@Benchmark public boolean 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); }

Potom spustíme testy a zobrazíme výsledky:

Benchmark Mode Cnt Score Error Units 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 benchmarkStringMatchs ss 10 118,364 ± 43,203 ms / op

Ako vždy, čísla hovoria jasnou rečou. The zápasy() trvá najdlhšie, pretože používa regex na porovnanie rovnosti.

Naproti tomu the rovná sa () a equalsIgnoreCase() sú najlepšou voľbou.

3.5. String.matches () vs Predkompilovaný vzor

Teraz sa pozrime na samostatný pohľad String.matches () a Matcher.matches () vzory. Prvý z nich berie regexp ako argument a pred vykonaním ho zostaví.

Takže zakaždým, keď zavoláme String.matches (), zostavuje Vzor:

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

Druhá metóda opätovne používa Vzor objekt:

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

A teraz výsledky:

Benchmark Mode Cnt Score Error Units benchmark PrecompiledMatches ss 10 29,594 ± 12,784 ms / op benchmarkStringMatches ss 10 106,821 ± 46,963 ms / op

Ako vidíme, porovnávanie s predkompilovanými regexpmi funguje asi trikrát rýchlejšie.

3.6. Kontrola dĺžky

Na záver si porovnajme String.isEmpty () metóda:

@Benchmark public boolean benchmarkStringIsEmpty () {return longString.isEmpty (); }

a String.length () metóda:

@Benchmark public boolean benchmarkStringLengthZero () {return emptyString.length () == 0; }

Najprv im zavoláme longString = “Dobrý deň, som v priemere o niečo dlhší ako iné reťazce”. The batchSize je 10,000:

Benchmark Mode Cnt Score Error Jednotky benchmarkStringIsEmpty ss 10 0,295 ± 0,277 ms / op benchmarkStringLengthZero ss 10 0,472 ± 0,840 ms / op

Potom nastavíme longString = “” prázdny reťazec a znova spustiť testy:

Benchmark Mode Cnt Score Error Jednotky benchmarkStringIsEmpty ss 10 0,245 ± 0,362 ms / op benchmarkStringLengthZero ss 10 0,351 ± 0,473 ms / op

Ako sme si všimli, benchmarkStringLengthZero () a benchmarkStringIsEmpty () metódy majú v obidvoch prípadoch približne rovnaké skóre. Avšak volanie je prázdny() funguje rýchlejšie ako kontrola, či je dĺžka reťazca nulová.

4. Deduplikácia reťazcov

Od verzie JDK 8 je k dispozícii funkcia deduplikácie reťazcov, ktorá eliminuje spotrebu pamäte. Jednoducho povedané, tento nástroj hľadá reťazce s rovnakým alebo duplicitným obsahom na uloženie jednej kópie každej odlišnej hodnoty reťazca do fondu reťazcov.

V súčasnosti existujú dva spôsoby riešenia String duplikáty:

  • pomocou String.intern () ručne
  • umožňujúce deduplikáciu reťazcov

Pozrime sa podrobnejšie na každú z možností.

4.1. String.intern ()

Pred skokom vpred bude užitočné prečítať si v našom dokumente o manuálnom vkladaní. S String.intern () môžeme manuálne nastaviť referenciu String objekt vo vnútri globálu String bazén.

Potom môže JVM v prípade potreby použiť vrátenie referencie. Z hľadiska výkonu môže mať naša aplikácia obrovský úžitok z opätovného použitia reťazcových odkazov z konštantného fondu.

Je dôležité vedieť, že JVM String pool nie je pre vlákno lokálny. Každý String ktoré pridáme do fondu, je k dispozícii aj pre ďalšie vlákna.

Existujú však aj vážne nevýhody:

  • aby sme si správne udržali našu aplikáciu, možno budeme musieť nastaviť a -XX: StringTableSize Parameter JVM na zväčšenie veľkosti bazéna. Na zväčšenie veľkosti fondu potrebuje JVM reštart
  • volanie String.intern () manuálne je časovo náročné. Rastie v lineárnom časovom algoritme s O (n) zložitosť
  • navyše, časté hovory na dlho String predmety môžu spôsobiť problémy s pamäťou

Ak chcete mať nejaké osvedčené čísla, spustme test benchmarku:

@Benchmark public String benchmarkStringIntern () {return baeldung.intern (); }

Výstupné skóre sú navyše v milisekundách:

Benchmark 1 000 10 000 100 000 1 000 000 benchmarkStringIntern 0,433 2,243 19,996 204,373

Hlavičky stĺpcov tu predstavujú inú iterácie počíta od 1000 do 1,000,000. Pre každé iteračné číslo máme skóre výkonnosti testu. Ako sme si všimli, skóre sa dramaticky zvyšuje okrem počtu iterácií.

4.2. Povoliť deduplikáciu automaticky

Po prvé, táto možnosť je súčasťou zberača odpadu G1. Predvolene je táto funkcia zakázaná. Musíme ho teda povoliť pomocou nasledujúceho príkazu:

 -XX: + UseG1GC -XX: + UseStringDeduplication

Je dôležité si uvedomiť, že Povolenie tejto možnosti to nezaručuje String dôjde k deduplikácii. Tiež nespracováva mladé Struny. Z dôvodu riadenia minimálneho veku spracovania Struny, XX: StringDeduplicationAgeThreshold = 3 K dispozícii je možnosť JVM. Tu, 3 je predvolený parameter.

5. Zhrnutie

V tomto tutoriáli sa snažíme dať nejaké rady, ako efektívnejšie používať reťazce v našom každodennom živote kódovania.

Ako výsledok, môžeme zvýrazniť niektoré návrhy s cieľom zvýšiť výkonnosť našej aplikácie:

  • pri spájaní reťazcov sa StringBuilder je najpohodlnejšia možnosť to ma napadne. S malými reťazcami však + prevádzka má takmer rovnaký výkon. Pod kapotou môže kompilátor Java používať znak StringBuilder triedy na zníženie počtu reťazcových objektov
  • na prevod hodnoty do reťazca, [nejaký typ] .toString () (Integer.toString () napríklad) funguje rýchlejšie String.valueOf (). Pretože tento rozdiel nie je podstatný, môžeme ho voľne používať String.valueOf () aby nemal závislosť od typu vstupnej hodnoty
  • pokiaľ ide o porovnanie reťazcov, nič neprekoná String.equals () zatiaľ
  • String deduplikácia zvyšuje výkon vo veľkých aplikáciách s viacerými vláknami. Ale nadužívanie String.intern () môže spôsobiť vážne úniky pamäte a spomaliť aplikáciu
  • na rozdelenie strún, ktoré by sme mali použiť indexOf () vyhrať vo výkone. V niektorých nekritických prípadoch String.split () by sa táto funkcia mohla dobre hodiť
  • Použitím Pattern.match () struna výrazne zvyšuje výkon
  • String.isEmpty () je rýchlejší ako reťazec.length () == 0

Tiež majte na pamäti, že čísla, ktoré tu uvádzame, sú iba referenčnými výsledkami JMH - mali by ste preto vždy testovať v rozsahu svojho vlastného systému a doby behu, aby ste určili vplyv týchto druhov optimalizácií.

Nakoniec, ako vždy, kód použitý počas diskusie nájdete na GitHub.


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