Tips om streng ytelse

1. Introduksjon

I denne veiledningen, vi skal fokusere på ytelsesaspektet til Java String API.

Vi skal grave i det String opprettelses-, konverterings- og modifikasjonsoperasjoner for å analysere tilgjengelige alternativer og sammenligne effektiviteten.

Forslagene vi kommer med vil ikke nødvendigvis passe riktig for alle applikasjoner. Men absolutt, vi skal vise hvordan du kan vinne på ytelse når applikasjonens driftstid er kritisk.

2. Konstruere en ny streng

Som du vet, i Java er strenger uforanderlige. Så hver gang vi konstruerer eller sammenkoker a String objekt, oppretter Java et nytt String - Dette kan være spesielt kostbart hvis det gjøres i en løkke.

2.1. Bruke Constructor

I de fleste tilfeller, vi bør unngå å skape Strenger bruker konstruktøren med mindre vi vet hva vi gjør.

La oss lage en newString objektet på innsiden av løkken først ved å bruke ny streng () konstruktør, deretter = operatør.

For å skrive vår referanse, bruker vi JMH (Java Microbenchmark Harness) -verktøyet.

Vår konfigurasjon:

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

Her bruker vi SingeShotTime modus, som bare kjører metoden en gang. Som vi vil måle ytelsen til String operasjoner inne i løkken, det er en @Mål kommentar tilgjengelig for det.

Viktig å vite, det benchmarking looper direkte i testene våre kan føre til at resultatene blir skjevt på grunn av ulike optimaliseringer som brukes av JVM.

Så vi beregner bare enkeltoperasjonen og lar JMH ta seg av loopingen. Kort fortalt utfører JMH gjentakelsene ved å bruke Partistørrelse, Gruppestørrelse parameter.

La oss nå legge til den første mikrobenchmarken:

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

I den første testen opprettes et nytt objekt i hver iterasjon. I den andre testen opprettes objektet bare en gang. For gjenværende iterasjoner returneres det samme objektet fra String's konstant basseng.

La oss kjøre testene med løpende iterasjoner = 1,000,000 og se resultatene:

Referansemodus Cnt Score Error Units benchmarkStringConstructor ss 10 16.089 ± 3.355 ms / op benchmarkStringLiteral ss 10 9.523 ± 3.331 ms / op

Fra Resultat verdier, kan vi tydelig se at forskjellen er betydelig.

2.2. + Operatør

La oss se på dynamikken String sammenføyningseksempel:

@State (Scope.Thread) offentlig statisk klasse StringPerformanceHints {String result = ""; String baeldung = "baeldung"; } @Benchmark public String benchmarkStringDynamicConcat () {returresultat + baeldung; } 

I resultatene ønsker vi å se den gjennomsnittlige utførelsestiden. Utdata nummerformatet er satt til millisekunder:

Referanseverdi 1000 10.000 referanseverdiStringDynamicConcat 47.331 4370.411

La oss nå analysere resultatene. Som vi ser, legger til 1000 gjenstander til state.result tar 47.331 millisekunder. Følgelig, økende antall iterasjoner på 10 ganger, vokser kjøretiden til 4370.441 millisekunder.

Oppsummert vokser henrettelsestiden kvadratisk. Derfor er kompleksiteten av dynamisk sammenkobling i en løkke med n iterasjoner O (n ^ 2).

2.3. String.concat ()

En annen måte å sammenkoble Strenger er ved å bruke concat () metode:

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

Utgangstidsenheten er et millisekund, antall iterasjoner er 100.000. Resultattabellen ser ut som:

Referansemodus Cnt Score Error Units benchmarkStringConcat ss 10 3403.146 ± 852.520 ms / op

2.4. String.format ()

En annen måte å lage strenger på er å bruke String.format () metode. Under panseret bruker den vanlige uttrykk for å analysere innspillene.

La oss skrive JMH-testsaken:

String formatString = "hallo% s, hyggelig å møte deg"; @Benchmark public String benchmarkStringFormat_s () {return String.format (formatString, baeldung); }

Etterpå kjører vi det og ser resultatene:

Antall gjentakelser 10.000 100.000 1.000.000 referanseindeksStringFormat_s 17.181 140.456 1636.279 ms / op

Selv om koden med String.format () ser mer ren og lesbar ut, vi vinner ikke her når det gjelder ytelse.

2.5. StringBuilder og StringBuffer

Vi har allerede en beskrivelse som forklarer StringBuffer og StringBuilder. Så her viser vi bare ekstra informasjon om ytelsen deres. StringBuilder bruker en resizable matrise og en indeks som indikerer plasseringen til den siste cellen som ble brukt i matrisen. Når matrisen er full utvider den den doble størrelsen og kopierer alle tegnene i den nye matrisen.

Med tanke på at størrelsesendring ikke forekommer veldig ofte, vi kan vurdere hver legg til () drift som O (1) konstant tid. Tatt i betraktning dette har hele prosessen gjort På) kompleksitet.

Etter endring og kjøring av den dynamiske sammenkoblingstesten for StringBuffer og StringBuilder, vi får:

Referansemodus Cnt Score Error Units benchmarkStringBuffer ss 10 1.409 ± 1.665 ms / op benchmarkStringBuilder ss 1.200 ± 0.648 ms / op

Selv om poengsumforskjellen ikke er mye, kan vi legge merke til at StringBuilder fungerer raskere.

Heldigvis trenger vi ikke i enkle tilfeller StringBuilder å sette en String med en annen. Noen ganger, statisk sammenkobling med + kan faktisk erstatte StringBuilder. Under panseret vil de nyeste Java-kompilatorene ringe StringBuilder.append () å sammenkoble strenger.

Dette betyr å vinne i ytelse betydelig.

3. Utility Operations

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

Interessant å vite, det Apache Commons versjon for å erstatte String gjør det bedre enn Stringens egen erstatte() metode. Svaret på denne forskjellen ligger under implementeringen av dem. String.replace () bruker et regex-mønster for å matche String.

I motsetning, StringUtils.replace () bruker mye oversikt over(), som er raskere.

Nå er det tid for referansetestene:

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

Sette inn Partistørrelse, Gruppestørrelse til 100.000 presenterer vi resultatene:

Referansemodus Cnt Score Error Units benchmarkStringBytte ss 10 6.233 ± 2.922 ms / op benchmarkStringUtilsBytte ss 10 5.355 ± 2.497 ms / op

Selv om forskjellen mellom tallene ikke er for stor, er StringUtils.replace () har bedre poengsum. Selvfølgelig kan tallene og gapet mellom dem variere avhengig av parametere som antall iterasjoner, strenglengde og til og med JDK-versjon.

Med de nyeste JDK 9+ (testene våre kjører på JDK 10) har begge implementeringene ganske like resultater. La oss nå nedgradere JDK-versjonen til 8 og testene igjen:

Referansemodus Cnt Score Error Units benchmarkStringReplace ss 10 48,061 ± 17,157 ms / op benchmarkStringUtils Bytt ss 10 14,478 ± 5,752 ms / op

Ytelsesforskjellen er enorm nå og bekrefter teorien som vi diskuterte i begynnelsen.

3.2. dele()

Før vi begynner, vil det være nyttig å sjekke ut strengdelingsmetoder som er tilgjengelige i Java.

Når det er behov for å dele en streng med avgrenseren, er den første funksjonen som kommer opp i tankene våre vanligvis String.split (regex). Imidlertid gir det noen alvorlige ytelsesproblemer, da det godtar et regex-argument. Alternativt kan vi bruke StringTokenizer klasse for å bryte strengen i tokens.

Et annet alternativ er Guavas Splitter API. Endelig den gode gamle oversikt over() er også tilgjengelig for å øke applikasjonens ytelse hvis vi ikke trenger funksjonaliteten til vanlige uttrykk.

Nå er det på tide å skrive referansetestene for String.split () alternativ:

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

Pattern.split () :

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

StringTokenizer :

Liste stringTokenizer = ny ArrayList (); @Benchmark public List benchmarkStringTokenizer () {StringTokenizer st = new StringTokenizer (longString); mens (st.hasMoreTokens ()) {stringTokenizer.add (st.nextToken ()); } returnere stringTokenizer; }

String.indexOf () :

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

Guava Splitter :

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

Til slutt kjører vi og sammenligner resultatene for batchSize = 100.000:

Referansemodus Cnt Score Error Units 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.678 ms / op benchmarkString op

Som vi ser, har den verste ytelsen den benchmarkStringSplitPattern metode, der vi bruker Mønster klasse. Som et resultat kan vi lære at bruk av en regex-klasse med dele() metoden kan føre til tap av ytelse flere ganger.

Like måte, Vi merker at de raskeste resultatene gir eksempler med bruk av indexOf () og split ().

3.3. Konverterer til String

I denne delen skal vi måle kjøretidsresultatene for strengkonvertering. For å være mer spesifikk, vil vi undersøke Integer.toString () sammenkjøringsmetode:

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

String.valueOf () :

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

[noe heltall] + “” :

@Benchmark public String benchmarkStringConvertPlus () {return sampleNumber + ""; }

String.format () :

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

Etter å ha kjørt testene, ser vi utdataene for batchSize = 10.000:

Referansemodus Cnt Score feil

Etter å ha analysert resultatene ser vi det testen for Integer.toString () har best poengsum på 0.953 millisekunder. Derimot en konvertering som innebærer String.format (“% d”) har den dårligste ytelsen.

Det er logisk fordi man analyserer formatet String er en kostbar operasjon.

3.4. Sammenligne strenger

La oss evaluere forskjellige måter å sammenligne på Strenger. Antallet iterasjoner er 100,000.

Her er våre referansetester for String.equals () operasjon:

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

Etterpå kjører vi testene og viser resultatene:

Referansemodus 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 benchmarkStringMatches ss 10 118.364 ± 43.203 ms / op

Som alltid snakker tallene for seg selv. De fyrstikker() tar lengst tid ettersom den bruker regex for å sammenligne likestillingen.

I motsetning, de er lik() og er likIgnoreCase() er de beste valgene.

3.5. String.matches () vs. Forkompilert mønster

La oss ta en egen titt på String.matches () og Matcher.matches () mønstre. Den første tar en regexp som argument og kompilerer den før den kjøres.

Så hver gang vi ringer String.matches (), kompilerer den Mønster:

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

Den andre metoden gjenbruker Mønster gjenstand:

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

Og nå resultatene:

Referansemodus Cnt Score Error

Som vi ser, fungerer matching med forhåndskompilert regexp omtrent tre ganger raskere.

3.6. Kontrollere lengden

Til slutt, la oss sammenligne String.isEmpty () metode:

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

og Strenglengde () metode:

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

Først kaller vi dem over longString = “Hei baeldung, jeg er litt lengre enn andre strenger i gjennomsnitt” streng. De Partistørrelse, Gruppestørrelse er 10,000:

Referansemodus Cnt Score Error Units benchmarkStringIsEmpty ss 10 0.295 ± 0.277 ms / op benchmarkStringLengthZero ss 10 0.472 ± 0.840 ms / op

Etter, la oss stille inn longString = “” tom streng og kjør testene igjen:

Referansemodus Cnt Score Error Units benchmarkStringIsEmpty ss 10 0.245 ± 0.362 ms / op benchmarkStringLengthZero ss 10 0.351 ± 0.473 ms / op

Som vi merker, benchmarkStringLengthZero () og benchmarkStringIsEmpty () metoder i begge tilfeller har omtrent samme poengsum. Imidlertid ringer er tom() fungerer raskere enn å sjekke om strengens lengde er null.

4. Strengduplisering

Siden JDK 8 er streng dedupliseringsfunksjon tilgjengelig for å eliminere minneforbruk. For å si det enkelt, dette verktøyet leter etter strengene med samme eller dupliserte innhold for å lagre en kopi av hver distinkt strengverdi i strengbassenget.

For tiden er det to måter å håndtere String duplikater:

  • bruker String.intern () manuelt
  • muliggjør streng deduplisering

La oss se nærmere på hvert alternativ.

4.1. String.intern ()

Før du hopper fremover, vil det være nyttig å lese om manuell interning i oppskriften. Med String.intern () vi kan manuelt sette referansen til String objekt inne i det globale String basseng.

Deretter kan JVM bruke referansen når det er nødvendig. Fra ytelsens synspunkt kan applikasjonen vår ha stor fordel ved å bruke strengreferansene fra det konstante bassenget.

Viktig å vite, det JVM String bassenget er ikke lokalt for tråden. Hver String som vi legger til i bassenget, er også tilgjengelig for andre tråder.

Imidlertid er det også alvorlige ulemper:

  • for å vedlikeholde søknaden vår riktig, kan det hende vi må sette inn -XX: StringTableSize JVM-parameter for å øke bassengstørrelsen. JVM trenger en omstart for å utvide bassengstørrelsen
  • ringer String.intern () manuelt er tidkrevende. Den vokser i en lineær tidsalgoritme med På) kompleksitet
  • i tillegg hyppige samtaler på lenge String gjenstander kan forårsake hukommelsesproblemer

For å ha noen dokumenterte tall, la oss kjøre en referansetest:

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

I tillegg er resultatene i millisekunder:

Referanseindeks 1000 10.000 100.000 1.000.000 referanseindeksStringIntern 0.433 2.243 19.996 204.373

Kolonneoverskriftene her representerer en annen iterasjoner teller fra 1000 til 1,000,000. For hvert iterasjonsnummer har vi testresultatene. Som vi merker, øker poengsummen dramatisk i tillegg til antall iterasjoner.

4.2. Aktiver fraduplisering automatisk

Først av alt, dette alternativet er en del av G1 søppeloppsamleren. Som standard er denne funksjonen deaktivert. Så vi må aktivere det med følgende kommando:

 -XX: + UseG1GC -XX: + UseStringDeduplication

Viktig å merke seg, at aktivering av dette alternativet garanterer ikke det String deduplisering vil skje. Dessuten behandler den ikke ung Strenger. For å klare den minste aldersgrensen for behandling Strenger, XX: StringDeduplicationAgeThreshold = 3 JVM-alternativet er tilgjengelig. Her, 3 er standardparameteren.

5. Sammendrag

I denne opplæringen prøver vi å gi noen tips om å bruke strenger mer effektivt i vårt daglige kodeliv.

Som et resultat, vi kan markere noen forslag for å øke applikasjonsytelsen:

  • ved sammenkobling av strenger, StringBuilder er det mest praktiske alternativet som kommer til hjernen. Imidlertid, med de små strengene, er + operasjonen har nesten samme ytelse. Under panseret kan Java-kompilatoren bruke StringBuilder klasse for å redusere antall strengobjekter
  • for å konvertere verdien til strengen, [noen type] .toString () (Integer.toString () for eksempel) fungerer raskere da String.valueOf (). Fordi denne forskjellen ikke er signifikant, kan vi fritt bruke String.valueOf () å ikke ha en avhengighet av inngangsverditypen
  • når det gjelder strengesammenligning, slår ingenting String.equals () så langt
  • String deduplisering forbedrer ytelsen i store applikasjoner med flere tråder. Men overbruk String.intern () kan forårsake alvorlige minnelekkasjer, noe som reduserer applikasjonen
  • for å dele strengene vi skal bruke oversikt over() å vinne i ytelse. Imidlertid i noen ikke-kritiske tilfeller String.split () funksjonen kan passe godt
  • Ved hjelp av Mønster.match () strengen forbedrer ytelsen betydelig
  • String.isEmpty () er raskere enn String.length () == 0

Også, husk at tallene vi presenterer her bare er JMH-referanseresultater - så du bør alltid teste i omfanget av ditt eget system og kjøretid for å bestemme virkningen av denne typen optimaliseringer.

Til slutt, som alltid, finner du koden som ble brukt under diskusjonen på GitHub.


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