ฉันจะเขียน micro-benchmark ที่ถูกต้องใน Java ได้อย่างไร


870

คุณเขียน (และรัน) เกณฑ์มาตรฐานขนาดเล็กที่ถูกต้องใน Java ได้อย่างไร

ฉันกำลังมองหาตัวอย่างโค้ดและความคิดเห็นที่แสดงถึงสิ่งต่าง ๆ ที่ควรพิจารณา

ตัวอย่าง: เกณฑ์มาตรฐานควรวัดเวลา / การวนซ้ำหรือการวนซ้ำ / เวลาและทำไม?

ที่เกี่ยวข้อง: การเปรียบเทียบเกณฑ์มาตรฐานของนาฬิกาจับเวลายอมรับได้หรือไม่


ดู [คำถามนี้] [1] เมื่อไม่กี่นาทีก่อนเพื่อดูข้อมูลที่เกี่ยวข้อง แก้ไข: ขออภัยนี่ไม่ควรจะเป็นคำตอบ ฉันควรจะโพสต์เป็นความคิดเห็น [1]: stackoverflow.com/questions/503877/…
Tiago

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

5
Java 9 อาจมีคุณสมบัติบางอย่างสำหรับการวัดแบบไมโคร: openjdk.java.net/jeps/230
Raedwald

1
@ Raedwald ฉันคิดว่า JEP มุ่งหวังที่จะเพิ่มเกณฑ์มาตรฐานขนาดเล็กลงในรหัส JDK แต่ฉันไม่คิดว่า jmh จะรวมอยู่ใน JDK ...
assylias

1
@Raedwald สวัสดีจากอนาคต มันไม่ได้ทำให้ตัด
Michael

คำตอบ:


787

เคล็ดลับเกี่ยวกับการเขียนมาตรฐานขนาดเล็กจากผู้สร้าง Java HotSpot :

กฎที่ 0:อ่านกระดาษที่มีชื่อเสียงบน JVMs และการวัดระดับไมโคร หนึ่งที่ดีคือไบรอันเก๊ 2005 อย่าคาดหวังมากเกินไปจากการวัดแบบไมโคร พวกเขาวัดเฉพาะช่วงประสิทธิภาพการทำงานของ JVM ที่ จำกัด

กฎที่ 1:รวมขั้นตอนการอุ่นเครื่องที่รันเคอร์เนลทดสอบของคุณตลอดทางเสมอเพียงพอที่จะทริกเกอร์การเริ่มต้นและการรวบรวมทั้งหมดก่อนช่วงเวลา (การวนซ้ำน้อยลงก็โอเคในช่วงวอร์มอัพกฎของหัวแม่มือคือการวนซ้ำภายในหลายหมื่นครั้ง)

กฎข้อที่ 2:เรียกใช้เสมอกับ-XX:+PrintCompilation, -verbose:gcฯลฯ เพื่อให้คุณสามารถตรวจสอบว่าคอมไพเลอร์และส่วนอื่น ๆ ของ JVM ยังไม่ได้ทำผลงานที่ไม่คาดคิดในช่วงระยะเวลาของคุณ

กฎข้อที่ 2.1:พิมพ์ข้อความที่จุดเริ่มต้นและจุดสิ้นสุดของการกำหนดเวลาและการอุ่นเครื่องเพื่อให้คุณสามารถตรวจสอบว่าไม่มีผลลัพธ์จากกฎ 2 ในช่วงระยะเวลา

กฎข้อที่ 3:ระวังความแตกต่างระหว่าง-clientและ-server, และ OSR และการคอมไพล์ปกติ -XX:+PrintCompilationธงรายงานรวบรวม OSR Trouble$1::run @ 2 (41 bytes)กับที่เข้าสู่ระบบเพื่อแสดงถึงจุดเริ่มต้นที่ไม่ได้เริ่มต้นตัวอย่างเช่น: ต้องการเซิร์ฟเวอร์ไปยังไคลเอนต์และ OSR ปกติหากคุณอยู่หลังประสิทธิภาพที่ดีที่สุด

กฎข้อที่ 4:ระวังผลกระทบการเริ่มต้น อย่าพิมพ์เป็นครั้งแรกในระหว่างช่วงเวลาของคุณเนื่องจากโหลดการพิมพ์และเริ่มต้นคลาส อย่าโหลดคลาสใหม่นอกระยะ warmup (หรือเฟสการรายงานขั้นสุดท้าย) เว้นแต่คุณจะทำการทดสอบคลาสที่โหลดโดยเฉพาะ (และในกรณีนั้นจะโหลดเฉพาะคลาสทดสอบเท่านั้น) กฎข้อที่ 2 เป็นบรรทัดแรกของการป้องกันผลกระทบดังกล่าว

กฎข้อที่ 5:ระวังการ deoptimization และการคอมไพล์ซ้ำ อย่าใช้โค้ดพา ธ ใด ๆ เป็นครั้งแรกในช่วงเวลาเนื่องจากคอมไพเลอร์อาจขยะและคอมไพล์โค้ดใหม่ตามข้อสันนิษฐานในแง่ดีก่อนหน้านี้ว่าพา ธ นั้นจะไม่ถูกใช้เลย กฎข้อที่ 2 เป็นบรรทัดแรกของการป้องกันผลกระทบดังกล่าว

กฎข้อที่ 6:ใช้เครื่องมือที่เหมาะสมเพื่ออ่านใจของคอมไพเลอร์และคาดว่าจะประหลาดใจกับโค้ดที่สร้างขึ้น ตรวจสอบโค้ดด้วยตัวเองก่อนสร้างทฤษฎีเกี่ยวกับสิ่งที่ทำให้บางอย่างเร็วขึ้นหรือช้าลง

กฎข้อที่ 7:ลดเสียงรบกวนในการวัดของคุณ รันเกณฑ์มาตรฐานของคุณบนเครื่องที่เงียบและรันหลายครั้งโดยทิ้งค่าผิดปกติ ใช้-Xbatchเพื่อคอมไพเลอร์ซีเรียล-XX:CICompilerCount=1ไลซ์เซชั่นกับแอพพลิเคชั่นและพิจารณาการตั้งค่าเพื่อป้องกันคอมไพเลอร์ไม่ให้ทำงานควบคู่กันไป พยายามอย่างดีที่สุดเพื่อลดค่าใช้จ่าย GC, ตั้งค่าXmx(ใหญ่พอ) เท่ากับXmsและใช้UseEpsilonGCหากมี

กฎข้อที่ 8:ใช้ไลบรารีสำหรับการวัดประสิทธิภาพของคุณเนื่องจากอาจมีประสิทธิภาพมากกว่าและดีบั๊กสำหรับวัตถุประสงค์นี้เพียงอย่างเดียวแล้ว เช่นJMH , คาลิปเปอร์หรือบิลและพอล UCSD มาตรฐานที่ดีเยี่ยมสำหรับ Java


5
นี่เป็นบทความที่น่าสนใจ: ibm.com/developerworks/java/library/j-jtp12214
John Nilsson

142
นอกจากนี้ห้ามใช้ System.currentTimeMillis () เว้นแต่ว่าคุณจะตกลงด้วยความแม่นยำ + หรือ - 15 ms ซึ่งเป็นเรื่องปกติสำหรับ OS + JVM ส่วนใหญ่ ใช้ System.nanoTime () แทน
Scott Carey

5
เอกสารบางส่วนจาก javaOne: azulsystems.com/events/javaone_2009/session/…
bestsss

93
มันควรจะตั้งข้อสังเกตว่าSystem.nanoTime()จะไม่รับประกันSystem.currentTimeMillis()ความถูกต้องมากกว่า รับประกันได้ว่าอย่างน้อยก็แม่นยำเท่านั้น อย่างไรก็ตามมักจะมีความแม่นยำมากกว่า
แรงโน้มถ่วง

41
เหตุผลหลักที่ทำไมต้องใช้System.nanoTime()แทนที่จะSystem.currentTimeMillis()เป็นว่าอดีตมีการรับประกันว่าจะเพิ่มขึ้นซ้ำซากจำเจ การลบค่าที่ส่งคืนสองcurrentTimeMillisการเรียกใช้จริงสามารถให้ผลลัพธ์เชิงลบได้เนื่องจากอาจมีการปรับเวลาของระบบโดย NTP daemon บางตัว
Waldheinz

239

ฉันรู้ว่าคำถามนี้ได้รับการทำเครื่องหมายว่าตอบแล้ว แต่ฉันต้องการพูดถึงห้องสมุดสองแห่งที่ช่วยให้เราเขียนเกณฑ์มาตรฐานขนาดเล็ก

Caliper จาก Google

แบบฝึกหัดเริ่มต้น

  1. http://codingjunkie.net/micro-benchmarking-with-caliper/
  2. http://vertexlabs.co.uk/blog/caliper

JMH จาก OpenJDK

แบบฝึกหัดเริ่มต้น

  1. หลีกเลี่ยงการเปรียบเทียบกับ JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/

37
+1 อาจถูกเพิ่มเป็นกฎข้อที่ 8 ของคำตอบที่ยอมรับได้: กฎข้อ 8: เนื่องจากมีหลายสิ่งหลายอย่างที่อาจผิดพลาดได้คุณควรใช้ห้องสมุดที่มีอยู่แทนที่จะพยายามทำด้วยตัวเอง!
assylias

8
@Pangea jmh น่าจะเหนือกว่า Caliper ในปัจจุบันดูเพิ่มเติมที่: groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/…
assylias

86

สิ่งสำคัญสำหรับการเปรียบเทียบ Java คือ:

  • อุ่นเครื่อง JIT ครั้งแรกโดยใช้รหัสหลายครั้งก่อนที่เวลามัน
  • ตรวจสอบให้แน่ใจว่าคุณรันนานพอที่จะสามารถวัดผลลัพธ์ในหน่วยวินาทีหรือ (ดีกว่า) นับสิบวินาที
  • ในขณะที่คุณไม่สามารถโทรSystem.gc()ระหว่างการทำซ้ำได้เป็นความคิดที่ดีที่จะเรียกใช้ระหว่างการทดสอบเพื่อให้การทดสอบแต่ละครั้งหวังว่าจะได้พื้นที่หน่วยความจำที่ "สะอาด" เพื่อใช้งาน (ใช่gc()เป็นคำใบ้มากกว่าการรับประกัน แต่มีโอกาสมากที่มันจะเก็บขยะในประสบการณ์ของฉัน)
  • ฉันชอบที่จะแสดงการทำซ้ำและเวลาและคะแนนของเวลา / การทำซ้ำซึ่งสามารถปรับสัดส่วนเพื่อให้อัลกอริทึม "ดีที่สุด" ได้รับคะแนน 1.0 และอื่น ๆ ได้คะแนนในแบบที่สัมพันธ์กัน ซึ่งหมายความว่าคุณสามารถเรียกใช้อัลกอริธึมทั้งหมดเป็นเวลานานโดยเปลี่ยนแปลงทั้งจำนวนการวนซ้ำและเวลา แต่ยังคงได้รับผลลัพธ์ที่เปรียบเทียบกันได้

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


3
ผู้เยาว์ nitpick: IMO "เพื่อให้การทดสอบแต่ละครั้งได้รับ" ควร "เพื่อให้การทดสอบแต่ละครั้งอาจได้รับ" ตั้งแต่อดีตให้การแสดงผลที่โทรgc มักจะเพิ่มหน่วยความจำที่ไม่ได้ใช้เสมอ
Sanjay T. Sharma

@ SanjayT.Sharma: อืมความตั้งใจก็คือมันทำจริงๆ แม้ว่ามันจะไม่ได้รับประกันอย่างเข้มงวด แต่จริงๆแล้วมันเป็นคำใบ้ที่แข็งแกร่ง จะแก้ไขให้ชัดเจนยิ่งขึ้น
Jon Skeet

1
ฉันไม่เห็นด้วยกับการโทร System.gc () มันคือคำใบ้นั่นคือทั้งหมด ไม่แม้แต่ "หวังว่าจะทำอะไรบางอย่าง" คุณไม่ควรเรียกมันว่าเคย นี่คือการเขียนโปรแกรมไม่ใช่ศิลปะ
gyorgyabraham

13
@gyabraham: ใช่มันเป็นคำใบ้ - แต่มันเป็นสิ่งที่ฉันสังเกตเห็นมักจะได้รับ ดังนั้นหากคุณไม่ต้องการใช้System.gc()คุณจะเสนอให้ลดการรวบรวมขยะในการทดสอบครั้งเดียวเนื่องจากวัตถุที่สร้างในการทดสอบก่อนหน้านี้อย่างไร ในทางปฏิบัติฉันไม่เชื่อฟัง
Jon Skeet

9
@gyabraham: ฉันไม่รู้ว่าคุณหมายถึงอะไรโดย "great fallback" คุณสามารถทำอย่างละเอียดและอีกครั้ง - คุณมีข้อเสนอเพื่อให้ผลลัพธ์ที่ดีกว่า? ฉันพูดอย่างชัดเจนว่าไม่ได้เป็นหลักประกัน ...
Jon Skeet

48

jmhเป็นส่วนเสริมล่าสุดของ OpenJDK และเขียนโดยวิศวกรด้านประสิทธิภาพจาก Oracle ดูคุ้มค่าแน่นอน

jmh เป็นชุดควบคุม Java สำหรับการสร้างการทำงานและการวิเคราะห์เกณฑ์มาตรฐานนาโน / ไมโคร / มาโครที่เขียนด้วยภาษาจาวาและภาษาอื่น ๆ ที่กำหนดเป้าหมาย JVM

ชิ้นที่น่าสนใจมากของข้อมูลที่ฝังอยู่ในความคิดเห็นทดสอบตัวอย่าง

ดูสิ่งนี้ด้วย:


1
ดูเพิ่มเติมที่โพสต์บล็อกนี้: psy-lob-saw.blogspot.com/2013/04/…สำหรับรายละเอียดเกี่ยวกับการเริ่มต้นใช้งาน JMH
Nitsan Wakart

FYI, JEP 230: Microbenchmark สวีทเป็นOpenJDKข้อเสนอบนพื้นฐานนี้Java Microbenchmark เทียม (JMH)โครงการ ไม่ได้ทำการตัดสำหรับ Java 9แต่อาจถูกเพิ่มในภายหลัง
Basil Bourque

23

มาตรฐานควรวัดเวลา / การทำซ้ำหรือการทำซ้ำ / เวลาและทำไม?

ขึ้นอยู่กับสิ่งที่คุณพยายามทดสอบ

หากคุณสนใจเวลาแฝงให้ใช้เวลา / การวนซ้ำและหากคุณสนใจปริมาณงานให้ใช้การทำซ้ำ / เวลา


16

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

for(i=1..n)
  alg1();
for(i=1..n)
  alg2();
for(i=1..n)
  alg2();
for(i=1..n)
  alg1();

ฉันพบความแตกต่างที่เห็นได้ชัดเจน (บางครั้ง 5-10%) ในรันไทม์ของอัลกอริทึมเดียวกันในรอบที่แตกต่างกัน ..

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


5
การเปลี่ยนแปลงคำสั่งโดยธรรมชาติมีผลต่อรันไทม์ การเพิ่มประสิทธิภาพ JVM และผลการแคชจะทำงานที่นี่ ดีกว่าคือ 'อุ่นเครื่อง' การเพิ่มประสิทธิภาพ JVM ทำให้หลายคนทำงานและเป็นมาตรฐานในการทดสอบทุกครั้งใน JVM ที่แตกต่างกัน
Mnementh

15

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


13

มีข้อผิดพลาดที่เป็นไปได้มากมายสำหรับการเขียนไมโครมาตรฐานใน Java

ขั้นแรก: คุณต้องคำนวณด้วยเหตุการณ์ทุกประเภทที่ต้องใช้เวลามากขึ้นหรือน้อยลงในการสุ่ม: การรวบรวมขยะผลการแคช (ของระบบปฏิบัติการสำหรับไฟล์และ CPU สำหรับหน่วยความจำ), IO เป็นต้น

ที่สอง: คุณไม่สามารถเชื่อถือความแม่นยำของเวลาที่วัดได้ในช่วงเวลาสั้น ๆ

ประการที่สาม: JVM ปรับโค้ดของคุณให้เหมาะสมขณะดำเนินการ ดังนั้นการรันที่ต่างกันในอินสแตนซ์ JVM เดียวกันจะกลายเป็นเร็วขึ้นและเร็วขึ้น

คำแนะนำของฉัน: ทำให้การวัดประสิทธิภาพของคุณทำงานได้ในไม่กี่วินาทีซึ่งมีความน่าเชื่อถือมากกว่ารันไทม์มากกว่ามิลลิวินาที อุ่นเครื่อง JVM (หมายถึงการรันเกณฑ์มาตรฐานอย่างน้อยหนึ่งครั้งโดยไม่มีการวัดว่า JVM สามารถรันการปรับให้เหมาะสม) และเรียกใช้เกณฑ์มาตรฐานของคุณหลายครั้ง (อาจจะ 5 ครั้ง) และรับค่ามัธยฐาน รัน micro-benchmark ทุกตัวใน JVM-instance ใหม่ (เรียกใช้ Java มาตรฐานใหม่ทุกครั้ง) มิฉะนั้นเอฟเฟกต์การเพิ่มประสิทธิภาพของ JVM จะส่งผลต่อการทดสอบในภายหลัง อย่าดำเนินการสิ่งต่าง ๆ ที่ไม่ได้ดำเนินการใน warmup-phase (เพราะอาจทำให้คลาสโหลดและการคอมไพล์ซ้ำได้)


8

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

เพราะนี่คือการดำเนินการอาจจะได้เร็วขึ้นมากที่สุดในช่วงของการทำงานของมาตรฐานกว่าการดำเนินการA Bแต่Aอาจมีการแพร่กระจายสูงขึ้นเพื่อให้ได้รับประโยชน์จากการวัดของจะไม่เป็นอย่างมีนัยสำคัญเมื่อเทียบกับAB

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


8

เพื่อเพิ่มคำแนะนำที่ยอดเยี่ยมอื่น ๆ ฉันยังต้องคำนึงถึงสิ่งต่อไปนี้:

สำหรับซีพียูบางตัว (เช่นช่วง Intel Core i5 กับ TurboBoost) อุณหภูมิ (และจำนวนคอร์ที่ใช้งานในปัจจุบันรวมถึงเปอร์เซ็นต์การใช้งาน) จะส่งผลต่อความเร็วสัญญาณนาฬิกา เนื่องจาก CPU มีการโอเวอร์คล็อกแบบไดนามิกสิ่งนี้อาจส่งผลต่อผลลัพธ์ของคุณ ตัวอย่างเช่นหากคุณมีแอปพลิเคชันแบบเธรดเดียวความเร็วสัญญาณนาฬิกาสูงสุด (ด้วย TurboBoost) จะสูงกว่าสำหรับแอปพลิเคชันที่ใช้คอร์ทั้งหมด สิ่งนี้อาจรบกวนการเปรียบเทียบประสิทธิภาพแบบซิงเกิลและแบบมัลติเธรดในบางระบบ โปรดจำไว้ว่าอุณหภูมิและแรงดันยังมีผลต่อระยะเวลาในการรักษาความถี่เทอร์โบ

อาจเป็นสิ่งสำคัญพื้นฐานที่คุณสามารถควบคุมได้โดยตรง: ตรวจสอบให้แน่ใจว่าคุณกำลังวัดสิ่งที่ถูกต้อง! ตัวอย่างเช่นหากคุณกำลังใช้System.nanoTime()เกณฑ์มาตรฐานของรหัสเฉพาะให้โทรไปที่การมอบหมายในสถานที่ที่เหมาะสมเพื่อหลีกเลี่ยงการวัดสิ่งที่คุณไม่สนใจตัวอย่างเช่นอย่าทำ:

long startTime = System.nanoTime();
//code here...
System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds");

ปัญหาคือคุณไม่ได้รับเวลาสิ้นสุดทันทีเมื่อรหัสเสร็จสิ้น ให้ลองทำสิ่งต่อไปนี้แทน:

final long endTime, startTime = System.nanoTime();
//code here...
endTime = System.nanoTime();
System.out.println("Code took "+(endTime-startTime)+"nano seconds");

ใช่เป็นสิ่งสำคัญที่จะไม่ทำงานที่ไม่เกี่ยวข้องภายในภูมิภาคที่มีการกำหนดเวลา แต่ตัวอย่างแรกของคุณยังคงใช้ได้ มีการเรียกเพียงครั้งเดียวเท่านั้นprintlnไม่ใช่บรรทัดส่วนหัวที่แยกจากกันหรือบางอย่างและSystem.nanoTime()ต้องได้รับการประเมินเป็นขั้นตอนแรกในการสร้างสตริง arg สำหรับการโทรนั้น คอมไพเลอร์ไม่สามารถทำอะไรกับสิ่งแรกที่พวกเขาทำไม่ได้กับสิ่งที่สองและไม่มีใครสนับสนุนให้พวกเขาทำงานพิเศษก่อนที่จะบันทึกเวลาหยุด
ปีเตอร์

7

http://opt.sourceforge.net/ Java Micro Benchmark - งานควบคุมที่จำเป็นในการกำหนดลักษณะการเปรียบเทียบประสิทธิภาพของระบบคอมพิวเตอร์บนแพลตฟอร์มที่แตกต่างกัน สามารถใช้เพื่อเป็นแนวทางในการตัดสินใจปรับให้เหมาะสมและเพื่อเปรียบเทียบการใช้งาน Java ที่แตกต่างกัน


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