การประกาศอาร์เรย์หลายรายการด้วยองค์ประกอบ 64 รายการเร็วกว่าการประกาศอาร์เรย์ 65 องค์ประกอบ 1,000 เท่า


91

เมื่อเร็ว ๆ นี้ฉันสังเกตเห็นว่าการประกาศอาร์เรย์ที่มี 64 องค์ประกอบนั้นเร็วกว่ามาก (> 1,000 เท่า) กว่าการประกาศอาร์เรย์ประเภทเดียวกันที่มี 65 องค์ประกอบ

นี่คือรหัสที่ฉันใช้ทดสอบสิ่งนี้:

public class Tests{
    public static void main(String args[]){
        double start = System.nanoTime();
        int job = 100000000;//100 million
        for(int i = 0; i < job; i++){
            double[] test = new double[64];
        }
        double end = System.nanoTime();
        System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
    }
}

สิ่งนี้จะทำงานในเวลาประมาณ 6 มิลลิวินาทีหากฉันแทนที่new double[64]ด้วยnew double[65]จะใช้เวลาประมาณ 7 วินาที ปัญหานี้จะรุนแรงขึ้นอย่างทวีคูณหากงานกระจายไปทั่วเธรดมากขึ้นเรื่อย ๆ ซึ่งเป็นที่มาของปัญหาของฉัน

ปัญหานี้ยังเกิดขึ้นกับชนิดของอาร์เรย์เช่นหรือint[65] String[65]ปัญหานี้ไม่เกิดขึ้นกับสตริงขนาดใหญ่: String test = "many characters";แต่จะเริ่มเกิดขึ้นเมื่อสิ่งนี้ถูกเปลี่ยนเป็นString test = i + "";

ฉันสงสัยว่าเหตุใดจึงเป็นเช่นนั้นและหากเป็นไปได้ที่จะหลีกเลี่ยงปัญหานี้


3
Off-note: System.nanoTime()ควรเป็นที่ต้องการมากกว่าSystem.currentTimeMillis()สำหรับการเปรียบเทียบ
rocketboy

4
ฉันแค่อยากรู้? คุณอยู่ภายใต้ Linux หรือไม่? พฤติกรรมเปลี่ยนไปตาม OS หรือไม่?
bsd

9
คำถามนี้ได้รับการโหวตลงมาบนโลกได้อย่างไร ??
Rohit Jain

2
FWIW ผมเห็นความแตกต่างประสิทธิภาพการทำงานที่คล้ายกันถ้าผมเรียกใช้รหัสนี้กับแทนbyte double
Oliver Charlesworth

3
@ThomasJungblut: แล้วอะไรที่อธิบายถึงความคลาดเคลื่อนในการทดลองของ OP?
Oliver Charlesworth

คำตอบ:


88

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

ก่อนที่จะลงรายละเอียดเรามาดูเนื้อหาของลูปอย่างละเอียด:

double[] test = new double[64];

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

สำหรับการเปรียบเทียบอย่างน้อยคุณควรปฏิบัติตามสองแนวทางต่อไปนี้ หากคุณทำเช่นนั้นความแตกต่างจะน้อยลงอย่างมาก

  • อุ่นเครื่องคอมไพเลอร์ JIT (และเครื่องมือเพิ่มประสิทธิภาพ) โดยเรียกใช้เกณฑ์มาตรฐานหลาย ๆ ครั้ง
  • ใช้ผลลัพธ์ของทุกนิพจน์และพิมพ์ที่ส่วนท้ายของเกณฑ์มาตรฐาน

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

-XX:-DoEscapeAnalysisการเพิ่มประสิทธิภาพสามารถใช้งานกับตัวเลือกบรรทัดคำสั่ง ค่าเวทมนตร์ 64 สำหรับอาร์เรย์สเกลาร์ยังสามารถเปลี่ยนแปลงได้ในบรรทัดคำสั่ง หากคุณรันโปรแกรมของคุณดังต่อไปนี้จะไม่มีความแตกต่างระหว่างอาร์เรย์ที่มีองค์ประกอบ 64 และ 65:

java -XX:EliminateAllocationArraySizeLimit=65 Tests

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


9
แต่ทำไมเครื่องมือเพิ่มประสิทธิภาพตรวจพบว่าอาร์เรย์ขนาด 64 ถอดออกได้ แต่ไม่ใช่ 65
ug_

10
@nosid: แม้ว่ารหัสของ OP อาจไม่เป็นจริง แต่ก็เป็นการกระตุ้นให้เกิดพฤติกรรมที่น่าสนใจ / ไม่คาดคิดใน JVM ซึ่งอาจมีผลในสถานการณ์อื่น ๆ ฉันคิดว่าถูกต้องที่จะถามว่าเหตุใดจึงเกิดขึ้น
Oliver Charlesworth

1
@ThomasJungblut ฉันไม่คิดว่าห่วงจะถูกลบออก คุณสามารถเพิ่ม "int total" นอกลูปและเพิ่ม "total + = test [0];" ตามตัวอย่างด้านบน จากนั้นพิมพ์ผลลัพธ์คุณจะเห็นว่าทั้งหมด = 100 ล้านและ stull ทำงานในเวลาไม่ถึงหนึ่งวินาที
Sipko

1
การแทนที่บนสแต็กเป็นเรื่องเกี่ยวกับการแทนที่โค้ดที่ตีความด้วยคอมไพล์ทันทีแทนที่จะแทนที่การจัดสรรฮีปด้วยการจัดสรรสแต็ก EliminateAllocationArraySizeLimit คือขนาดขีด จำกัด ของอาร์เรย์ที่ถือว่าสามารถเปลี่ยนสเกลาร์ได้ในการวิเคราะห์ Escape ดังนั้นประเด็นหลักที่เอฟเฟกต์เกิดจากการเพิ่มประสิทธิภาพคอมไพลเลอร์นั้นถูกต้อง แต่ไม่ได้เกิดจากการจัดสรรสแต็ก แต่เนื่องจากเฟสการวิเคราะห์การหลีกเลี่ยงไม่สังเกตเห็นว่าไม่จำเป็นต้องมีการจัดสรร
kiheru

2
@Sipko: คุณกำลังเขียนว่าแอปพลิเคชันไม่ได้ปรับขนาดตามจำนวนเธรด นั่นเป็นข้อบ่งชี้ว่าปัญหาไม่เกี่ยวข้องกับการเพิ่มประสิทธิภาพขนาดเล็กที่คุณกำลังถามถึง ขอแนะนำให้มองภาพใหญ่แทนส่วนเล็ก ๆ
nosid

2

มีหลายวิธีที่อาจทำให้เกิดความแตกต่างขึ้นอยู่กับขนาดของวัตถุ

ตามที่ระบุไว้ nosid JITC อาจเป็น (ส่วนใหญ่คือ) จัดสรรอ็อบเจ็กต์ "local" ขนาดเล็กบนสแต็กและขนาดคัตออฟสำหรับอาร์เรย์ "เล็ก" อาจอยู่ที่ 64 องค์ประกอบ

การจัดสรรบนสแต็กนั้นเร็วกว่าการจัดสรรในฮีปอย่างมากและยิ่งไปกว่านั้นสแต็กไม่จำเป็นต้องเก็บขยะดังนั้นค่าโสหุ้ย GC จึงลดลงอย่างมาก (และสำหรับกรณีทดสอบนี้ค่าใช้จ่าย GC น่าจะเป็น 80-90% ของเวลาดำเนินการทั้งหมด)

นอกจากนี้เมื่อมีการจัดสรรค่าสแต็กแล้ว JITC สามารถดำเนินการ "กำจัดรหัสตาย" ได้ตรวจสอบว่าnewไม่เคยใช้ผลลัพธ์ของค่านี้ที่ใดและหลังจากมั่นใจว่าจะไม่มีผลข้างเคียงที่จะสูญหายให้กำจัดการnewดำเนินการทั้งหมดแล้วลูป (ตอนนี้ว่างเปล่า) เอง

แม้ว่า JITC จะไม่ทำการจัดสรรสแต็ก แต่ก็เป็นไปได้ทั้งหมดที่วัตถุที่มีขนาดเล็กกว่าขนาดที่กำหนดจะถูกจัดสรรในฮีปต่างกัน (เช่นจาก "ช่องว่าง" ที่แตกต่างกัน) มากกว่าวัตถุขนาดใหญ่ (โดยปกติสิ่งนี้จะไม่ทำให้เกิดความแตกต่างของเวลาอย่างมาก)


สายไปที่กระทู้นี้ เหตุใดการจัดสรรบนสแต็กจึงเร็วกว่าการจัดสรรบนฮีป ตามบทความสองสามข้อการจัดสรรบนฮีปใช้เวลาประมาณ 12 คำสั่ง ไม่มีพื้นที่สำหรับการปรับปรุงมากนัก
Vortex

@Vortex - การจัดสรรให้กับสแต็กใช้เวลา 1-2 คำสั่ง แต่นั่นคือการจัดสรรเฟรมสแต็กทั้งหมด กรอบสแต็กจะต้องได้รับการจัดสรรเพื่อให้มีพื้นที่บันทึกการลงทะเบียนสำหรับรูทีนดังนั้นตัวแปรอื่น ๆ ที่จัดสรรพร้อมกันจึง "ว่าง" และอย่างที่บอกว่าสแต็กไม่จำเป็นต้องใช้ GC ค่าโสหุ้ย GC สำหรับไอเท็มฮีปนั้นสูงกว่าต้นทุนของการดำเนินการจัดสรรฮีปมาก
Hot Licks
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.