Vonós előadás tippek

1. Bemutatkozás

Ebben az oktatóanyagban a Java String API teljesítmény szempontjára fogunk összpontosítani.

Majd beleásunk Húr létrehozási, átalakítási és módosítási műveletek az elérhető opciók elemzéséhez és hatékonyságuk összehasonlításához.

Az általunk megfogalmazott javaslatok nem feltétlenül lesznek megfelelőek minden alkalmazáshoz. De minden bizonnyal megmutatjuk, hogyan nyerhetünk teljesítményt, amikor az alkalmazás futási ideje kritikus.

2. Új húr létrehozása

Mint tudják, a Java-ban a karakterláncok megváltoztathatatlanok. Tehát minden alkalommal, amikor elkészítjük vagy összefűzzük a Húr objektumot, a Java létrehoz egy újat Húr - ez különösen költséges lehet, ha hurokban végzik.

2.1. A Constructor használata

A legtöbb esetben, kerülnünk kell a teremtést Húrok a konstruktort használva, hacsak nem tudjuk, mit csinálunk.

Hozzunk létre egy newString először a hurok belsejében lévő objektum, a új karakterlánc () kivitelező, majd a = operátor.

A referenciaértékünk megírásához a JMH (Java Microbenchmark Harness) eszközt fogjuk használni.

Konfigurációnk:

@BenchmarkMode (Mode.SingleShotTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) @Measurement (batchSize = 10000, iterációk = 10) @Warmup (batchSize = 10000, iterációk = 10) public class StringPerformance {}

Itt használjuk a SingeShotTime mód, amely csak egyszer futtatja a módszert. Ahogy meg akarjuk mérni a Húr műveletek a hurok belsejében, van egy @Mérés ehhez rendelkezésre álló feljegyzés.

Fontos tudni, hogy A tesztjeinkben szereplő benchmarking ciklusok torzíthatják az eredményeket a JVM által alkalmazott különféle optimalizálások miatt.

Tehát csak az egyetlen műveletet számoljuk ki, és hagyjuk, hogy a JMH vigyázzon a hurkolásra. Röviden: a JMH az iterációt az csomó méret paraméter.

Most tegyük hozzá az első mikro-benchmarkot:

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

Az első tesztben minden iterációban új objektum jön létre. A második tesztben az objektum csak egyszer jön létre. A fennmaradó iterációk esetén ugyanazt az objektumot adja vissza a Húrok állandó medence.

Futtassuk a teszteket a ciklusos iterációk számlálásával = 1,000,000 és nézze meg az eredményeket:

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

Tól Pontszám értékeket, egyértelműen láthatjuk, hogy a különbség jelentős.

2.2. + Operátor

Vessünk egy pillantást a dinamikára Húr összefűzési példa:

@State (Scope.Thread) public static class StringPerformanceHints {String result = ""; Karakterlánc baeldung = "baeldung"; } @Benchmark public String benchmarkStringDynamicConcat () {return result + baeldung; } 

Eredményeinkben szeretnénk látni az átlagos végrehajtási időt. A kimeneti szám formátuma ezredmásodpercekre van beállítva:

1000-es referenciaérték 10 000 benchmarkStringDynamicConcat 47,331 4370,411

Most elemezzük az eredményeket. Mint látjuk, hozzátéve 1000 elemeket állapot.eredmény veszi 47.331 ezredmásodpercig. Következésképpen, az iterációk számának tízszeres növelésével a futási idő megnő 4370.441 ezredmásodpercig.

Összefoglalva: a kivégzés ideje kvadratikusan növekszik. Ezért a dinamikus összefűzés bonyolultsága egy n iterációs ciklusban O (n ^ 2).

2.3. String.concat ()

Az összefűzésnek még egy módja Húrok a concat () módszer:

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

A kimeneti időegység ezredmásodperc, az iterációk száma 100 000. Az eredménytábla a következőképpen néz ki:

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

2.4. String.format ()

A karakterláncok létrehozásának másik módja a használat String.format () módszer. A motorháztető alatt szabályos kifejezéseket használ a bemenet elemzésére.

Írjuk meg a JMH tesztesetet:

String formatString = "szia% s, örülök, hogy találkoztunk"; @Benchmark public String benchmarkStringFormat_s () {return String.format (formatString, baeldung); }

Ezután futtatjuk és megnézzük az eredményeket:

Iterációk száma 10 000 100 000 1 000 000 benchmark StringFormat_s 17,181 140,456 1636,279 ms / op

Bár a kód a String.format () tisztábbnak és olvashatóbbnak tűnik, a teljesítmény szempontjából itt nem nyerünk.

2.5. StringBuilder és StringBuffer

Már van egy írásunk, amely elmagyarázza StringBuffer és StringBuilder. Tehát itt csak további információkat mutatunk be a teljesítményükről. StringBuilder átméretezhető tömböt és egy indexet használ, amely jelzi a tömbben utoljára használt cella helyzetét. Ha a tömb megtelt, akkor megduplázza a méretét, és az összes karaktert átmásolja az új tömbbe.

Figyelembe véve, hogy az átméretezés nem gyakran fordul elő, mindegyiket figyelembe vehetjük mellékel() művelet mint O (1) állandó idő. Ezt figyelembe véve az egész folyamat megtörtént Tovább) bonyolultság.

A dinamikus összefűző teszt módosítása és futtatása után StringBuffer és StringBuilder, kapunk:

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

Bár a pontszámkülönbség nem sok, észrevehetjük hogy StringBuilder gyorsabban működik.

Szerencsére egyszerű esetekben nincs rá szükségünk StringBuilder hogy tegyen egyet Húr másikkal. Néha, a + -al történő statikus összefűzés valójában helyettesítheti StringBuilder. A burkolat alatt a legújabb Java fordítók meghívják a StringBuilder.append () összefűzni a húrokat.

Ez azt jelenti, hogy jelentősen nyerni kell a teljesítményben.

3. Segédprogram műveletek

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

Érdekes tudni, hogy Apache Commons verzió a Húr jobban megy, mint a húr sajátja cserélje () módszer. A válasz erre a különbségre a megvalósításuk alatt áll. String.replace () regex mintát használ a Húr.

Ellentétben, StringUtils.replace () széles körben használja indexe(), ami gyorsabb.

Itt az ideje a benchmark teszteknek:

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

A csomó méret 100 000-ig, bemutatjuk az eredményeket:

Benchmark Mode Cnt Score Hibaegységek benchmarkStringReplace ss 10 6.233 ± 2.922 ms / op benchmarkStringUtilsReplace ss 10 5.355 ± 2.497 ms / op

Bár a számok közötti különbség nem túl nagy, a StringUtils.replace () jobb pontszáma van. Természetesen a számok és a köztük lévő különbség az olyan paraméterektől függően változhat, mint az iterációk száma, a karakterlánc hossza és akár a JDK verziója is.

A legújabb JDK 9+ verzióval (tesztjeink a JDK 10-n futnak) mindkét verzióval meglehetősen egyenlő eredmények vannak. Most tegyük vissza a JDK verzióját 8-ra és a teszteket újra:

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

A teljesítménybeli különbség most hatalmas, és megerősíti azt az elméletet, amelyet az elején tárgyaltunk.

3.2. hasított()

Mielőtt elkezdenénk, hasznos lesz megnézni a Java-ban elérhető sztringfelosztási módszereket.

Ha szükség van egy karakterlánc szétválasztására a határolóval, akkor általában az első funkció jut eszünkbe String.split (regex). Ez azonban komoly teljesítményproblémákat vet fel, mivel elfogad egy regex érvet. Alternatív megoldásként használhatjuk a StringTokenizer osztályban, hogy a húrokat tokenekre bontsa.

Egy másik lehetőség a Guava's Hasító API. Végül a jó öreg indexe() elérhető az alkalmazásunk teljesítményének növelésére is, ha nincs szükségünk a reguláris kifejezések funkcionalitására.

Itt az ideje megírni a benchmark teszteket String.split () választási lehetőség:

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

Pattern.split () :

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

StringTokenizer :

Lista stringTokenizer = new ArrayList (); @Benchmark public List benchmarkStringTokenizer () {StringTokenizer st = új StringTokenizer (longString); while (st.hasMoreTokens ()) {stringTokenizer.add (st.nextToken ()); } return stringTokenizer; }

String.indexOf () :

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

Guava's Hasító :

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

Végül lefuttatjuk és összehasonlítjuk a következő eredményeket: batchSize = 100 000:

Benchmark Mode Cnt Score Hibaegységek 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 / opstand op

Mint látjuk, a legrosszabb teljesítmény az benchmarkStringSplitPattern módszer, ahol a Minta osztály. Ennek eredményeként megtanulhatjuk, hogy egy regex osztály használata a hasított() módszer többször is teljesítményvesztést okozhat.

Hasonlóképpen, észrevesszük, hogy a leggyorsabb eredmények példákat mutatnak a indexOf () és split ().

3.3. Konvertálás erre: Húr

Ebben a szakaszban meg fogjuk mérni a karakterlánc-átalakítás futási idejét. Pontosabban: megvizsgáljuk Integer.toString () összefűzési módszer:

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

String.valueOf () :

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

[valamilyen egész érték] + “” :

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

String.format () :

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

A tesztek futtatása után meglátjuk a batchSize = 10 000:

Benchmark Mode Cnt Score Hibaegységek 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,15

Az eredmények elemzése után azt látjuk a teszt Integer.toString () a legjobb pontszáma 0.953 ezredmásodpercig. Ezzel szemben egy átalakítás, amely magában foglalja String.format („% d”) a legrosszabb teljesítményt nyújtja.

Ez logikus, mert a formátum elemzése Húr drága művelet.

3.4. Húrok összehasonlítása

Értékeljük az összehasonlítás különböző módjait Húrok. Az iterációk száma 100,000.

Itt vannak a benchmark tesztjeink a String.equals () művelet:

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

String.equalsIgnoreCase () :

@Benchmark nyilvános logikai 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); }

Ezután lefuttatjuk a teszteket és megjelenítjük az eredményeket:

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

Mint mindig, a számok önmagukért beszélnek. A mérkőzések () a leghosszabb ideig tart, mivel a regexet használja az egyenlőség összehasonlítására.

Ellentétben, a egyenlő () és equalsIgnoreCase() a legjobb választás.

3.5. String.matches () vs. Előfordított minta

Most nézzük meg külön a pillantást String.matches () és Matcher.matches () minták. Az első a regexp-et veszi argumentumnak, és a végrehajtás előtt összeállítja.

Tehát minden alkalommal, amikor hívunk String.matches (), összeállítja a Minta:

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

A második módszer újrafelhasználja a Minta tárgy:

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

És most az eredmények:

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

Mint látjuk, az előre lefordított regexp-hez való megfelelés körülbelül háromszor gyorsabb.

3.6. A hossz ellenőrzése

Végül hasonlítsuk össze a String.isEmpty () módszer:

@Benchmark nyilvános logikai benchmarkStringIsEmpty () {return longString.isEmpty (); }

és a String.length () módszer:

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

Először hívjuk őket a longString = “Hello baeldung, kicsit hosszabb vagyok, mint a többi Strings átlagban” String. A csomó méret van 10,000:

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

Ezután állítsuk be a longString = “” üres karakterláncot, és futtassa újra a teszteket:

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

Ahogy észrevesszük, benchmarkStringLengthZero () és benchmarkStringIsEmpty () módszerek mindkét esetben megközelítőleg azonos pontszámmal rendelkeznek. Hívás azonban üres() gyorsabban működik, mint annak ellenőrzése, hogy a karakterlánc hossza nulla-e.

4. Húr deduplikáció

A JDK 8 óta a karakterlánc deduplikáció funkció elérhető a memóriafelhasználás kiküszöbölésére. Egyszerűen fogalmazva, ez az eszköz ugyanazon vagy duplikált tartalommal rendelkező karakterláncokat keresi, hogy minden egyes karakterlánc-érték egy példányát tárolja a karakterlánc-készletben.

Jelenleg kétféle módon lehet kezelni Húr másolatok:

  • használni a String.intern () manuálisan
  • karakterlánc deduplikáció engedélyezése

Nézzük meg közelebbről az egyes lehetőségeket.

4.1. String.intern ()

Mielőtt előre lépnénk, hasznos lesz elolvasnunk a kézi internálást az írásunkban. Val vel String.intern () manuálisan beállíthatjuk a referenciát Húr objektum a globális belsejében Húr medence.

Ezután a JVM szükség esetén visszaadhatja a referenciát. A teljesítmény szempontjából az alkalmazásunk hatalmas hasznot húzhat azáltal, hogy újból felhasználja a string-referenciákat az állandó készletből.

Fontos tudni, hogy JVM Húr a pool nem helyi a szálhoz. Minden egyes Húr amit hozzáadunk a készlethez, elérhető más szálak számára is.

Vannak azonban komoly hátrányai is:

  • alkalmazásunk megfelelő fenntartásához szükség lehet a -XX: StringTableSize JVM paraméter a készlet méretének növeléséhez. A JVM újraindítására van szükség a medence méretének bővítéséhez
  • hívás String.intern () manuálisan időigényes. Lineáris idő algoritmusban növekszik Tovább) bonyolultság
  • ezenkívül gyakori hívások hosszú ideig Húr az objektumok memóriaproblémákat okozhatnak

Néhány bizonyított számhoz futtassunk egy benchmark tesztet:

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

Ezenkívül a kimeneti pontszám milliszekundumban van megadva:

Benchmark 1000 10 000 100 000 1 000 000 benchmark StringIntern 0,433 2,243 19,996 204,373

Az oszlopfejlécek itt mást jelentenek iterációk számít 1000 nak nek 1,000,000. Minden iterációs számhoz megvan a teszt teljesítmény pontszáma. Amint észrevesszük, az iterációk száma mellett a pontszám drámaian növekszik.

4.2. A deduplikáció automatikus engedélyezése

Először is, ez az opció a G1 szemétgyűjtő része. Alapértelmezés szerint ez a funkció le van tiltva. Tehát a következő paranccsal kell engedélyeznünk:

 -XX: + UseG1GC -XX: + UseStringDeduplication

Fontos megjegyezni, hogy ennek az opciónak az engedélyezése nem garantálja Húr deduplikáció megtörténik. Ezenkívül nem dolgozza fel fiatalon Húrok. A feldolgozás minimális életkorának kezelése érdekében Strings, XX: StringDeduplicationAgeThreshold = 3 JVM opció elérhető. Itt, 3 az alapértelmezett paraméter.

5. Összefoglalás

Ebben az oktatóanyagban megpróbálunk néhány tippet adni a karakterláncok hatékonyabb használatára a mindennapi kódolás során.

Ennek eredményeként kiemelhetünk néhány javaslatot az alkalmazás teljesítményének növelése érdekében:

  • a húrok összefűzésénél a StringBuilder a legkényelmesebb lehetőség az jut eszembe. A kis húrokkal azonban a + művelet majdnem ugyanolyan teljesítményű. A burkolat alatt a Java fordító a StringBuilder osztály a karakterlánc objektumok számának csökkentése érdekében
  • az érték karakterlánccá alakításához az [valamilyen típus] .toString () (Integer.toString () például) akkor gyorsabban működik String.valueOf (). Mivel ez a különbség nem jelentős, szabadon felhasználhatjuk String.valueOf () hogy ne függjen a bemeneti érték típusától
  • amikor a húr összehasonlításról van szó, semmi sem veri a String.equals () eddig
  • Húr a deduplikáció javítja a teljesítményt nagy, több szálon futó alkalmazásokban. De túlzottan String.intern () súlyos memóriaszivárgást okozhat, ami lelassíthatja az alkalmazást
  • a húrok felosztásához használnunk kell indexe() nyerni a teljesítményben. Egyes nem kritikus esetekben azonban String.split () a funkció jó lehet
  • Használata Pattern.match () a húr jelentősen javítja a teljesítményt
  • String.isEmpty () gyorsabb, mint a String.hossz () == 0

Is, ne feledje, hogy az itt bemutatott számok csak a JMH benchmark eredményei - ezért mindig tesztelnie kell saját rendszere és futási ideje alatt, hogy meghatározza az ilyen típusú optimalizálások hatását.

Végül, mint mindig, a vita során használt kód megtalálható a GitHubon.