רמזים לביצועי מיתרים

1. הקדמה

במדריך זה, אנו נתמקד בהיבט הביצועים של ממשק ה- API של Java String.

אנחנו נתעמק חוּט פעולות יצירה, המרה ושינוי לניתוח האפשרויות הזמינות והשוואת יעילותן.

ההצעות שאנו מציעים לא בהכרח יתאימו לכל יישום. אבל בהחלט, אנו הולכים להראות כיצד לזכות בביצועים כאשר זמן הריצה של היישום הוא קריטי.

2. בניית מחרוזת חדשה

כידוע, ב- Java, מיתרים הם בלתי ניתנים לשינוי. אז בכל פעם שאנחנו בונים או משתרכים א חוּט אובייקט, Java יוצר חדש מחרוזת - זה עשוי להיות יקר במיוחד אם נעשה בלולאה.

2.1. באמצעות קונסטרוקטור

ברוב המקרים, עלינו להימנע מיצירה מיתרים באמצעות הקונסטרוקטור אלא אם כן אנו יודעים מה אנו עושים.

בואו ניצור newString חפץ בתוך הלולאה תחילה, באמצעות מחרוזת חדשה () בנאי, ואז ה = מַפעִיל.

כדי לכתוב את אמת המידה שלנו, נשתמש בכלי JMH (Java Microbenchmark Harness).

התצורה שלנו:

@BenchmarkMode (Mode.SingleShotTime) @OutputTimeUnit (TimeUnit.MILLISECONDS) @Measurement (batchSize = 10000, iterations = 10) @Warmup (batchSize = 10000, iterations = 10) StringPerformance בכיתה ציבורית {}

הנה, אנו משתמשים ב- SingeShotTime מצב, שמפעיל את השיטה פעם אחת בלבד. כפי שאנחנו רוצים למדוד את הביצועים של חוּט פעולות בתוך הלולאה, יש @מדידה ביאור זמין לכך.

חשוב לדעת, את זה לולאות מידוד ישירות במבחנים שלנו עשויים להטות את התוצאות בגלל אופטימיזציות שונות שהופעלו על ידי JVM.

אז אנו מחשבים רק את הפעולה היחידה ונותנים ל- JMH לדאוג לולאה. בקצרה, JMH מבצע את האיטרציות באמצעות batchSize פָּרָמֶטֶר.

עכשיו, בואו נוסיף את המיקרו-מידוד הראשון:

@Benchmark ציבורי מחרוזת benchmarkStringConstructor () {להחזיר מחרוזת חדשה ("baeldung"); } @ Benchmark public benchmarkStringLiteral () {להחזיר "baeldung"; }

במבחן הראשון נוצר אובייקט חדש בכל איטרציה. במבחן השני, האובייקט נוצר רק פעם אחת. עבור איטרציות שנותרו, אותו אובייקט מוחזר מה- מחרוזת בריכה קבועה.

בואו נפעיל את הבדיקות עם ספירת איטרציות הלולאה = 1,000,000 וראה את התוצאות:

מידת מידוד שגיאת ציון ציון יחידות מידוד מחרוזת בנאי ss 10 16.089 ± 3.355 ms / op benchmark מחרוזת ספרותית 10 9.523 ± 3.331 ms / op

מ ה ציון ערכים, אנו יכולים לראות בבירור שההבדל הוא משמעותי.

2.2. + מפעיל

בואו נסתכל על דינמיקה חוּט דוגמה לשרשור:

@State (Scope.Thread) מחלקה סטטית ציבורית StringPerformanceHints {String result = ""; מחרוזת baeldung = "baeldung"; } @Benchmark ציבורי מחרוזת benchmarkStringDynamicConcat () {החזרת תוצאה + baeldung; } 

בתוצאות שלנו אנו רוצים לראות את זמן הביצוע הממוצע. פורמט מספר הפלט מוגדר לאלפיות השנייה:

מדד 1000 10,000 benchmarkStringDynamicConcat 47.331 4370.411

עכשיו, בואו ננתח את התוצאות. כפי שאנו רואים, להוסיף 1000 פריטים ל state.result לוקח 47.331 אלפיות השנייה. כתוצאה מכך, הגדלת מספר החזרות פי 10, זמן הריצה גדל ל 4370.441 אלפיות השנייה.

לסיכום, זמן הביצוע גדל באופן ריבועי. לכן, המורכבות של שרשור דינמי בלולאה של n איטרציות היא O (n ^ 2).

2.3. String.concat ()

דרך נוספת לשרשור מיתרים הוא באמצעות concat () שיטה:

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

יחידת זמן הפלט היא אלפית השנייה, ספירת איטרציות היא 100,000. טבלת התוצאות נראית כך:

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

2.4. String.format ()

דרך נוספת ליצור מחרוזות היא באמצעות String.format () שיטה. מתחת למכסה המנוע, הוא משתמש בביטויים רגולריים כדי לנתח את הקלט.

בואו נכתוב את מקרה הבדיקה JMH:

מחרוזת FormatString = "שלום% s, נעים להכיר"; @Benchmark ציבורי מחרוזת benchmarkStringFormat_s () {להחזיר String.format (formatString, baeldung); }

לאחר מכן אנו מריצים אותו ורואים את התוצאות:

מספר איטרציות 10,000 100,000 1,000,000 benchmark StringFormat_s 17.181 140.456 1636.279 ms / op

למרות שהקוד עם String.format () נראה נקי וקריא יותר, אנחנו לא מנצחים כאן מבחינת ביצועים.

2.5. StringBuilder ו StringBuffer

יש לנו כבר כתב מסביר StringBuffer ו StringBuilder. אז הנה, אנו נציג רק מידע נוסף על ביצועיהם. StringBuilder משתמש במערך הניתן לשינוי גודל ובאינדקס המציין את מיקום התא האחרון ששימש במערך. כאשר המערך מלא, הוא מתרחב כפול מגודלו ומעתיק את כל התווים למערך החדש.

אם ניקח בחשבון ששינוי הגודל לא מתרחש לעתים קרובות מאוד, אנחנו יכולים להתחשב בכל אחד מהם לְצַרֵף() פעולה כמו O (1) זמן קבוע. אם לוקחים זאת בחשבון, כל התהליך קיים עַל) מוּרכָּבוּת.

לאחר שינוי והפעלת מבחן השרשור הדינמי עבור StringBuffer ו StringBuilder, אנחנו מקבלים:

מידת שוויון ציון Cnt שגיאה יחידות מידוד StringBuffer ss 10 1.409 ± 1.665 ms / op benchmark StringBuilder ss 1.200 ± 0.648 ms / op

למרות שהפרש הציונים אינו גדול, אנו יכולים להבחין בכך זֶה StringBuilder עובד מהר יותר.

למרבה המזל, במקרים פשוטים, אנחנו לא צריכים StringBuilder לשים אחד חוּט עם אחר. לִפְעָמִים, שרשור סטטי עם + יכול למעשה להחליף StringBuilder. מתחת למכסה המנוע, מהדרי Java האחרונים יקראו StringBuilder.append () לשרשור מיתרים.

המשמעות היא זכייה בביצועים באופן משמעותי.

3. פעולות שירות

3.1. StringUtils.replace () לעומת String.replace ()

מעניין לדעת, את זה גרסת Apache Commons להחלפת חוּט עושה טוב יותר משל מחרוזת עצמו החלף() שיטה. התשובה להבדל זה טמונה ביישומם. String.replace () משתמש בתבנית regex כדי להתאים את חוּט.

בניגוד, StringUtils.replace () נמצא בשימוש נרחב אינדקס של(), שהוא מהיר יותר.

עכשיו הגיע הזמן למבחני אמת המידה:

@Benchmark ציבורי מחרוזת benchmarkStringReplace () {return longString.replace ("ממוצע", ​​"ממוצע !!!"); } @ Benchmark ציבורי מחרוזת benchmarkStringUtilsReplace () {החזר StringUtils.replace (longString, "ממוצע", ​​"ממוצע !!!"); }

הגדרת ה- batchSize עד 100,000, אנו מציגים את התוצאות:

מצב מידוד Cnt ציון שגיאה יחידות מידוד StringRlace ss 10 6.233 ± 2.922 ms / op benchmark StringUtils החלף ss 10 5.355 ± 2.497 ms / op

למרות שההבדל בין המספרים אינו גדול מדי, StringUtils.replace () יש ציון טוב יותר. כמובן, המספרים והפער ביניהם עשויים להשתנות בהתאם לפרמטרים כמו ספירת איטרציות, אורך מחרוזת ואפילו גרסת JDK.

עם הגרסאות העדכניות ביותר של JDK 9+ (הבדיקות שלנו פועלות ב- JDK 10) גרסאות שתי היישומים תוצאות שוות למדי. עכשיו, בואו ונדרדר שוב את גרסת ה- JDK ל- 8 והבדיקות שוב:

מידת שוויון ציון Cnt שגיאת יחידות מידוד StringRlace ss 10 48.061 ± 17.157 ms / op benchmark StringUtils החלף ss 10 14.478 ± 5.752 ms / op

ההבדל בביצועים עצום כעת ומאשר את התיאוריה עליה דנו בתחילת הדרך.

3.2. לְפַצֵל()

לפני שנתחיל, יהיה שימושי לבדוק את שיטות פיצול המיתרים הקיימות ב- Java.

כשיש צורך לפצל מחרוזת עם המפריד, הפונקציה הראשונה שעולה בראשנו בדרך כלל היא String.split (regex). עם זאת, זה מביא כמה בעיות ביצועיות חמורות, שכן הוא מקבל טיעון regex. לחלופין, אנו יכולים להשתמש ב- StringTokenizer בכיתה לשבור את המחרוזת לאסימונים.

אפשרות נוספת היא של גויאבה ספליטר ממשק API. לבסוף, הישן והטוב אינדקס של() זמין גם כדי לשפר את ביצועי היישום שלנו אם איננו זקוקים לפונקציונליות של ביטויים רגולריים.

עכשיו הגיע הזמן לכתוב את מבחני הביצועים String.split () אוֹפְּצִיָה:

מחרוזת emptyString = ""; מחרוזת ציבורית @Benchmark [] benchmarkStringSplit () {return longString.split (emptyString); }

Pattern.split () :

מחרוזת ציבורית @Benchmark [] benchmarkStringSplitPattern () {return spacePattern.split (longString, 0); }

StringTokenizer :

רשימת stringTokenizer = ArrayList חדש (); @ רישום ציבורי @Benchmark benchmarkStringTokenizer () {StringTokenizer st = StringTokenizer חדש (longString); בעוד (st.hasMoreTokens ()) {stringTokenizer.add (st.nextToken ()); } להחזיר stringTokenizer; }

String.indexOf () :

רשימת stringSplit = ArrayList חדש (); @ רישום ציבורי @Benchmark benchmarkStringIndexOf () {int pos = 0, end; while ((end = longString.indexOf ('', pos))> = 0) {stringSplit.add (longString.substring (pos, end)); pos = סוף + 1; } להחזיר stringSplit; }

של גויאבה ספליטר :

@ Benchmark רשימת ציבורי benchmarkGuavaSplitter () {return Splitter.on ("") .trimResults () .omitEmptyStrings () .splitToList (longString); }

לבסוף, אנו רצים ומשווים תוצאות עבור batchSize = 100,000:

מדד מצב מידוד Cnt ציון שגיאה יחידות מידה GuavaSplitter ss 10 4.008 ± 1.836 ms / op benchmarkStringIndexOf ss 10 1.144 ± 0.322 ms / op benchmark StringSplit ss 10 1.983 ± 1.075 ms / op benchmark StringSplit דפוס ss 10 14.891 ± 5.678 ms / op benchmark סטרינג 2. ms אופ

כפי שאנו רואים, הביצועים הגרועים ביותר הם benchmarkStringSplitPattern שיטה, בה אנו משתמשים ב- תבנית מעמד. כתוצאה מכך, אנו יכולים ללמוד כי שימוש בכיתת regex עם ה- לְפַצֵל() השיטה עלולה לגרום לאובדן ביצועים פעמים רבות.

כְּמוֹ כֵן, אנו מבחינים שהתוצאות המהירות ביותר מספקות דוגמאות לשימוש ב- indexOf () ו split ().

3.3. ממיר ל חוּט

בחלק זה נמדוד את ציוני זמן הריצה של המרת מחרוזות. ליתר דיוק, נבדוק Integer.toString () שיטת שרשור:

int sampleNumber = 100; @Benchmark ציבורי מחרוזת benchmarkIntegerToString () {להחזיר Integer.toString (sampleNumber); }

String.valueOf () :

@Benchmark ציבורי מחרוזת benchmarkStringValueOf () {return String.valueOf (sampleNumber); }

[ערך שלם כלשהו] + "" :

@ Benchmark public benchmarkStringConvertPlus () {מחרוזת sampleNumber + ""; }

String.format () :

מחרוזת formatDigit = "% d"; @Benchmark ציבורי מחרוזת benchmarkStringFormat_d () {להחזיר String.format (formatDigit, sampleNumber); }

לאחר הפעלת הבדיקות נראה את הפלט עבור batchSize = 10,000:

מידת מדידה Cnt ציון שגיאה יחידות מידוד שלם ToString ss 10 0.953 ± 0.707 ms / op benchmark StringConvertPlus ss 10 1.464 ± 1.670 ms / op benchmark StringFormat_d ss 10 15.656 ± 8.896 ms / op benchmarkStringValueOf ss 10 2.847 ± 11.153 ms / op

לאחר ניתוח התוצאות אנו רואים זאת המבחן עבור Integer.toString () בעל הציון הטוב ביותר של 0.953 אלפיות השנייה. לעומת זאת, המרה הכוללת String.format ("% d") יש את הביצועים הגרועים ביותר.

זה הגיוני מכיוון שמנתח את הפורמט חוּט היא פעולה יקרה.

3.4. השוואת מיתרים

בואו נעריך דרכי השוואה שונות מיתרים. ספירת האיטרציות היא 100,000.

להלן מבחני המבחן שלנו עבור String.equals () מבצע:

@ Benchmark ציבורי בוליאני ציבורי benchmarkStringEquals () {return longString.equals (baeldung); }

String.equalsIgnoreCase () :

@Benchmark ציבורי בוליאני ציבורי benchmarkStringEqualsIgnoreCase () {return longString.equalsIgnoreCase (baeldung); }

String.matches () :

@ Benchmark ציבורי בוליאני ציבורי benchmarkStringMatches () {return longString.matches (baeldung); } 

String.compareTo () :

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

לאחר מכן אנו מבצעים את הבדיקות ומציגים את התוצאות:

מצב מידוד Cnt ציון שגיאה יחידות מדד מחרוזת השווה ל ss 10 2.561 ± 0.899 ms / op benchmark מחרוזת שווה ss 10 1.712 ± 0.839 ms / op benchmark מחרוזת שוויון התעלם מקרה ss 10 2.081 ± 1.221 ms / op benchmark מחרוזת ss 10 118.364 ± 43.203 ms / op

כמו תמיד, המספרים מדברים בעד עצמם. ה התאמות() לוקח הכי הרבה זמן שהוא משתמש ב- regex כדי להשוות את השוויון.

בניגוד, ה שווים() ושווה ל- IgnoreCase() הן הבחירות הטובות ביותר.

3.5. String.matches () לעומת תבנית מהודרת מראש

עכשיו, בואו נסתכל על נפרד String.matches () ו Matcher.matches () דפוסים. הראשון לוקח רגקס כארגומנט ומרכיב אותו לפני ביצועו.

אז בכל פעם שאנחנו מתקשרים String.matches (), זה מרכיב את תבנית:

@ Benchmark ציבורי בוליאני ציבורי benchmarkStringMatches () {return longString.matches (baeldung); }

השיטה השנייה עושה שימוש חוזר ב תבנית לְהִתְנַגֵד:

תבנית longPattern = Pattern.compile (longString); @ Benchmark ציבורי בוליאני ציבורי benchmarkPrecompiledMatches () {return longPattern.matcher (baeldung) .matches (); }

ועכשיו התוצאות:

מדד מצב מידוד Cnt ציון שגיאה מדד יחידות בידור מחדש התאמות ss 10 29.594 ± 12.784 ms / op benchmark StringMatches ss 10 106.821 ± 46.963 ms / op

כפי שאנו רואים, התאמה עם regexp שהורכב מראש עובדת פי שלוש מהר יותר.

3.6. בדיקת האורך

לבסוף, בואו נשווה את String.isEmpty () שיטה:

@Benchmark ציבורי בוליאני ציבורי benchmarkStringIsEmpty () {return longString.isEmpty (); }

וה אורך המחרוזת() שיטה:

@ Benchmark ציבורי בוליאני ציבורי StringLengthZero () {להחזיר emptyString.length () == 0; }

ראשית, אנו קוראים להם דרך longString = "שלום ביילדונג, אני קצת יותר ממחרוזות אחרות בממוצע". ה batchSize הוא 10,000:

מידת שוויון ציון Cnt ציון שגיאות יחידות benchmark StringIs ריק ss 10 0.295 ± 0.277 ms / op benchmark StringLengthZero ss 0.472 ± 0.840 ms / op

לאחר מכן, בואו נקבע את longString = “” מחרוזת ריקה והפעל שוב את הבדיקות:

מצב מידוד Cnt ציון שגיאה יחידות מידוד StringIs ריק ss 10 0.245 ± 0.362 ms / op benchmark StringLengthZero ss 10 0.351 ± 0.473 ms / op

כפי שאנו מבחינים, benchmarkStringLengthZero () ו benchmarkStringIsEmpty () לשיטות בשני המקרים ציון זהה בערך. עם זאת, מתקשר זה ריק() עובד מהר יותר מאשר לבדוק אם אורך המחרוזת הוא אפס.

4. כפילות מיתרים

מאז JDK 8, תכונת כפילות מיתרים זמינה כדי למנוע צריכת זיכרון. פשוט שים, כלי זה מחפש מחרוזות עם תוכן זהה או כפול לאחסון עותק אחד של כל ערך מחרוזת מובחן במאגר המיתרים.

נכון לעכשיו, ישנן שתי דרכים לטפל בהן חוּט כפילויות:

  • משתמש ב String.intern () באופן ידני
  • הפעלת הכפילת מחרוזת

בואו נסתכל מקרוב על כל אפשרות.

4.1. String.intern ()

לפני שתקפוץ קדימה, יהיה שימושי לקרוא על התמחות ידנית בכתיבה שלנו. עם String.intern () אנחנו יכולים להגדיר ידנית את ההפניה של חוּט אובייקט בתוך הגלובלי חוּט בריכה.

לאחר מכן, JVM יכול להשתמש בהחזרת ההפניה במידת הצורך. מנקודת מבט של ביצועים, היישום שלנו יכול להפיק תועלת רבה על ידי שימוש חוזר בהפניות המחרוזות מהמאגר התמידי.

חשוב לדעת, את זה JVM חוּט הבריכה אינה מקומית לשרשור. כל אחד חוּט שנוסיף לבריכה, זמין גם לשרשורים אחרים.

עם זאת, ישנם גם חסרונות חמורים:

  • כדי לשמור על היישום שלנו כראוי, ייתכן שנצטרך להגדיר א -XX: StringTableSize פרמטר JVM להגדלת גודל הבריכה. JVM זקוק להפעלה מחדש כדי להרחיב את גודל הבריכה
  • יִעוּד String.intern () באופן ידני זה גוזל זמן. הוא גדל באלגוריתם זמן ליניארי עם עַל) מוּרכָּבוּת
  • בנוסף, שיחות תכופות בשיחות ארוכות חוּט אובייקטים עלולים לגרום לבעיות זיכרון

כדי לקבל מספרים מוכחים, בואו נבצע בדיקת אמת מידה:

@Benchmark ציבורי מחרוזת benchmarkStringIntern () {להחזיר baeldung.intern (); }

בנוסף, ציוני הפלט הם באלפיות השנייה:

מדד 1000 10,000 100,000 100,000 1,000,000 benchmarkStringIntern 0.433 2.243 19.996 204.373

כותרות העמודות כאן מייצגות שונה איטרציות סופר מ 1000 ל 1,000,000. עבור כל מספר איטרציה, יש לנו את ציון ביצועי המבחן. כפי שאנו מבחינים, הציון עולה באופן דרמטי בנוסף למספר האיטרציות.

4.2. אפשר כפילות באופן אוטומטי

ראשית כל, אפשרות זו היא חלק מאוסף האשפה G1. כברירת מחדל, תכונה זו מושבתת. אז עלינו לאפשר זאת באמצעות הפקודה הבאה:

 -XX: + UseG1GC -XX: + UseStringDeduplication

חשוב לציין, כי הפעלת אפשרות זו אינה מבטיחה זאת חוּט כפילות תתרחש. כמו כן, זה לא מעבד צעירים מיתרים. על מנת לנהל את גיל העיבוד המינימלי מיתרים, XX: StringDeduplicationAgeThreshold = 3 אפשרות JVM זמינה. פה, 3 הוא פרמטר ברירת המחדל.

5. סיכום

במדריך זה אנו מנסים לתת כמה רמזים לשימוש במחרוזות בצורה יעילה יותר בחיי הקידוד היומיומיים שלנו.

כתוצאה, אנו יכולים להדגיש כמה הצעות על מנת לשפר את ביצועי היישומים שלנו:

  • בעת שרשור מיתרים, StringBuilder היא האופציה הנוחה ביותר שעולה בראש. עם זאת, עם המיתרים הקטנים, ה + מבצע כמעט זהה לביצועים. מתחת למכסה המנוע, מהדר Java עשוי להשתמש ב- StringBuilder בכיתה להפחתת מספר אובייקטים מחרוזת
  • להמיר את הערך למחרוזת, ה- [סוג כלשהו] .toString () (Integer.toString () למשל) עובד מהר יותר אז String.valueOf (). מכיוון שההבדל אינו משמעותי, אנו יכולים להשתמש בחופשיות String.valueOf () שלא תהיה תלות בסוג ערך הקלט
  • כשמדובר בהשוואת מחרוזות, שום דבר לא מכה את String.equals () עד כה
  • חוּט כפילויות משפרות את הביצועים ביישומים גדולים ורב-הברגה. אבל שימוש יתר String.intern () עלול לגרום לדליפות זיכרון חמורות ולהאטת היישום
  • לפיצול המיתרים שעלינו להשתמש בהם אינדקס של() לנצח בהופעה. עם זאת, בחלק מהמקרים הלא קריטית String.split () פונקציה עשויה להתאים היטב
  • באמצעות Pattern.match () המחרוזת משפרת את הביצועים באופן משמעותי
  • String.isEmpty () מהיר יותר ממחרוזת.length () == 0

גַם, זכור כי המספרים שאנו מציגים כאן הם רק תוצאות אמת מידה של JMH - לכן עליך לבדוק תמיד את היקף המערכת וזמן הריצה שלך כדי לקבוע את ההשפעה של אופטימיזציות מסוג זה.

לבסוף, כמו תמיד, ניתן למצוא את הקוד ששימש במהלך הדיון ב- GitHub.


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