[... ] (ได้รับในสภาพแวดล้อมไมโครวินาที) [... ]
เพิ่มขึ้นในเวลาเพียงไม่กี่วินาทีหากเราวนซ้ำหลายล้านต่อหลายพันล้านสิ่งของ เซสชัน vtune / micro-optimization ส่วนตัวจาก C ++ (ไม่มีการปรับปรุงอัลกอริทึม):
T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds
ทุกอย่างนอกเหนือจาก "มัลติเธรด", "SIMD" (เขียนด้วยลายมือเพื่อเอาชนะคอมไพเลอร์) และการเพิ่มประสิทธิภาพแพตช์ 4 วาเลนซ์คือการปรับแต่งหน่วยความจำระดับไมโคร รหัสต้นฉบับที่เริ่มต้นจากเวลาเริ่มต้นของ 32 วินาทีนั้นได้รับการปรับปรุงให้ดีขึ้นเล็กน้อย (ความซับซ้อนของอัลกอริทึมที่ดีที่สุดในทางทฤษฎี) และนี่คือเซสชันล่าสุด รุ่นดั้งเดิมยาวนานก่อนเซสชันล่าสุดนี้ใช้เวลาประมวลผลนานกว่า 5 นาที
การเพิ่มประสิทธิภาพประสิทธิภาพหน่วยความจำสามารถช่วยได้บ่อยครั้งทุกที่ตั้งแต่หลาย ๆ ครั้งไปจนถึงลำดับของขนาดในบริบทเธรดเดียวและอื่น ๆ ในบริบทแบบมัลติเธรด (ประโยชน์ของหน่วยความจำที่มีประสิทธิภาพมักจะทวีคูณด้วยหลายเธรด
ในความสำคัญของการเพิ่มประสิทธิภาพ Micro
ฉันรู้สึกกระวนกระวายใจเล็กน้อยจากความคิดนี้ว่าการปรับขนาดเล็กแบบไมโครทำให้เสียเวลา ฉันยอมรับว่ามันเป็นคำแนะนำทั่วไปที่ดี แต่ไม่ใช่ทุกคนที่ทำมันอย่างไม่ถูกต้องโดยยึดตามลางสังหรณ์และไสยศาสตร์มากกว่าการวัด ทำอย่างถูกต้องไม่จำเป็นต้องส่งผลกระทบต่อไมโคร ถ้าเราใช้ Embree kernel (raytracing kernel) ของ Intel และทดสอบเฉพาะ scalar BVH ที่พวกเขาเขียน (ไม่ใช่ ray packet ซึ่งยากต่อการอธิบาย) จากนั้นลองเอาชนะประสิทธิภาพของโครงสร้างข้อมูลนั้น ประสบการณ์ต่ำต้อยแม้สำหรับทหารผ่านศึกที่ใช้ในการทำโปรไฟล์และปรับแต่งโค้ดมานานหลายทศวรรษ และทั้งหมดนี้เป็นเพราะมีการใช้การเพิ่มประสิทธิภาพแบบไมโคร วิธีแก้ปัญหาของพวกเขาสามารถประมวลผลรังสีกว่าร้อยล้านต่อวินาทีเมื่อฉันเห็นผู้เชี่ยวชาญด้านอุตสาหกรรมที่ทำงานใน raytracing ที่สามารถ '
ไม่มีวิธีใดที่จะนำ BVH ไปใช้งานได้อย่างตรงไปตรงมาด้วยการโฟกัสแบบอัลกอริธึมและรับการฉายรังสีหลักมากกว่าร้อยล้านครั้งต่อวินาทีจากการเปรียบเทียบกับคอมไพเลอร์ที่ปรับให้เหมาะสม (แม้แต่ ICC ของ Intel เอง) สิ่งที่ตรงไปตรงมามักจะไม่ได้รับรังสีถึงหนึ่งล้านต่อวินาที ต้องใช้โซลูชันคุณภาพระดับมืออาชีพเพื่อให้ได้รังสีเพียงไม่กี่ล้านครั้งต่อวินาที ใช้การเพิ่มประสิทธิภาพไมโครระดับ Intel เพื่อให้ได้รับรังสีมากกว่าร้อยล้านต่อวินาที
อัลกอริทึม
ฉันคิดว่าการเพิ่มประสิทธิภาพขนาดเล็กไม่สำคัญตราบใดที่ประสิทธิภาพไม่สำคัญในระดับนาทีถึงวินาทีเช่นหรือชั่วโมงเป็นนาที หากเราใช้อัลกอริทึมที่น่ากลัวเช่นการจัดเรียงฟองและใช้มันมากกว่าการป้อนข้อมูลจำนวนมากเป็นตัวอย่างแล้วเปรียบเทียบกับการดำเนินการผสานแบบพื้นฐานขั้นพื้นฐานในอดีตอาจใช้เวลาหลายเดือนในการประมวลผลซึ่งอาจใช้เวลา 12 นาทีหลัง ของสมการเชิงซ้อนกำลังสองเชิงเส้นและเชิงเส้นตรง
ความแตกต่างระหว่างเดือนและนาทีน่าจะทำให้คนส่วนใหญ่แม้จะไม่ได้ทำงานในสาขาที่สำคัญต่อประสิทธิภาพพิจารณาว่าเวลาดำเนินการไม่สามารถยอมรับได้หากต้องการให้ผู้ใช้รอเป็นเดือนเพื่อรับผล
ในขณะเดียวกันถ้าเราเปรียบเทียบการจัดเรียงแบบผสานที่ไม่ได้ปรับขนาดเล็กและตรงไปตรงกับ quicksort (ซึ่งไม่ได้เหนือกว่าขั้นตอนวิธีการจัดเรียงแบบผสานและเสนอการปรับปรุงระดับจุลภาคสำหรับการอ้างอิงในพื้นที่เท่านั้น) 15 วินาทีเมื่อเทียบกับ 12 นาที การทำให้ผู้ใช้รอ 12 นาทีอาจเป็นที่ยอมรับอย่างสมบูรณ์ (เวลาพักดื่มกาแฟ)
ฉันคิดว่าความแตกต่างนี้อาจไม่สำคัญกับคนส่วนใหญ่ระหว่างพูด 12 นาทีและ 15 วินาทีและนั่นคือเหตุผลที่การเพิ่มประสิทธิภาพขนาดเล็กมักจะไร้ประโยชน์เพราะมันมักจะชอบความแตกต่างระหว่างนาทีและวินาทีไม่ใช่นาทีและเดือน อีกเหตุผลที่ฉันคิดว่ามันไร้ประโยชน์ก็คือมันมักจะใช้กับพื้นที่ที่ไม่สำคัญ: พื้นที่เล็ก ๆ ที่ไม่ได้เป็นวงแหวนและสำคัญซึ่งทำให้เกิดความแตกต่าง 1% ที่น่าสงสัย (ซึ่งอาจเป็นเพียงเสียงรบกวน) แต่สำหรับผู้ที่สนใจเกี่ยวกับความแตกต่างของเวลาเหล่านี้และยินดีที่จะวัดและทำสิ่งที่ถูกต้องฉันคิดว่ามันควรค่าแก่ความสนใจอย่างน้อยแนวคิดพื้นฐานของลำดับชั้นหน่วยความจำ (โดยเฉพาะระดับบนที่เกี่ยวข้องกับความผิดพลาดของหน้าเว็บ .
Java ออกจากพื้นที่มากมายสำหรับการเพิ่มประสิทธิภาพขนาดเล็กที่ดี
ว้าขอโทษด้วยการพูดจาโผงผางแบบนั้น:
"ความมหัศจรรย์" ของ JVM ขัดขวางอิทธิพลของโปรแกรมเมอร์ที่มีต่อการปรับให้เหมาะสมขนาดจิ๋วใน Java หรือไม่?
เล็กน้อย แต่ไม่มากเท่าที่คนอื่นคิดว่าคุณทำถูกต้อง ตัวอย่างเช่นหากคุณกำลังทำการประมวลผลภาพในโค้ดเนทีฟพร้อมด้วยลายมือ SIMD, multithreading และการปรับแต่งหน่วยความจำ (รูปแบบการเข้าถึงและอาจเป็นตัวแทนขึ้นอยู่กับอัลกอริทึมการประมวลผลภาพ) มันง่ายที่จะกระทืบพิกเซลหลายร้อยล้านพิกเซลต่อวินาที บิตพิกเซล RGBA (ช่องสี 8 บิต) และบางครั้งอาจพันล้านต่อวินาที
เป็นไปไม่ได้ที่จะเข้าใกล้ทุกแห่งใน Java ถ้าคุณบอกว่าสร้างPixel
วัตถุ (เพียงอย่างเดียวนี้จะขยายขนาดของพิกเซลจาก 4 ไบต์ถึง 16 ใน 64- บิต)
แต่คุณอาจเข้าใกล้ได้มากขึ้นถ้าคุณหลีกเลี่ยงPixel
วัตถุใช้อาร์เรย์จำนวนไบต์และทำโมเดลImage
วัตถุ Java ยังคงมีความสามารถอยู่ที่นั่นหากคุณเริ่มใช้อาร์เรย์ของข้อมูลเก่าแบบธรรมดา ฉันเคยลองสิ่งเหล่านี้มาก่อนใน Java และรู้สึกประทับใจมากหากคุณไม่ได้สร้างวัตถุเล็ก ๆ น้อย ๆ ทุกที่ที่ใหญ่กว่าปกติ 4 เท่า (เช่นใช้int
แทนInteger
) และเริ่มสร้างโมเดลอินเทอร์เฟซจำนวนมากเช่นImage
อินเตอร์เฟสไม่ใช่Pixel
อินเตอร์เฟส ฉันยังอยากบอกว่า Java สามารถเทียบเคียงประสิทธิภาพ C ++ ได้หากคุณวนซ้ำข้อมูลเก่าธรรมดาไม่ใช่วัตถุ ( float
เช่นอาร์เรย์ขนาดใหญ่เช่นไม่ใช่Float
)
บางทีสิ่งที่สำคัญกว่าขนาดหน่วยความจำก็คืออาเรย์ของint
การรับประกันการแสดงที่ต่อเนื่องกัน อาร์เรย์ของInteger
ไม่ Contiguity มักจะเป็นสิ่งจำเป็นสำหรับสถานที่อ้างอิงเนื่องจากมันหมายถึงองค์ประกอบหลายอย่าง (เช่น: 16 ints
) ทั้งหมดสามารถใส่ลงในแคชบรรทัดเดียว ในขณะเดียวกันInteger
อาจติดที่ใดที่หนึ่งในหน่วยความจำที่มีหน่วยความจำรอบไม่เกี่ยวข้องเท่านั้นที่จะมีพื้นที่ของหน่วยความจำที่โหลดลงในสายแคชเท่านั้นที่จะใช้จำนวนเต็มเดียวก่อนที่จะขับไล่เมื่อเทียบกับ 16 จำนวนเต็ม แม้ว่าเราจะโชคดีและสิ่งรอบตัวIntegers
อยู่ติดกันในหน่วยความจำเราสามารถใส่ 4 เข้าไปในแคชไลน์ที่สามารถเข้าถึงได้ก่อนการขับไล่เนื่องจากมีInteger
ขนาดใหญ่กว่า 4 เท่าและนั่นเป็นสถานการณ์ที่ดีที่สุด
และมีการปรับแต่งขนาดเล็กให้เหมาะสมเนื่องจากเรารวมเป็นหนึ่งเดียวกันภายใต้สถาปัตยกรรม / ลำดับชั้นของหน่วยความจำเดียวกัน รูปแบบการเข้าถึงหน่วยความจำไม่ว่าคุณจะใช้ภาษาใดแนวคิดทั่วไปเช่นการเรียง / บล็อกการวนซ้ำอาจถูกนำมาใช้บ่อยกว่าใน C หรือ C ++ แต่พวกเขาได้รับประโยชน์จากจาวามาก
ฉันเพิ่งอ่านใน C ++ บางครั้งการเรียงลำดับของข้อมูลสมาชิกสามารถให้การเพิ่มประสิทธิภาพ [... ]
ลำดับของสมาชิกข้อมูลโดยทั่วไปไม่สำคัญใน Java แต่ส่วนใหญ่เป็นสิ่งที่ดี ใน C และ C ++ การรักษาลำดับของข้อมูลสมาชิกมักมีความสำคัญต่อเหตุผลของ ABI เพื่อให้คอมไพเลอร์ไม่ยุ่งกับเรื่องนั้น นักพัฒนามนุษย์ที่ทำงานจะต้องระมัดระวังในการทำสิ่งต่าง ๆ เช่นจัดเรียงข้อมูลสมาชิกตามลำดับจากมากไปหาน้อยที่สุดเพื่อหลีกเลี่ยงการสูญเสียความจำในการขยาย ด้วย Java นั้น JIT สามารถเรียงลำดับสมาชิกให้คุณได้อย่างรวดเร็วเพื่อให้แน่ใจว่ามีการจัดเรียงที่เหมาะสมในขณะที่ลดช่องว่างภายในลงดังนั้นหากเป็นกรณีนี้โดยอัตโนมัติสิ่งที่โปรแกรมเมอร์ C และ C ++ โดยเฉลี่ยสามารถทำได้ไม่ดี ซึ่งไม่เพียง แต่เป็นการสูญเสียความทรงจำ แต่บ่อยครั้งที่สูญเสียความเร็วโดยการเพิ่มความก้าวหน้าระหว่างโครงสร้าง AoS โดยไม่จำเป็นและทำให้แคชหายไปมากขึ้น) มัน' เป็นสิ่งที่หุ่นยนต์มากที่จะจัดเรียงเขตข้อมูลใหม่เพื่อลดช่องว่างภายในเพื่อให้มนุษย์ไม่ต้องจัดการกับเรื่องนั้น เวลาเดียวที่การจัดเรียงฟิลด์อาจสำคัญในแบบที่มนุษย์ต้องการทราบการจัดเรียงที่เหมาะสมคือถ้าวัตถุมีขนาดใหญ่กว่า 64 ไบต์และเรากำลังจัดเรียงเขตข้อมูลตามรูปแบบการเข้าถึง (ไม่ใช่ช่องว่างภายในที่เหมาะสม) - ในกรณีนี้ อาจเป็นความพยายามของมนุษย์มากขึ้น (ต้องมีการทำความเข้าใจเส้นทางที่สำคัญซึ่งบางอย่างเป็นข้อมูลที่คอมไพเลอร์ไม่สามารถคาดการณ์ได้โดยไม่ทราบว่าผู้ใช้จะทำอะไรกับซอฟต์แวร์)
ถ้าไม่ใช่คนอาจยกตัวอย่างว่าคุณสามารถใช้กลอุบายอะไรใน Java (นอกเหนือจากการคอมไพล์แฟล็กเรียบง่าย)
ความแตกต่างที่ยิ่งใหญ่ที่สุดสำหรับฉันในแง่ของการเพิ่มประสิทธิภาพความคิดระหว่าง Java และ C ++ คือ C ++ อาจอนุญาตให้คุณใช้ออบเจ็กต์เล็กน้อย (เล็ก) มากกว่า Java ในสถานการณ์ที่มีประสิทธิภาพสูง ตัวอย่างเช่น C ++ สามารถล้อมค่าจำนวนเต็มไปยังคลาสที่ไม่มีค่าใช้จ่ายใด ๆ (เปรียบเทียบกับทุกที่) Java จะต้องมีค่าใช้จ่ายที่เมตาดาต้าชี้สไตล์ + จัดตำแหน่ง padding ต่อวัตถุซึ่งเป็นเหตุผลที่Boolean
มีขนาดใหญ่กว่าboolean
( แต่ในการแลกเปลี่ยนการให้สิทธิประโยชน์สม่ำเสมอของการสะท้อนและความสามารถในการแทนที่ฟังก์ชันใด ๆ ที่ไม่ทำเครื่องหมายเป็นfinal
สำหรับทุก UDT เดียว)
มันง่ายกว่านิดหน่อยใน C ++ ที่จะควบคุมความต่อเนื่องของเลย์เอาต์ของหน่วยความจำในฟิลด์ที่ไม่เป็นเนื้อเดียวกัน (เช่น interleaving floats และ ints เป็นหนึ่งอาเรย์ผ่านโครงสร้าง / คลาส) เนื่องจากตำแหน่งเชิงพื้นที่มักจะหายไป ใน Java เมื่อจัดสรรวัตถุผ่าน GC
... แต่บ่อยครั้งที่โซลูชันที่มีประสิทธิภาพสูงสุดมักจะแยกสิ่งเหล่านั้นออกไปและใช้รูปแบบการเข้าถึง SOA ผ่านอาร์เรย์ข้อมูลเก่าธรรมดาที่ต่อเนื่องกัน ดังนั้นสำหรับพื้นที่ที่ต้องการประสิทธิภาพสูงสุดกลยุทธ์ในการปรับเลย์เอาท์หน่วยความจำระหว่าง Java และ C ++ มักจะเหมือนกันและมักจะทำให้คุณต้องทำลายอินเทอร์เฟซแบบเชิงวัตถุขนาดเล็กเพื่อสนับสนุนอินเตอร์เฟสสไตล์คอลเลกชันที่สามารถทำสิ่งต่างๆเช่นร้อน / การแยกเขตข้อมูลเย็นตัวแทนของ SOA ฯลฯ ตัวแทนที่ไม่เหมือนกัน AoSoA ดูเหมือนว่าเป็นไปไม่ได้ใน Java (เว้นแต่คุณจะใช้อาร์เรย์ดิบเป็นไบต์หรืออะไรทำนองนั้น) แต่กรณีเหล่านี้เป็นกรณีที่หายากซึ่งทั้งคู่รูปแบบการเข้าถึงแบบลำดับและแบบสุ่มจำเป็นต้องรวดเร็วในขณะเดียวกันก็มีประเภทของฟิลด์ผสมสำหรับฟิลด์ร้อนพร้อมกัน สำหรับฉันแล้วความแตกต่างในกลยุทธ์การปรับให้เหมาะสม (ในระดับทั่วไป) ระหว่างสองสิ่งนี้เป็นสิ่งที่สงสัยหากคุณเข้าถึงประสิทธิภาพสูงสุด
ความแตกต่างแตกต่างกันไปเล็กน้อยถ้าคุณแค่เอื้อมไปถึงประสิทธิภาพ "ดี" - ไม่สามารถทำอะไรได้มากนักกับวัตถุขนาดเล็กเช่นInteger
vs. vs. int
อาจเป็น PITA ได้อีกเล็กน้อยโดยเฉพาะอย่างยิ่งเมื่อมันโต้ตอบกับยาชื่อสามัญ . เป็นบิตยากที่จะเป็นเพียงแค่การสร้างโครงสร้างข้อมูลหนึ่งทั่วไปเป็นเป้าหมายการเพิ่มประสิทธิภาพของกลางใน Java ที่ผลงานint
, float
ฯลฯ ขณะที่หลีกเลี่ยงผู้ UDTs ที่ใหญ่กว่าและมีราคาแพง แต่ส่วนใหญ่มักจะพื้นที่การปฏิบัติงานที่สำคัญจะต้องมีมือกลิ้งโครงสร้างข้อมูลของคุณเอง ปรับเพื่อจุดประสงค์ที่เฉพาะเจาะจงอยู่แล้วดังนั้นจึงเป็นเรื่องน่ารำคาญสำหรับโค้ดที่พยายามให้มีประสิทธิภาพที่ดี แต่ไม่ใช่ประสิทธิภาพสูงสุด
ค่าใช้จ่ายวัตถุ
โปรดทราบว่าค่าใช้จ่ายวัตถุ Java (ข้อมูลเมตาและการสูญเสียพื้นที่เชิงพื้นที่และการสูญเสียตำแหน่งชั่วคราวหลังจากรอบ GC เริ่มต้น) มักจะมีขนาดใหญ่สำหรับสิ่งที่มีขนาดเล็กมาก (เช่นint
vs. Integer
) ซึ่งถูกเก็บไว้เป็นล้านในโครงสร้างข้อมูลบางส่วน ส่วนใหญ่ต่อเนื่องและเข้าถึงได้ในลูปแน่นมาก ดูเหมือนว่าจะมีความไวมากเกี่ยวกับเรื่องนี้ดังนั้นฉันควรชี้แจงว่าคุณไม่ต้องการที่จะกังวลเกี่ยวกับค่าใช้จ่ายวัตถุสำหรับวัตถุขนาดใหญ่เช่นภาพเพียงแค่วัตถุจิ๋วเช่นพิกเซลเดียว
หากใครรู้สึกสงสัยเกี่ยวกับส่วนนี้ฉันขอแนะนำให้สร้างมาตรฐานระหว่างการสรุปการสุ่มints
ล้านครั้งIntegers
และการสุ่มแบบสุ่มและการทำเช่นนี้ซ้ำ ๆ (การIntegers
จะสับเปลี่ยนหน่วยความจำหลังจากรอบ GC เริ่มต้น)
Ultimate Trick: การออกแบบส่วนต่อประสานที่ทำให้ห้องมีประสิทธิภาพสูงสุด
ดังนั้นเคล็ดลับ Java ขั้นสุดท้ายที่ฉันเห็นถ้าคุณจัดการกับสถานที่ที่รับภาระอย่างหนักเหนือวัตถุขนาดเล็ก (เช่น: a Pixel
, 4-vector, 4x4 matrix, a Particle
, อาจเป็นไปได้Account
ถ้ามันมีขนาดเล็กเพียงไม่กี่ เขตข้อมูล) คือการหลีกเลี่ยงการใช้วัตถุสำหรับสิ่งเล็ก ๆ เหล่านี้และใช้อาร์เรย์ (อาจถูกล่ามโซ่ไว้ด้วยกัน) ของข้อมูลเก่าธรรมดา วัตถุกลายเป็นแล้วอินเตอร์เฟซคอลเลกชันเช่นImage
, ParticleSystem
, Accounts
คอลเลกชันของการฝึกอบรมหรือเวกเตอร์ ฯลฯ แต่ละคนสามารถเข้าถึงได้โดยดัชนีเช่นนี้ยังเป็นหนึ่งในเทคนิคการออกแบบที่ดีที่สุดใน C และ C ++ ตั้งแต่แม้จะไม่ได้ว่าค่าใช้จ่ายวัตถุขั้นพื้นฐานและ หน่วยความจำที่แยกจากกันการสร้างแบบจำลองอินเทอร์เฟซที่ระดับอนุภาคเดียวช่วยป้องกันการแก้ปัญหาที่มีประสิทธิภาพสูงสุด