Tip til strengydelse

1. Introduktion

I denne vejledning vi vil fokusere på ydeevne aspektet af Java String API.

Vi graver ind i Snor oprettelse, konvertering og modifikation for at analysere de tilgængelige muligheder og sammenligne deres effektivitet.

De forslag, vi kommer med, passer ikke nødvendigvis til den rette applikation. Men bestemt skal vi vise, hvordan man vinder på ydeevne, når applikationens driftstid er kritisk.

2. Konstruktion af en ny streng

Som du ved, i Java er strenge uforanderlige. Så hver gang vi konstruerer eller sammenkæder en Snor objekt opretter Java et nyt Streng - dette kan være særligt dyrt, hvis det gøres i en løkke.

2.1. Brug af Constructor

I de fleste tilfælde, vi bør undgå at skabe Strenge ved hjælp af konstruktøren, medmindre vi ved, hvad vi laver.

Lad os oprette en newString objekt inde i sløjfen først ved hjælp af ny streng () konstruktør, derefter = operatør.

For at skrive vores benchmark bruger vi JMH (Java Microbenchmark Harness) -værktøjet.

Vores konfiguration:

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

Her bruger vi SingeShotTime tilstand, som kun kører metoden en gang. Som vi vil måle ydeevnen for Snor operationer inde i sløjfen, er der en @Måling kommentar tilgængelig for det.

Vigtigt at vide det benchmarking sløjfer direkte i vores tests kan skæve resultaterne på grund af forskellige optimeringer anvendt af JVM.

Så vi beregner kun den enkelte operation og lader JMH tage sig af loopingen. Kort sagt udfører JMH gentagelserne ved hjælp af batchSize parameter.

Lad os nu tilføje det første mikro-benchmark:

@Benchmark public String benchmarkStringConstructor () {returner ny streng ("baeldung"); } @Benchmark public String benchmarkStringLiteral () {return "baeldung"; }

I den første test oprettes et nyt objekt i hver iteration. I den anden test oprettes objektet kun en gang. For resterende iterationer returneres det samme objekt fra String's konstant pool.

Lad os køre testene med antallet af sløjfe-iterationer = 1,000,000 og se resultaterne:

Benchmark Mode Cnt Score Error Enheder benchmarkStringConstructor ss 10 16.089 ± 3.355 ms / op benchmarkStringLiteral ss 10 9.523 ± 3.331 ms / op

Fra Score værdier, kan vi tydeligt se, at forskellen er signifikant.

2.2. + Operatør

Lad os se på dynamikken Snor sammenkædningseksempel:

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

I vores resultater ønsker vi at se den gennemsnitlige udførelsestid. Outputnummerformatet er indstillet til millisekunder:

Benchmark 1000 10.000 benchmarkStringDynamicConcat 47.331 4370.411

Lad os nu analysere resultaterne. Som vi ser, tilføjer 1000 genstande til state.result tager 47.331 millisekunder. Derfor øges kørselstiden til at øge antallet af iterationer på 10 gange 4370.441 millisekunder.

Sammenfattende vokser udførelsestidspunktet kvadratisk. Derfor er kompleksiteten af ​​dynamisk sammenkædning i en loop af n iterationer O (n ^ 2).

2.3. String.concat ()

En anden måde at sammenkæde Strenge er ved hjælp af concat () metode:

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

Output-tidsenheden er en millisekund, antallet af iterationer er 100.000. Resultattabellen ser ud som:

Benchmark Mode Cnt Score Error Enheder benchmarkStringConcat ss 10 3403.146 ± 852.520 ms / op

2.4. String.format ()

En anden måde at oprette strenge på er ved hjælp af String.format () metode. Under hætten bruger den regelmæssige udtryk til at analysere input.

Lad os skrive JMH test case:

String formatString = "hej% s, dejligt at møde dig"; @Benchmark public String benchmarkStringFormat_s () {return String.format (formatString, baeldung); }

Derefter kører vi det og ser resultaterne:

Antal gentagelser 10.000 100.000 1.000.000 benchmarkStringFormat_s 17.181 140.456 1636.279 ms / op

Selvom koden med String.format () ser mere ren og læsbar ud, vi vinder ikke her med hensyn til ydeevne.

2.5. StringBuilder og StringBuffer

Vi har allerede en opskrivning, der forklarer StringBuffer og StringBuilder. Så her viser vi kun ekstra information om deres præstationer. StringBuilder bruger et resizable array og et indeks, der angiver placeringen af ​​den sidste celle, der blev brugt i arrayet. Når arrayet er fuldt udvider det det dobbelte af dets størrelse og kopierer alle tegnene i det nye array.

Under hensyntagen til, at størrelsesændring ikke forekommer meget ofte, vi kan overveje hver Tilføj() drift som O (1) konstant tid. Under hensyntagen til dette har hele processen gjort På) kompleksitet.

Efter ændring og kørsel af den dynamiske sammenkædningstest for StringBuffer og StringBuilder, vi får:

Benchmark-tilstand Cnt Score Fejl Enheder benchmarkStringBuffer ss 10 1.409 ± 1.665 ms / op benchmarkStringBuilder ss 1.200 ± 0.648 ms / op

Selvom score forskellen ikke er meget, kan vi bemærke at StringBuilder fungerer hurtigere.

I enkle tilfælde har vi heldigvis ikke brug for det StringBuilder at sætte en Snor med en anden. Sommetider, statisk sammenkædning med + kan faktisk erstatte StringBuilder. Under emhætten kalder de nyeste Java-compilere StringBuilder.append () at sammenkæde strenge.

Dette betyder at vinde i præstation betydeligt.

3. Hjælpeprogrammer

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

Interessant at vide det Apache Commons version til udskiftning af Snor klarer sig bedre end strengens egen erstatte() metode. Svaret på denne forskel ligger under deres implementering. String.replace () bruger et regex-mønster til at matche Snor.

I modsætning, StringUtils.replace () bruger i vid udstrækning indeks af(), hvilket er hurtigere.

Nu er det tid til benchmarktestene:

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

Indstilling af batchSize til 100.000, præsenterer vi resultaterne:

Benchmark-tilstand Cnt-score Fejl Enheder benchmarkStringReplace ss 10 6.233 ± 2.922 ms / op benchmarkStringUtils Erstat ss 10 5.355 ± 2.497 ms / op

Selvom forskellen mellem tallene ikke er for stor, er StringUtils.replace () har en bedre score. Naturligvis kan tallene og afstanden mellem dem variere afhængigt af parametre som antal gentagelser, strenglængde og endda JDK-version.

Med de nyeste JDK 9+ (vores tests kører på JDK 10) versioner har begge implementeringer nogenlunde lige resultater. Lad os nu nedgradere JDK-versionen til 8 og testene igen:

Benchmark-tilstand Cnt-score Fejl Enheder benchmarkStringReplace ss 10 48.061 ± 17.157 ms / op benchmarkStringUtils Udskift ss 10 14.478 ± 5.752 ms / op

Ydelsesforskellen er enorm nu og bekræfter den teori, som vi diskuterede i starten.

3.2. dele()

Før vi starter, vil det være nyttigt at tjekke de strengopdelingsmetoder, der er tilgængelige i Java.

Når der er behov for at opdele en streng med afgrænseren, er den første funktion, der kommer til vores sind, normalt String.split (regex). Det bringer dog nogle alvorlige præstationsproblemer, da det accepterer et regex-argument. Alternativt kan vi bruge StringTokenizer klasse for at bryde strengen i tokens.

En anden mulighed er Guavas Splitter API. Endelig den gode gamle indeks af() er også tilgængelig for at øge vores applikations ydeevne, hvis vi ikke har brug for funktionaliteten af ​​regulære udtryk.

Nu er det tid til at skrive benchmarktestene til String.split () mulighed:

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

Mønster.split () :

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

StringTokenizer :

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

String.indexOf () :

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

Guava Splitter :

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

Endelig kører vi og sammenligner resultater for batchSize = 100.000:

Benchmark Mode Cnt Score Error Enheder 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 værste præstation den benchmarkStringSplitPattern metode, hvor vi bruger Mønster klasse. Som et resultat kan vi lære at bruge en regex-klasse med dele() metode kan medføre præstationstab flere gange.

Ligeledes, vi bemærker, at de hurtigste resultater giver eksempler med brugen af indexOf () og split ().

3.3. Konvertering til Snor

I dette afsnit skal vi måle runtime-score for strengkonvertering. For at være mere specifik, vil vi undersøge Integer.toString () sammenkædningsmetode:

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

String.valueOf () :

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

[noget heltal] + “” :

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

String.format () :

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

Efter at have kørt testene ser vi output for batchSize = 10.000:

Benchmark Mode Cnt Score Fejl Enheder 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 / op

Efter at have analyseret resultaterne ser vi det testen for Integer.toString () har den bedste score på 0.953 millisekunder. Derimod en konvertering, der involverer String.format (“% d”) har den dårligste præstation.

Det er logisk, fordi man analyserer formatet Snor er en dyr operation.

3.4. Sammenligning af strenge

Lad os evaluere forskellige måder at sammenligne på Strenge. Antallet af iterationer er 100,000.

Her er vores benchmarktest til String.equals () operation:

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

Derefter kører vi testene og viser resultaterne:

Benchmark Mode Cnt Score Fejl Enheder 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 altid taler tallene for sig selv. Det Tændstikker() tager længst tid, da det bruger regex til at sammenligne ligestillingen.

I modsætning, det lige med() og er lig medIgnoreCase() er de bedste valg.

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

Lad os nu se et separat kig på String.matches () og Matcher.matches () mønstre. Den første tager en regexp som et argument og kompilerer den, før den udføres.

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

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

Den anden metode genbruger Mønster objekt:

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

Og nu er resultaterne:

Benchmark Mode Cnt Score Fejl Enheder benchmark ForkompileretMatches ss 10 29.594 ± 12.784 ms / op benchmarkStringMatches ss 10 106.821 ± 46.963 ms / op

Som vi ser fungerer matchning med forudkompileret regexp cirka tre gange hurtigere.

3.6. Kontrol af længden

Lad os endelig sammenligne String.isEmpty () metode:

@Benchmark offentlig boolsk benchmarkStringIsEmpty () {return longString.isEmpty (); }

og Strenglængde () metode:

@Benchmark public boolean benchmarkStringLengthZero () {returner tomString.length () == 0; }

Først kalder vi dem over longString = “Hej baeldung, jeg er lidt længere end andre strenge i gennemsnit” streng. Det batchSize er 10,000:

Benchmark-tilstand Cnt Score Fejl Enheder benchmarkStringIsEmpty ss 10 0.295 ± 0.277 ms / op benchmarkStringLengthZero ss 10 0.472 ± 0.840 ms / op

Lad os derefter indstille longString = “” tom streng og kør testene igen:

Benchmark Mode Cnt Score Fejl Enheder benchmarkStringIsEmpty ss 10 0.245 ± 0.362 ms / op benchmarkStringLengthZero ss 10 0.351 ± 0.473 ms / op

Som vi bemærker, benchmarkStringLengthZero () og benchmarkStringIsEmpty () metoder i begge tilfælde har omtrent samme score. Dog ringer er tom() fungerer hurtigere end at kontrollere, om strengens længde er nul.

4. Deduplikering af streng

Siden JDK 8 er string deduplication-funktion tilgængelig for at eliminere hukommelsesforbrug. Kort fortalt, dette værktøj er på udkig efter strengene med det samme eller duplikerede indhold for at gemme en kopi af hver særskilt strengværdi i strengpoolen.

I øjeblikket er der to måder at håndtere Snor dubletter:

  • bruger String.intern () manuelt
  • muliggør streng deduplicering

Lad os se nærmere på hver mulighed.

4.1. String.intern ()

Inden du springer videre, vil det være nyttigt at læse om manuel interning i vores opskrivning. Med String.intern () vi kan manuelt indstille referencen til Snor objekt inde i det globale Snor pool.

Derefter kan JVM bruge returnere referencen, når det er nødvendigt. Set fra ydeevnen kan vores ansøgning have en enorm fordel ved at genbruge strengreferencer fra den konstante pool.

Vigtigt at vide det JVM Snor poolen er ikke lokal for tråden. Hver Snor som vi føjer til puljen, er også tilgængelig for andre tråde.

Der er dog også alvorlige ulemper:

  • for at vedligeholde vores ansøgning korrekt skal vi muligvis indstille en -XX: StringTableSize JVM-parameter for at øge poolstørrelsen. JVM har brug for en genstart for at udvide poolstørrelsen
  • ringer String.intern () manuelt er tidskrævende. Det vokser i en lineær tidsalgoritme med På) kompleksitet
  • derudover hyppige opkald på lang Snor genstande kan forårsage hukommelsesproblemer

For at have nogle dokumenterede tal, lad os køre en benchmark test:

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

Derudover er output score i millisekunder:

Benchmark 1000 10.000 100.000 1.000.000 benchmarkStringIntern 0.433 2.243 19.996 204.373

Kolonneoverskrifterne her repræsenterer en anden gentagelser tæller fra 1000 til 1,000,000. For hvert iterationsnummer har vi testets præstationsscore. Som vi bemærker, stiger scoren dramatisk ud over antallet af iterationer.

4.2. Aktivér deduplisering automatisk

Først og fremmest, denne mulighed er en del af G1-affaldssamleren. Som standard er denne funktion deaktiveret. Så vi er nødt til at aktivere det med følgende kommando:

 -XX: + UseG1GC -XX: + UseStringDeduplication

Vigtigt at bemærke, at Aktivering af denne mulighed garanterer ikke det Snor deduplikering vil ske. Det behandler heller ikke ung Strenge. For at styre den minimale alder af behandlingen Strings, XX: StringDeduplicationAgeThreshold = 3 JVM-indstilling er tilgængelig. Her, 3 er standardparameteren.

5. Resume

I denne vejledning forsøger vi at give nogle tip til at bruge strenge mere effektivt i vores daglige kodningsliv.

Som resultat, Vi kan fremhæve nogle forslag for at øge vores applikationsydelse:

  • ved sammenkædning af strenge, StringBuilder er den mest bekvemme mulighed der kommer til at tænke på. Men med de små strenge, den + betjening har næsten samme ydeevne. Under emhætten kan Java-kompilatoren bruge StringBuilder klasse for at reducere antallet af strengobjekter
  • for at konvertere værdien til strengen, [en eller anden type] .toString () (Integer.toString () fungerer f.eks. hurtigere String.valueOf (). Fordi denne forskel ikke er signifikant, kan vi frit bruge String.valueOf () for ikke at have en afhængighed af inputværditypen
  • når det kommer til strengesammenligning, slår intet den String.equals () indtil nu
  • Snor deduplikering forbedrer ydeevnen i store applikationer med flere tråde. Men overforbrug String.intern () kan medføre alvorlige hukommelseslækager, hvilket nedsætter applikationen
  • til opdeling af strengene, vi skal bruge indeks af() at vinde i ydeevne. I nogle ikke-kritiske tilfælde String.split () funktion kan være en god pasform
  • Ved brug af Mønster. Match () strengen forbedrer ydeevnen markant
  • String.isEmpty () er hurtigere end streng.length () == 0

Også, husk, at de tal, vi præsenterer her, kun er JMH-benchmark-resultater - så du skal altid teste i omfanget af dit eget system og runtime for at bestemme virkningen af ​​denne form for optimeringer.

Endelig, som altid, kan koden, der blev brugt under diskussionen, findes på GitHub.


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