.toArray (ใหม่ MyClass [0]) หรือ. toArray (ใหม่ MyClass [myList.size ()])?


176

สมมติว่าฉันมี ArrayList

ArrayList<MyClass> myList;

และฉันต้องการโทรหาอาเรย์มีเหตุผลด้านประสิทธิภาพที่จะใช้หรือไม่

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

เกิน

MyClass[] arr = myList.toArray(new MyClass[0]);

?

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

แน่นอนใน 99% ของกรณีมันไม่ได้สร้างความแตกต่างอย่างใดอย่างหนึ่ง แต่ฉันต้องการที่จะรักษาสไตล์ที่สอดคล้องกันระหว่างรหัสปกติของฉันและลูปภายในที่ดีที่สุดของฉัน ...


6
ดูเหมือนว่าคำถามจะได้รับการแก้ไขในโพสต์บล็อกใหม่โดย Aleksey Shipilёv, Arrays of Wisdom of Ancients !
glts

6
จากการโพสต์บล็อก: "Bottom line: toArray (ใหม่ T [0]) ดูเหมือนเร็วขึ้นปลอดภัยขึ้นและทำความสะอาดตามสัญญาดังนั้นจึงควรเป็นตัวเลือกเริ่มต้นทันที"
DavidS

คำตอบ:


109

เวอร์ชั่นที่เร็วที่สุดบน Hotspot 8 โดยไม่ได้ตั้งใจคือ:

MyClass[] arr = myList.toArray(new MyClass[0]);

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

ผลการเปรียบเทียบเกณฑ์มาตรฐาน (คะแนนเป็นไมโครวินาที, เล็กกว่า = ดีกว่า):

Benchmark                      (n)  Mode  Samples    Score   Error  Units
c.a.p.SO29378922.preSize         1  avgt       30    0.025  0.001  us/op
c.a.p.SO29378922.preSize       100  avgt       30    0.155  0.004  us/op
c.a.p.SO29378922.preSize      1000  avgt       30    1.512  0.031  us/op
c.a.p.SO29378922.preSize      5000  avgt       30    6.884  0.130  us/op
c.a.p.SO29378922.preSize     10000  avgt       30   13.147  0.199  us/op
c.a.p.SO29378922.preSize    100000  avgt       30  159.977  5.292  us/op
c.a.p.SO29378922.resize          1  avgt       30    0.019  0.000  us/op
c.a.p.SO29378922.resize        100  avgt       30    0.133  0.003  us/op
c.a.p.SO29378922.resize       1000  avgt       30    1.075  0.022  us/op
c.a.p.SO29378922.resize       5000  avgt       30    5.318  0.121  us/op
c.a.p.SO29378922.resize      10000  avgt       30   10.652  0.227  us/op
c.a.p.SO29378922.resize     100000  avgt       30  139.692  8.957  us/op

สำหรับการอ้างอิงรหัส:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
public class SO29378922 {
  @Param({"1", "100", "1000", "5000", "10000", "100000"}) int n;
  private final List<Integer> list = new ArrayList<>();
  @Setup public void populateList() {
    for (int i = 0; i < n; i++) list.add(0);
  }
  @Benchmark public Integer[] preSize() {
    return list.toArray(new Integer[n]);
  }
  @Benchmark public Integer[] resize() {
    return list.toArray(new Integer[0]);
  }
}

คุณสามารถค้นหาผลที่คล้ายกันวิเคราะห์เต็มรูปแบบและการอภิปรายในโพสต์บล็อกอาร์เรย์ของภูมิปัญญาของคนโบราณ ในการสรุป: คอมไพเลอร์ JVM และ JIT มีการปรับแต่งหลายอย่างที่ช่วยให้สามารถสร้างและเริ่มต้นอาร์เรย์ที่มีขนาดที่ถูกต้องใหม่ได้อย่างถูกและไม่สามารถใช้การเพิ่มประสิทธิภาพเหล่านั้นได้หากคุณสร้างอาร์เรย์ด้วยตัวเอง


2
ความคิดเห็นที่น่าสนใจมาก ฉันประหลาดใจที่ไม่มีใครแสดงความคิดเห็นในเรื่องนี้ ฉันเดาว่าเป็นเพราะมันขัดแย้งกับคำตอบอื่น ๆ ที่นี่เท่าที่ความเร็ว นอกจากนี้ที่น่าสนใจที่จะทราบชื่อเสียงของคนนี้เกือบจะสูงกว่าคำตอบอื่น ๆ ทั้งหมด (รวมกัน)
Pimp Trizkit

ฉันเชือนแช ฉันอยากจะดูมาตรฐานสำหรับMyClass[] arr = myList.stream().toArray(MyClass[]::new);.. ซึ่งฉันเดาว่าจะช้าลง นอกจากนี้ฉันต้องการดูเกณฑ์มาตรฐานสำหรับความแตกต่างกับการประกาศอาร์เรย์ ในความแตกต่างระหว่าง: MyClass[] arr = new MyClass[myList.size()]; arr = myList.toArray(arr);และMyClass[] arr = myList.toArray(new MyClass[myList.size()]);... หรือควรจะไม่มีความแตกต่าง? ฉันเดาว่าทั้งสองนี้เป็นปัญหาที่toArrayเกิดขึ้นนอกเหนือจากฟังก์ชั่นที่เกิดขึ้น แต่เดี๋ยวก่อน! ฉันไม่คิดว่าฉันจะเรียนรู้เกี่ยวกับความแตกต่างที่ซับซ้อนอื่น ๆ
Pimp Trizkit

1
@PimpTrizkit เพิ่งตรวจสอบ: การใช้ตัวแปรพิเศษไม่ทำให้เกิดความแตกต่างอย่างที่คาดไว้การใช้สตรีมจะใช้เวลาระหว่าง 60% ถึง 100% ในการโทรtoArrayโดยตรง (ยิ่งขนาดเล็กยิ่งมีค่าใช้จ่ายที่สัมพันธ์กันมากขึ้น)
assylias

ว้าวนั่นคือการตอบสนองที่รวดเร็ว! ขอบคุณ! ใช่ฉันสงสัยว่า การแปลงเป็นสตรีมไม่ได้มีประสิทธิภาพ แต่คุณไม่เคยรู้!
Pimp Trizkit

2
ข้อสรุปเดียวกันนี้พบได้ที่นี่: shipilev.net/blog/2016/arrays-wisdom-ancients
167019

122

ในฐานะของArrayList ใน Java 5อาร์เรย์จะถูกเติมแล้วถ้ามีขนาดที่เหมาะสม (หรือใหญ่กว่า) ดังนั้น

MyClass[] arr = myList.toArray(new MyClass[myList.size()]);

จะสร้างวัตถุอาร์เรย์หนึ่งกรอกข้อมูลแล้วส่งกลับไปที่ "arr" ในทางกลับกัน

MyClass[] arr = myList.toArray(new MyClass[0]);

จะสร้างสองอาร์เรย์ อันที่สองคืออาร์เรย์ของ MyClass ที่มีความยาว 0 ดังนั้นจึงมีการสร้างวัตถุสำหรับวัตถุที่จะถูกโยนทิ้งทันที เท่าที่ซอร์สโค้ดแนะนำคอมไพเลอร์ / JIT ไม่สามารถปรับให้เหมาะสมเพื่อที่จะไม่ถูกสร้างขึ้น นอกจากนี้การใช้ผลลัพธ์วัตถุที่มีความยาวเป็นศูนย์ในการคัดเลือก (s) ภายในวิธี toArray () -

ดูที่มาของ ArrayList.toArray ():

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

ใช้วิธีแรกเพื่อให้มีเพียงวัตถุเดียวเท่านั้นที่ถูกสร้างและหลีกเลี่ยงการหล่อ (โดยนัย แต่มีราคาแพง)


1
สองความคิดเห็นอาจเป็นที่สนใจของใครบางคน: 1) LinkedList.toArray (T [] a) ช้าลง (ใช้การสะท้อน: Array.newInstance) และซับซ้อนยิ่งขึ้น 2)ในทางกลับกันในการเปิดตัว JDK7 ฉันรู้สึกประหลาดใจมากที่พบว่า Array ที่ทำงานช้าอย่างเจ็บปวดมักจะทำงานเร็วเกือบจะเร็วเท่ากับการสร้างอาเรย์ทั่วไป!
java.is.for.desktop

1
ขนาด @ktaria เป็นสมาชิกส่วนตัวของ ArrayList โดยเฉพาะ **** แปลกใจ **** ขนาด ดูArrayList SourceCode
MyPasswordIsLasercats

3
การคาดเดาประสิทธิภาพที่ไม่มีมาตรฐานทำงานเฉพาะในกรณีที่ไม่สำคัญ ที่จริงแล้วnew Myclass[0]เร็วกว่า: shipilev.net/blog/2016/arrays-wisdom-ancients
Karol S

นี่ไม่ใช่คำตอบที่ถูกต้องตั้งแต่ JDK6 +
АнтонАнтонов

28

จากการตรวจสอบ JetBrains Intellij Idea:

มีสองลักษณะในการแปลงคอลเล็กชันเป็นอาร์เรย์: อาจใช้อาร์เรย์ที่มีขนาดล่วงหน้า (เช่นc.toArray (สตริงใหม่ [c.size ()]) ) หรือใช้อาร์เรย์ว่าง (เช่นc.toArray (สตริงใหม่ [ 0])

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

การตรวจสอบนี้อนุญาตให้ทำตามรูปแบบที่เหมือนกัน: ไม่ว่าจะใช้อาเรย์ที่ว่างเปล่า (ซึ่งแนะนำในจาวาสมัยใหม่) หรือการใช้อาเรย์ที่มีขนาดก่อนหน้า (ซึ่งอาจเร็วกว่าในจาวารุ่นเก่า


หากทั้งหมดนี้ถูกคัดลอก / ข้อความที่ยกมาเราสามารถจัดรูปแบบตามนั้นและยังให้ลิงค์ไปยังแหล่งที่มา? ที่จริงฉันมาที่นี่เพราะการตรวจสอบ IntelliJ และฉันสนใจในลิงค์เพื่อค้นหาการตรวจสอบทั้งหมดและเหตุผลที่อยู่เบื้องหลังพวกเขา
Tim Büthe

3
ที่นี่คุณสามารถตรวจสอบข้อความการตรวจสอบ: github.com/JetBrains/intellij-community/tree/master/plugins/
......


17

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


2
Upvoting 'นำอาเรย์ที่ว่างมาใช้ซ้ำ' เนื่องจากเป็นการประนีประนอมระหว่างความสามารถในการอ่านและประสิทธิภาพที่อาจเกิดขึ้นซึ่งควรค่าแก่การพิจารณา การผ่านอาร์กิวเมนต์ที่ประกาศไว้private static final MyClass[] EMPTY_MY_CLASS_ARRAY = new MyClass[0]จะไม่ป้องกันอาร์เรย์ที่ถูกส่งคืนจากการสร้างการสะท้อนกลับ แต่มันจะป้องกันไม่ให้อาร์เรย์เพิ่มเติมถูกสร้างขึ้นในแต่ละครั้ง
Michael Scheper

Machael ถูกต้องถ้าคุณใช้อาร์เรย์ที่มีความยาวเป็นศูนย์จะไม่มีทางแก้ไข: (T []) java.lang.reflect.Array.newInstance (a.getClass (). getComponentType (), ขนาด); ซึ่งจะฟุ่มเฟือยในถ้าขนาดจะเป็น> = actualSize (JDK7)
อเล็กซ์

หากคุณสามารถให้การอ้างอิงสำหรับ "JVM ที่ทันสมัยเพิ่มประสิทธิภาพการสร้างอาร์เรย์สะท้อนแสงในกรณีนี้" ฉันยินดีที่จะโหวตคำตอบนี้
Tom Panning

ฉันกำลังเรียนรู้ที่นี่ ถ้าฉันใช้: MyClass[] arr = myList.stream().toArray(MyClass[]::new);มันจะช่วยหรือทำร้ายคอลเลคชั่นที่ซิงโครไนซ์และเกิดขึ้นพร้อมกันหรือไม่ และทำไม? กรุณา.
Pimp Trizkit

3

toArray ตรวจสอบว่าอาร์เรย์ที่ส่งผ่านนั้นมีขนาดที่ถูกต้อง (นั่นคือมีขนาดใหญ่พอที่จะใส่องค์ประกอบจากรายการของคุณ) และถ้าใช่ให้ใช้มัน ดังนั้นหากขนาดของอาร์เรย์ที่ให้มีขนาดเล็กกว่าที่ต้องการอาร์เรย์ใหม่จะถูกสร้างขึ้นใหม่

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

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


2

กรณีแรกนั้นมีประสิทธิภาพมากกว่า

นั่นเป็นเพราะในกรณีที่สอง:

MyClass[] arr = myList.toArray(new MyClass[0]);

ขณะรันไทม์สร้างอาร์เรย์ว่างเปล่า (มีขนาดเป็นศูนย์) จากนั้นภายในเมธอด toArray จะสร้างอาร์เรย์อีกอันเพื่อให้พอดีกับข้อมูลจริง การสร้างนี้ทำได้โดยใช้การสะท้อนโดยใช้รหัสต่อไปนี้ (นำมาจาก jdk1.5.0_10):

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        a = (T[])java.lang.reflect.Array.
    newInstance(a.getClass().getComponentType(), size);
System.arraycopy(elementData, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

โดยใช้แบบฟอร์มแรกคุณหลีกเลี่ยงการสร้างอาร์เรย์ที่สองและหลีกเลี่ยงการใช้รหัสการสะท้อนกลับ


toArray () ไม่ได้ใช้การสะท้อน อย่างน้อยตราบใดที่คุณไม่นับ "ชี้ขาด" เป็นภาพสะท้อนอยู่ดี ;-)
Georgi

toArray (T []) ทำ จำเป็นต้องสร้างอาเรย์ประเภทที่เหมาะสม JVM สมัยใหม่ปรับแต่งการสะท้อนแบบนั้นให้มีความเร็วใกล้เคียงกับรุ่นที่ไม่สะท้อนแสง
Tom Hawtin - tackline

ฉันคิดว่ามันใช้การสะท้อน JDK 1.5.0_10 ทำอย่างแน่นอนและการไตร่ตรองเป็นวิธีเดียวที่ฉันรู้ในการสร้างอาร์เรย์ของประเภทที่คุณไม่ทราบเวลารวบรวม
Panagiotis Korros

จากนั้นหนึ่งในซอร์สโค้ดตัวอย่างของเธอ (อันหนึ่งข้างบนหรือของฉัน) นั้นล้าสมัย น่าเสียดายที่ฉันไม่พบหมายเลขรุ่นย่อยที่ถูกต้องสำหรับฉัน
Georgi

1
Georgi รหัสของคุณมาจาก JDK 1.6 และถ้าคุณเห็นการใช้งานของวิธี Arrays.copyTo คุณจะเห็นว่าการใช้งานนั้นใช้การสะท้อน
Panagiotis Korros

-1

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


-2

การใช้ 'toArray' กับอาเรย์ของขนาดที่ถูกต้องจะทำงานได้ดีขึ้นเนื่องจากทางเลือกจะสร้างอาเรย์แรกที่มีขนาดเป็นศูนย์จากนั้นอาเรย์ของขนาดที่ถูกต้อง อย่างไรก็ตามในขณะที่คุณพูดความแตกต่างมีแนวโน้มที่จะเล็กน้อย

นอกจากนี้โปรดทราบว่าคอมไพเลอร์ javac ไม่ได้ทำการปรับให้เหมาะสม ทุกวันนี้การปรับให้เหมาะสมทั้งหมดดำเนินการโดยคอมไพเลอร์ JIT / HotSpot ตอนรันไทม์ ฉันไม่ได้ตระหนักถึงการเพิ่มประสิทธิภาพรอบ ๆ 'toArray' ใน JVM ใด ๆ

คำตอบสำหรับคำถามของคุณนั้นเป็นเรื่องของรูปแบบ แต่เพื่อประโยชน์ของความสอดคล้องควรเป็นส่วนหนึ่งของมาตรฐานการเข้ารหัสที่คุณปฏิบัติตาม (ไม่ว่าจะเป็นเอกสารหรืออื่น ๆ )


OTOH ถ้ามาตรฐานคือการใช้อาร์เรย์ที่มีความยาวเป็นศูนย์กรณีที่เบี่ยงเบนก็หมายความว่าประสิทธิภาพเป็นเรื่องที่น่ากังวล
Michael Scheper

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