String prestatiehints

1. Inleiding

In deze tutorial we gaan ons concentreren op het prestatieaspect van de Java String API.

We zullen er in graven Draad creatie-, conversie- en wijzigingsoperaties om de beschikbare opties te analyseren en hun efficiëntie te vergelijken.

De suggesties die we gaan doen, zijn niet per se geschikt voor elke toepassing. Maar we gaan zeker laten zien hoe u kunt winnen op het gebied van prestaties wanneer de looptijd van de applicatie kritiek is.

2. Een nieuwe string construeren

Zoals u weet, zijn strings in Java onveranderlijk. Dus elke keer dat we een Draad object, maakt Java een nieuw Draad - dit kan vooral duur zijn als het in een lus wordt gedaan.

2.1. Constructor gebruiken

In de meeste gevallen, we moeten vermijden om te creëren Snaren met behulp van de constructor, tenzij we weten wat we doen.

Laten we een newString object in de lus eerst met behulp van de nieuwe String () constructor en vervolgens het = operator.

Om onze benchmark te schrijven, gebruiken we de JMH-tool (Java Microbenchmark Harness).

Onze configuratie:

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

Hier gebruiken we de SingeShotTime mode, die de methode slechts één keer uitvoert. Omdat we de prestaties van Draad operaties binnen de lus, is er een @Meting annotatie daarvoor beschikbaar.

Belangrijk om te weten, dat benchmarking loops direct in onze tests kunnen de resultaten vertekenen vanwege verschillende optimalisaties die door JVM zijn toegepast.

We berekenen dus alleen de enkele bewerking en laten JMH de looping verzorgen. Kort gezegd voert JMH de iteraties uit met behulp van de seriegrootte parameter.

Laten we nu de eerste micro-benchmark toevoegen:

@Benchmark public String benchmarkStringConstructor () {retourneer nieuwe String ("baeldung"); } @Benchmark public String benchmarkStringLiteral () {retourneer "baeldung"; }

In de eerste test wordt bij elke iteratie een nieuw object gemaakt. In de tweede test wordt het object slechts één keer gemaakt. Voor resterende iteraties wordt hetzelfde object geretourneerd vanuit de String's constant zwembad.

Laten we de tests uitvoeren met het aantal herhalingen = 1,000,000 en zie de resultaten:

Benchmarkmodus Cnt Score Fout Eenheden benchmarkStringConstructor ss 10 16.089 ± 3.355 ms / op benchmarkStringLiteral ss 10 9.523 ± 3.331 ms / op

Van de Score waarden, kunnen we duidelijk zien dat het verschil significant is.

2.2. + Operator

Laten we eens kijken naar dynamiek Draad aaneenschakelingsvoorbeeld:

@State (Scope.Thread) openbare statische klasse StringPerformanceHints {String result = ""; String baeldung = "baeldung"; } @Benchmark public String benchmarkStringDynamicConcat () {resultaat + baeldung; } 

In onze resultaten willen we de gemiddelde uitvoeringstijd zien. Het formaat van het uitvoernummer is ingesteld op milliseconden:

Benchmark 1000 10.000 benchmarkStringDynamicConcat 47.331 4370.411

Laten we nu de resultaten analyseren. Zoals we zien, toevoegen 1000 items naar state.result neemt 47.331 milliseconden. Dientengevolge, door het aantal iteraties in 10 keer te verhogen, groeit de looptijd naar 4370.441 milliseconden.

Samenvattend groeit de tijd van uitvoering kwadratisch. Daarom is de complexiteit van dynamische aaneenschakeling in een lus van n iteraties O (n ^ 2).

2.3. String.concat ()

Nog een manier om samen te voegen Snaren is door de concat () methode:

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

De uitvoertijdeenheid is een milliseconde, het aantal iteraties is 100.000. De resultatentabel ziet er als volgt uit:

Benchmarkmodus Cnt Score Fout Eenheden benchmarkStringConcat ss 10 3403.146 ± 852.520 ms / op

2.4. String.format ()

Een andere manier om strings te maken, is door String.format () methode. Onder de motorkap gebruikt het reguliere expressies om de invoer te ontleden.

Laten we de JMH-testcase schrijven:

String formatString = "hallo% s, leuk je te ontmoeten"; @Benchmark public String benchmarkStringFormat_s () {return String.format (formatString, baeldung); }

Daarna voeren we het uit en zien we de resultaten:

Aantal herhalingen 10.000 100.000 1.000.000 benchmarkStringFormat_s 17.181 140.456 1636.279 ms / op

Hoewel de code met String.format () ziet er netter en leesbaarder uit, we winnen hier niet op het gebied van prestaties.

2.5. StringBuilder en StringBuffer

We hebben al een artikel waarin wordt uitgelegd StringBuffer en StringBuilder. Dus hier laten we alleen extra informatie zien over hun prestaties. StringBuilder gebruikt een aanpasbare array en een index die de positie aangeeft van de laatste cel die in de array is gebruikt. Als de array vol is, wordt het dubbel zo groot vergroot en worden alle tekens naar de nieuwe array gekopieerd.

Rekening houdend met het feit dat formaatwijziging niet vaak voorkomt, we kunnen elk overwegen toevoegen () werking als O (1) constante tijd. Hiermee rekening houdend, heeft het hele proces Aan) complexiteit.

Na het wijzigen en uitvoeren van de dynamische aaneenschakeltest voor StringBuffer en StringBuilder, we krijgen:

Benchmarkmodus Cnt Score Fout Eenheden benchmarkStringBuffer ss 10 1.409 ± 1.665 ms / op benchmarkStringBuilder ss 10 1.200 ± 0.648 ms / op

Hoewel het scoreverschil niet veel is, kunnen we het merken dat StringBuilder werkt sneller.

Gelukkig hebben we in eenvoudige gevallen geen StringBuilder om er een te plaatsen Draad met iemand anders. Soms, statische aaneenschakeling met + kan eigenlijk vervangen StringBuilder. Onder de motorkap zullen de nieuwste Java-compilers het StringBuilder.append () om tekenreeksen samen te voegen.

Dit betekent aanzienlijk winnen in prestaties.

3. Hulpprogramma's

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

Interessant om te weten, dat Apache Commons-versie voor het vervangen van het Draad doet veel beter dan die van de String vervangen() methode. Het antwoord op dit verschil ligt in de implementatie ervan. String.replace () gebruikt een regex-patroon dat overeenkomt met het Draad.

In tegenstelling tot, StringUtils.replace () wordt veel gebruikt index van(), welke is sneller.

Nu is het tijd voor de benchmarktests:

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

Instellen van de seriegrootte tot 100.000 presenteren we de resultaten:

Benchmarkmodus Cnt Score Fout Eenheden benchmarkStringReplace ss 10 6.233 ± 2.922 ms / op benchmarkStringUtilsReplace ss 10 5.355 ± 2.497 ms / op

Hoewel het verschil tussen de cijfers niet al te groot is, is de StringUtils.replace () heeft een betere score. Natuurlijk kunnen de nummers en de afstand ertussen variëren, afhankelijk van parameters zoals het aantal iteraties, de stringlengte en zelfs de JDK-versie.

Met de nieuwste JDK 9+ (onze tests draaien op JDK 10) versies hebben beide implementaties redelijk gelijke resultaten. Laten we nu de JDK-versie downgraden naar 8 en de tests opnieuw:

Benchmark Mode Cnt Score Error Units benchmarkStringReplace ss 10 48.061 ± 17.157 ms / op benchmarkStringUtilsReplace ss 10 14.478 ± 5.752 ms / op

Het prestatieverschil is nu enorm en bevestigt de theorie die we in het begin bespraken.

3.2. splitsen ()

Voordat we beginnen, is het handig om de methoden voor het splitsen van strings in Java te bekijken.

Wanneer het nodig is om een ​​string te splitsen met het scheidingsteken, is de eerste functie die in ons opkomt meestal String.split (regex). Het brengt echter enkele ernstige prestatieproblemen met zich mee, omdat het een regex-argument accepteert. Als alternatief kunnen we de StringTokenizer class om de string in tokens te breken.

Een andere optie is Guava's Splitser API. Eindelijk, de goede oude index van() is ook beschikbaar om de prestaties van onze applicatie te verbeteren als we de functionaliteit van reguliere expressies niet nodig hebben.

Nu is het tijd om de benchmarktests voor te schrijven String.split () keuze:

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

Pattern.split () :

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

StringTokenizer :

Lijst stringTokenizer = nieuwe ArrayList (); @Benchmark openbare lijst benchmarkStringTokenizer () {StringTokenizer st = nieuwe StringTokenizer (longString); while (st.hasMoreTokens ()) {stringTokenizer.add (st.nextToken ()); } return stringTokenizer; }

String.indexOf () :

Lijst stringSplit = nieuwe ArrayList (); @Benchmark openbare lijst benchmarkStringIndexOf () {int pos = 0, end; while ((end = longString.indexOf ('', pos))> = 0) {stringSplit.add (longString.substring (pos, end)); pos = einde + 1; } return stringSplit; }

Guava's Splitser :

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

Ten slotte voeren we resultaten uit en vergelijken we de resultaten voor batchSize = 100.000:

Benchmark Mode 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 benchmark 0,448String 10 op

Zoals we zien, heeft de slechtste prestatie de benchmarkStringSplitPattern methode, waarbij we de Patroon klasse. Als gevolg hiervan kunnen we leren dat het gebruik van een regex-klasse met de extensie splitsen () methode kan meerdere keren prestatieverlies veroorzaken.

Hetzelfde, we merken dat de snelste resultaten voorbeelden zijn met het gebruik van indexOf () en split ().

3.3. Converteren naar Draad

In deze sectie gaan we de runtime-scores van stringconversie meten. Om specifieker te zijn, zullen we onderzoeken Integer.toString () aaneenschakelingsmethode:

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

String.valueOf () :

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

[een geheel getal] + "" :

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

String.format () :

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

Na het uitvoeren van de tests zien we de uitvoer voor batchSize = 10.000:

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

Na analyse van de resultaten zien we dat de test voor Integer.toString () heeft de beste score van 0.953 milliseconden. Een conversie daarentegen waarbij String.format ("% d") heeft de slechtste prestatie.

Dat is logisch omdat het formaat wordt geparseerd Draad is een dure operatie.

3.4. Strings vergelijken

Laten we eens kijken naar verschillende manieren om te vergelijken Snaren. Het aantal iteraties is 100,000.

Hier zijn onze benchmarktests voor het String.equals () operatie:

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

String.equalsIgnoreCase () :

@Benchmark openbare boolean benchmarkStringEqualsIgnoreCase () {retourneer longString.equalsIgnoreCase (baeldung); }

String.matches () :

@Benchmark openbare boolean benchmarkStringMatches () {retourneer longString.matches (baeldung); } 

String.compareTo () :

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

Daarna voeren we de tests uit en geven de resultaten weer:

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

Zoals altijd spreken de cijfers voor zich. De wedstrijden() duurt het langst omdat het de regex gebruikt om de gelijkheid te vergelijken.

In tegenstelling tot, de is gelijk aan () en is gelijk aanIgnoreCase() zijn de beste keuzes.

3.5. String.matches () vs Voorgecompileerd patroon

Laten we nu eens apart kijken String.matches () en Matcher.matches () patronen. De eerste neemt een regexp als argument en compileert deze voordat hij wordt uitgevoerd.

Dus elke keer dat we bellen String.matches (), het compileert het Patroon:

@Benchmark openbare boolean benchmarkStringMatches () {retourneer longString.matches (baeldung); }

De tweede methode hergebruikt het Patroon voorwerp:

Pattern longPattern = Pattern.compile (longString); @Benchmark openbare booleaanse benchmarkPrecompiledMatches () {retourneer longPattern.matcher (baeldung) .matches (); }

En nu de resultaten:

Benchmark-modus Cnt Score Error Units benchmarkPrecompiledMatches ss 10 29.594 ± 12.784 ms / op benchmarkStringMatches ss 10 106.821 ± 46.963 ms / op

Zoals we zien, werkt het matchen met voorgecompileerde regexp ongeveer drie keer zo snel.

3.6. De lengte controleren

Laten we tot slot de String.isEmpty () methode:

@Benchmark openbare booleaanse benchmarkStringIsEmpty () {retourneer longString.isEmpty (); }

en de Draadlengte() methode:

@Benchmark openbare booleaanse benchmarkStringLengthZero () {return emptyString.length () == 0; }

Ten eerste noemen we ze via de longString = "Hallo baeldung, ik ben gemiddeld iets langer dan andere strings" String. De seriegrootte is 10,000:

Benchmarkmodus Cnt Score Fout Eenheden benchmarkStringIsEmpty ss 10 0.295 ± 0.277 ms / op benchmarkStringLengthZero ss 10 0.472 ± 0.840 ms / op

Laten we daarna het longString = "" lege string en voer de tests opnieuw uit:

Benchmarkmodus Cnt Score Fout Eenheden benchmarkStringIsEmpty ss 10 0.245 ± 0.362 ms / op benchmarkStringLengthZero ss 10 0.351 ± 0.473 ms / op

Zoals we opmerken, benchmarkStringLengthZero () en benchmarkStringIsEmpty () methoden hebben in beide gevallen ongeveer dezelfde score. Echter, bellen is leeg() werkt sneller dan controleren of de lengte van de string nul is.

4. Stringontdubbeling

Sinds JDK 8 is de functie voor string-ontdubbeling beschikbaar om geheugengebruik te elimineren. Simpel gezegd, deze tool zoekt naar de strings met dezelfde of dubbele inhoud om een ​​kopie van elke afzonderlijke stringwaarde op te slaan in de String-pool.

Momenteel zijn er twee manieren om ermee om te gaan Draad duplicaten:

  • de ... gebruiken String.intern () handmatig
  • waardoor string-ontdubbeling mogelijk is

Laten we elke optie eens nader bekijken.

4.1. String.intern ()

Voordat we verder gaan, is het handig om in ons artikel over handmatige stage te lezen. Met String.intern () we kunnen handmatig de referentie van de Draad object binnen het globale Draad zwembad.

Vervolgens kan JVM indien nodig de referentie retourneren. Vanuit het oogpunt van prestaties kan onze applicatie enorm profiteren door de stringreferenties uit de constante pool te hergebruiken.

Belangrijk om te weten, dat JVM Draad pool is niet lokaal voor de thread. Elk Draad die we aan de pool toevoegen, is ook beschikbaar voor andere threads.

Er zijn echter ook ernstige nadelen:

  • om onze applicatie goed te onderhouden, moeten we mogelijk een -XX: StringTableSize JVM-parameter om de grootte van het zwembad te vergroten. JVM heeft een herstart nodig om de poolgrootte uit te breiden
  • roeping String.intern () handmatig is tijdrovend. Het groeit in een lineair tijdalgoritme met Aan) complexiteit
  • bovendien, frequente oproepen lang Draad objecten kunnen geheugenproblemen veroorzaken

Om enkele bewezen cijfers te hebben, laten we een benchmarktest uitvoeren:

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

Bovendien zijn de uitvoerscores in milliseconden:

Benchmark 1000 10.000 100.000 1.000.000 benchmarkStringIntern 0,433 2,243 19,996 204,373

De kolomkoppen vertegenwoordigen hier een andere iteraties telt vanaf 1000 naar 1,000,000. Voor elk iteratiegetal hebben we de testprestatiescore. Zoals we merken, stijgt de score dramatisch naast het aantal iteraties.

4.2. Schakel ontdubbeling automatisch in

Allereerst, deze optie is een onderdeel van de G1 garbage collector. Deze functie is standaard uitgeschakeld. We moeten het dus inschakelen met het volgende commando:

 -XX: + UseG1GC -XX: + UseStringDeduplication

Belangrijk om op te merken, dat het inschakelen van deze optie garandeert dat niet Draad ontdubbeling zal plaatsvinden. Ook verwerkt het geen jongen Snaren. Om de minimale leeftijd van verwerking te beheren Strings, XX: StringDeduplicationAgeThreshold = 3 JVM-optie is beschikbaar. Hier, 3 is de standaardparameter.

5. Samenvatting

In deze tutorial proberen we enkele hints te geven om strings efficiënter te gebruiken in ons dagelijkse codeerleven.

Als resultaat, we kunnen enkele suggesties naar voren halen om de prestaties van onze applicaties te verbeteren:

  • bij het aaneenschakelen van tekenreeksen, de StringBuilder is de handigste optie dat in je opkomt. Echter, met de kleine snaren, de + operatie heeft bijna dezelfde prestaties. Onder de motorkap kan de Java-compiler de StringBuilder class om het aantal string-objecten te verminderen
  • om de waarde om te zetten in de tekenreeks, de [een type] .toString () (Integer.toString () bijvoorbeeld) werkt dan sneller String.valueOf (). Omdat dat verschil niet significant is, kunnen we er vrij gebruik van maken String.valueOf () om geen afhankelijkheid te hebben van het invoerwaardetype
  • als het gaat om het vergelijken van snaren, gaat er niets boven de String.equals () dusver
  • Draad deduplicatie verbetert de prestaties in grote applicaties met meerdere threads. Maar overmatig gebruik String.intern () kan ernstige geheugenlekken veroorzaken, waardoor de toepassing wordt vertraagd
  • voor het splitsen van de snaren die we moeten gebruiken index van() om te winnen in prestaties. In sommige niet-kritieke gevallen String.split () functie zou een goede match kunnen zijn
  • Gebruik makend van Pattern.match () de snaar verbetert de prestaties aanzienlijk
  • String.isEmpty () is sneller dan String.length () == 0

Ook, Houd er rekening mee dat de cijfers die we hier presenteren slechts JMH-benchmarkresultaten zijn - dus u moet altijd testen in het bereik van uw eigen systeem en runtime om de impact van dit soort optimalisaties te bepalen.

Eindelijk, zoals altijd, is de code die tijdens de discussie is gebruikt, te vinden op GitHub.