การต่อสตริงถูกนำไปใช้ใน Java 9 อย่างไร?


111

ตามที่เขียนไว้ในJEP 280: Indify String Concatenation :

เปลี่ยนStringลำดับไบต์โค้ด -concatenation แบบคงที่ที่สร้างขึ้นโดยjavacใช้การinvokedynamicเรียกไปยังฟังก์ชันไลบรารี JDK สิ่งนี้จะเปิดใช้งานการเพิ่มประสิทธิภาพของการStringเรียงต่อกันในอนาคตโดยไม่ต้องมีการเปลี่ยนแปลงเพิ่มเติมกับ bytecode ที่ปล่อยjavacออกมา

ที่นี่ฉันต้องการทำความเข้าใจว่าการใช้การinvokedynamicโทรคืออะไรและการเชื่อมต่อ bytecode แตกต่างจากinvokedynamicอย่างไร


11
ฉันเขียนเกี่ยวกับเรื่องนั้นในขณะที่ย้อนกลับไป - ถ้าช่วยได้ฉันจะย่อเป็นคำตอบ
Nicolai

10
นอกจากนี้ลองดูวิดีโอนี้ซึ่งอธิบายประเด็นของกลไกการต่อสายอักขระแบบใหม่ได้เป็นอย่างดี: youtu.be/wIyeOaitmWM?t=37m58s
ZhekaKozlov

3
@ZhekaKozlov ฉันหวังว่าฉันจะสามารถโหวตความคิดเห็นของคุณได้สองครั้งลิงก์ที่มาจากผู้คนที่นำสิ่งเหล่านี้ไปใช้จริงจะดีที่สุด
Eugene

2
@Nicolai: นั่นจะดีมากและจะเป็นคำตอบที่ดีกว่าที่อื่น ๆ ที่นี่ (รวมถึงของฉันด้วย) ส่วนใด ๆ ของคำตอบของฉันที่คุณต้องการรวมไว้เมื่อคุณทำอย่าลังเล - ถ้าคุณรวม (โดยพื้นฐาน) ทั้งสิ่งเป็นส่วนหนึ่งของคำตอบที่กว้างขึ้นฉันจะลบของฉัน อีกวิธีหนึ่งหากคุณต้องการเพิ่มคำตอบของฉันตามที่เห็นได้ชัดเจนฉันได้กำหนดให้เป็นวิกิชุมชน
TJ Crowder

คำตอบ:


95

วิธี "เก่า" จะแสดงStringBuilderการดำเนินการที่มีพื้นฐาน พิจารณาโปรแกรมนี้:

public class Example {
    public static void main(String[] args)
    {
        String result = args[0] + "-" + args[1] + "-" + args[2];
        System.out.println(result);
    }
}

หากเรารวบรวมสิ่งนั้นด้วย JDK 8 หรือก่อนหน้าแล้วใช้javap -c Exampleเพื่อดู bytecode เราจะเห็นสิ่งนี้:

public class ตัวอย่าง {
  ตัวอย่างสาธารณะ ();
    รหัส:
       0: aload_0
       1: invokespecial # 1 // วิธี java / lang / Object. "<init>" :() V
       4: กลับ

  โมฆะคงที่สาธารณะหลัก (java.lang.String []);
    รหัส:
       0: ใหม่ # 2 // คลาส java / lang / StringBuilder
       3: dup
       4: invokespecial # 3 // Method java / lang / StringBuilder. "<init>" :() V
       7: aload_0
       8: iconst_0
       9: aaload
      10: invokevirtual # 4 // วิธี java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      13: ldc # 5 // สตริง -
      15: invokevirtual # 4 // วิธี java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      18: aload_0
      19: iconst_1
      20: aaload
      21: invokevirtual # 4 // วิธี java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      24: ldc # 5 // สตริง -
      26: invokevirtual # 4 // วิธี java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      29: aload_0
      30: iconst_2
      31: aaload
      32: invokevirtual # 4 // วิธี java / lang / StringBuilder.append: (Ljava / lang / String;) Ljava / lang / StringBuilder;
      35: invokevirtual # 6 // วิธี java / lang / StringBuilder.toString :() Ljava / lang / String;
      38: astore_1
      39: getstatic # 7 // ฟิลด์ java / lang / System.out: Ljava / io / PrintStream;
      42: aload_1
      43: invokevirtual # 8 // วิธี java / io / PrintStream.println: (Ljava / lang / String;) V
      46: กลับ
}

อย่างที่คุณเห็นมันสร้างStringBuilderและใช้append. สิ่งนี้มีชื่อเสียงค่อนข้างไม่มีประสิทธิภาพเนื่องจากความจุเริ่มต้นของบัฟเฟอร์ในตัวStringBuilderมีเพียง 16 ตัวอักษรและไม่มีทางที่คอมไพเลอร์จะรู้ว่าจะจัดสรรเพิ่มเติมล่วงหน้าได้ดังนั้นจึงต้องจัดสรรใหม่ นอกจากนี้ยังมีการเรียกวิธีการมากมาย (โปรดทราบว่าบางครั้ง JVM สามารถตรวจจับและเขียนรูปแบบการโทรเหล่านี้ใหม่เพื่อให้มีประสิทธิภาพมากขึ้น)

มาดูกันว่า Java 9 สร้างอะไรบ้าง:

public class ตัวอย่าง {
  ตัวอย่างสาธารณะ ();
    รหัส:
       0: aload_0
       1: invokespecial # 1 // วิธี java / lang / Object. "<init>" :() V
       4: กลับ

  โมฆะคงที่สาธารณะหลัก (java.lang.String []);
    รหัส:
       0: aload_0
       1: iconst_0
       2: aaload
       3: aload_0
       4: iconst_1
       5: aaload
       6: aload_0
       7: iconst_2
       8: aaload
       9: invokedynamic # 2, 0 // InvokeDynamic # 0: makeConcatWithConstants: (Ljava / lang / String; Ljava / lang / String; Ljava / lang / String;) Ljava / lang / String;
      14: astore_1
      15: getstatic # 3 // ฟิลด์ java / lang / System.out: Ljava / io / PrintStream;
      18: aload_1
      19: invokevirtual # 4 // วิธี java / io / PrintStream.println: (Ljava / lang / String;) V
      22: กลับ
}

โอ้ แต่มันสั้นกว่า :-) มันทำให้สายเดียวที่จะmakeConcatWithConstantsจากStringConcatFactoryที่ว่านี้ใน Javadoc ที่:

วิธีการอำนวยความสะดวกในการสร้างวิธีการต่อสายอักขระที่สามารถใช้เพื่อเชื่อมอาร์กิวเมนต์ที่เป็นที่รู้จักจำนวนมากอย่างมีประสิทธิภาพซึ่งอาจเป็นไปได้หลังจากการปรับประเภทและการประเมินอาร์กิวเมนต์บางส่วน โดยทั่วไปวิธีการเหล่านี้ใช้เป็นวิธีการบูตสำหรับinvokedynamicไซต์การโทรเพื่อสนับสนุนคุณลักษณะการต่อสายอักขระของภาษาการเขียนโปรแกรม Java


41
สิ่งนี้ทำให้ฉันนึกถึงคำตอบที่ฉันเขียนเมื่อเกือบ 6 ปีที่แล้วจนถึงวันนี้: stackoverflow.com/a/7586780/330057 - มีคนถามว่าพวกเขาควรสร้าง StringBuilder หรือแค่ใช้ old ธรรมดา+=ในการวนซ้ำ ฉันบอกพวกเขาว่ามันขึ้นอยู่กับ แต่อย่าลืมว่าพวกเขาอาจพบวิธีที่ดีกว่าในการต่อสายกันในบางครั้ง บรรทัดสำคัญคือบรรทัดสุดท้าย:So by being smart, you have caused a performance hit when Java got smarter than you.
corsiKa

3
@corsiKa: ฮ่า ๆ ! แต่ว้าวใช้เวลานานกว่าจะไปถึงที่นั่น (ฉันไม่ได้หมายถึงหกปีฉันหมายถึง 22 หรือมากกว่านั้น ... :-))
TJ Crowder

1
@supercat: ตามที่ฉันเข้าใจมีสาเหตุสองประการไม่น้อยที่การสร้างอาร์เรย์ varargs เพื่อส่งต่อไปยังเมธอดบนพา ธ ที่สำคัญต่อประสิทธิภาพนั้นไม่เหมาะอย่างยิ่ง นอกจากนี้การใช้invokedynamicช่วยให้สามารถเลือกกลยุทธ์การต่อข้อมูลที่แตกต่างกันได้ที่รันไทม์และผูกไว้กับการเรียกครั้งแรกโดยไม่มีค่าใช้จ่ายในการเรียกใช้เมธอดและตารางการจัดส่งในการเรียกแต่ละครั้ง อื่น ๆ ในNicolai ของบทความที่นี่และในJEP
TJ Crowder

1
@supercat: แล้วมีความจริงที่ว่ามันจะเล่นได้ไม่ดีกับ non-Strings เนื่องจากพวกเขาจะต้องถูกแปลงเป็น String ล่วงหน้าแทนที่จะถูกแปลงเป็นผลลัพธ์สุดท้าย ไม่มีประสิทธิภาพมากขึ้น ทำได้Objectแต่คุณต้องใส่กล่องดั้งเดิมทั้งหมด ... (ซึ่ง Nicolai กล่าวไว้ในบทความที่ยอดเยี่ยมของเขา btw)
TJ Crowder

2
@supercat ฉันอ้างถึงString.concat(String)วิธีการที่มีอยู่แล้วซึ่งการใช้งานกำลังสร้างอาร์เรย์ของสตริงที่เป็นผลลัพธ์ในสถานที่ ข้อได้เปรียบจะกลายเป็นข้อสงสัยเมื่อเราต้องเรียกใช้toString()วัตถุโดยพลการ ในทำนองเดียวกันเมื่อเรียกเมธอดที่ยอมรับอาร์เรย์ผู้เรียกจะต้องสร้างและเติมอาร์เรย์ซึ่งจะลดประโยชน์โดยรวม แต่ตอนนี้ไม่เกี่ยวข้องเนื่องจากโดยพื้นฐานแล้วโซลูชันใหม่คือสิ่งที่คุณกำลังพิจารณายกเว้นว่าไม่มีค่าใช้จ่ายในการชกมวยไม่จำเป็นต้องสร้างอาร์เรย์และแบ็กเอนด์อาจสร้างตัวจัดการที่เหมาะสมที่สุดสำหรับสถานการณ์เฉพาะ
Holger

20

ก่อนที่จะเข้าสู่รายละเอียดของการinvokedynamicใช้งานที่ใช้สำหรับการเพิ่มประสิทธิภาพของการเรียงต่อกันของสตริงในความคิดของฉันเราต้องได้รับความเป็นมาเกี่ยวกับWhat's invokedynamic และฉันจะใช้มันได้อย่างไร

invokedynamic ช่วยลดความยุ่งยากการเรียนการสอนและอาจช่วยเพิ่มการใช้งานของคอมไพเลอร์และระบบรันไทม์สำหรับภาษาแบบไดนามิกใน JVM โดยอนุญาตให้ผู้ใช้ภาษากำหนดพฤติกรรมการเชื่อมโยงแบบกำหนดเองด้วยinvokedynamicคำสั่งซึ่งเกี่ยวข้องกับขั้นตอนด้านล่างนี้


ฉันอาจจะลองและนำคุณผ่านสิ่งเหล่านี้ด้วยการเปลี่ยนแปลงที่เกิดขึ้นสำหรับการใช้งานการเพิ่มประสิทธิภาพการเชื่อมต่อสตริง

  • การกำหนด Bootstrap Method : - ด้วย Java9 ซึ่งเป็นเมธอด bootstrap สำหรับinvokedynamicไซต์การโทรเพื่อสนับสนุนการต่อสตริงเป็นหลักmakeConcatและmakeConcatWithConstantsถูกนำมาใช้กับStringConcatFactoryการนำไปใช้งาน

    การใช้ invokedynamic เป็นทางเลือกในการเลือกกลยุทธ์การแปลจนถึงรันไทม์ กลยุทธ์การแปลที่ใช้StringConcatFactoryจะคล้ายกับที่LambdaMetafactoryแนะนำในเวอร์ชันจาวาก่อนหน้านี้ นอกจากนี้หนึ่งในเป้าหมายของ JEP ที่กล่าวถึงในคำถามคือการยืดกลยุทธ์เหล่านี้ออกไป

  • การระบุรายการ Constant Pool : - นี่คืออาร์กิวเมนต์แบบคงที่เพิ่มเติมสำหรับinvokedynamicคำสั่งอื่นที่ไม่ใช่MethodHandles.Lookupอ็อบเจ็กต์(1) ซึ่งเป็นโรงงานสำหรับสร้างเมธอดจัดการในบริบทของinvokedynamicคำสั่ง (2) Stringอ็อบเจ็กต์ชื่อเมธอดที่กล่าวถึงในการเรียกแบบไดนามิก ไซต์และ (3) MethodTypeอ็อบเจ็กต์ซึ่งเป็นลายเซ็นประเภทที่แก้ไขแล้วของไซต์การเรียกแบบไดนามิก

    มีการเชื่อมโยงระหว่างการเชื่อมโยงของรหัสแล้ว ในรันไทม์เมธอด bootstrap จะรันและลิงก์ในโค้ดจริงที่ทำการเชื่อมต่อ มันเขียนการinvokedynamicโทรใหม่ด้วยการinvokestaticโทรที่เหมาะสม สิ่งนี้จะโหลดสตริงคงที่จากพูลคงที่ซึ่งวิธีการบูตสแต็กจะถูกใช้ประโยชน์เพื่อส่งผ่านค่าคงที่เหล่านี้และค่าคงที่อื่น ๆ ไปยังการเรียกเมธอด bootstrap

  • การใช้คำสั่งที่เรียกใช้แบบไดนามิก : - สิ่งนี้นำเสนอสิ่งอำนวยความสะดวกสำหรับการเชื่อมโยงแบบเกียจคร้านโดยให้วิธีการบูตสแตรปเป้าหมายการโทรหนึ่งครั้งในระหว่างการเรียกใช้ครั้งแรก แนวคิดที่เป็นรูปธรรมสำหรับการเพิ่มประสิทธิภาพในที่นี้คือการแทนที่การStringBuilder.appendเต้นรำทั้งหมดด้วยการinvokedynamicเรียกง่ายๆjava.lang.invoke.StringConcatFactoryว่าจะยอมรับค่าที่ต้องการการเชื่อมต่อกัน

Indify สตริงรัฐข้อเสนอกับตัวอย่างเปรียบเทียบของการประยุกต์ใช้กับ Java9 ที่วิธีการที่คล้ายกันเป็นร่วมกันโดย@TJ Crowderจะรวบรวมและความแตกต่างใน bytecode เป็นธรรมระหว่างการมองเห็นการดำเนินการที่แตกต่างกัน


17

ฉันจะเพิ่มรายละเอียดเล็กน้อยที่นี่ ส่วนหลักที่จะได้รับก็คือว่าวิธีสตริงจะทำคือการตัดสินใจรันไทม์ไม่รวบรวมเวลาหนึ่งอีกต่อไป ดังนั้นจึงสามารถเปลี่ยนแปลงได้หมายความว่าคุณได้คอมไพล์โค้ดของคุณครั้งเดียวกับ java-9และสามารถเปลี่ยนการใช้งานพื้นฐานได้ตามต้องการโดยไม่จำเป็นต้องคอมไพล์ใหม่

และประเด็นที่สองคือในขณะนี้มี6 possible strategies for concatenation of String:

 private enum Strategy {
    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder}.
     */
    BC_SB,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but trying to estimate the required storage.
     */
    BC_SB_SIZED,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but computing the required storage exactly.
     */
    BC_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also tries to estimate the required storage.
     */
    MH_SB_SIZED,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also estimate the required storage exactly.
     */
    MH_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that constructs its own byte[] array from
     * the arguments. It computes the required storage exactly.
     */
    MH_INLINE_SIZED_EXACT
}

คุณสามารถเลือกใด ๆ -Djava.lang.invoke.stringConcatของพวกเขาผ่านทางพารามิเตอร์: สังเกตว่าStringBuilderยังคงเป็นตัวเลือก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.