ทำความเข้าใจกับความแตกต่าง: ล่ามแบบดั้งเดิมคอมไพเลอร์ JIT ล่าม JIT และคอมไพเลอร์ AOT


130

ฉันพยายามที่จะเข้าใจความแตกต่างระหว่างล่ามแบบดั้งเดิมคอมไพเลอร์ JIT ล่าม JIT และคอมไพเลอร์ AOT

ล่ามเป็นเพียงเครื่อง (เสมือนหรือทางกายภาพ) ที่ดำเนินการคำสั่งในภาษาคอมพิวเตอร์บางภาษา ในแง่นี้ JVM เป็นล่ามและ CPU จริงเป็นล่าม

การคอมไพล์ล่วงหน้าเวลาหมายถึงการคอมไพล์โค้ดเป็นภาษาบางภาษาก่อนดำเนินการ (ตีความ) มัน

อย่างไรก็ตามฉันไม่แน่ใจเกี่ยวกับคำจำกัดความที่แน่นอนของผู้แปล JIT และล่าม JIT

ตามคำนิยามผมอ่าน, JIT รวบรวมเป็นเพียงการรวบรวมรหัสเพียงก่อนที่จะแปลความหมายของมัน

ดังนั้นโดยทั่วไปการรวบรวม JIT ก็คือการรวบรวม AOT เสร็จสิ้นก่อนดำเนินการ (ตีความ) ใช่ไหม

และล่าม JIT เป็นโปรแกรมที่มีทั้งคอมไพเลอร์ JIT และล่ามและรวบรวมรหัส (JITs มัน) ก่อนที่มันจะตีความมัน?

โปรดอธิบายความแตกต่าง


4
ทำไมทำให้คุณเชื่อว่ามีความแตกต่างระหว่าง "ผู้แปล JIT" และ "ล่าม JIT"? มันเป็นคำสองคำที่ต่างกันสำหรับสิ่งเดียวกัน แนวคิดโดยรวมของ JIT เหมือนกัน แต่มีเทคนิคการใช้งานที่หลากหลายซึ่งไม่สามารถแบ่งออกเป็น "คอมไพเลอร์" กับ "ล่าม" ได้อย่างหลากหลาย
Greg Hewgill

2
อ่าน wikipages ในการรวบรวม Just In Time , คอมไพเลอร์ AOT , คอมไพเลอร์ , ล่าม , bytecodeและหนังสือของ Queinnec เสียงกระเพื่อมเป็นชิ้นเล็ก ๆ
Basile Starynkevitch

คำตอบ:


198

ภาพรวม

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

คอมไพเลอร์จากXไปYเป็นโปรแกรม (หรือเครื่องหรือเพียงแค่ชนิดของกลไกโดยทั่วไปบางคน) ที่แปลโปรแกรมใด ๆหน้าจากภาษาบางXลงในโปรแกรมเทียบเท่าหมายP 'ในภาษาบางYในลักษณะที่หมาย ของโปรแกรมจะถูกเก็บไว้คือว่าการตีความพี 'กับล่ามสำหรับYจะให้ผลลัพธ์ที่เหมือนกันและมีผลเช่นเดียวกับการตีความพีกับล่ามสำหรับX (โปรดทราบว่าXและYอาจเป็นภาษาเดียวกัน)

เงื่อนไขข้างหน้าของเวลา (มหาชน)และJust-In-Time (JIT)หมายถึงเมื่อสะสมที่เกิดขึ้น: ความ "เวลา" ที่อ้างถึงในคำเหล่านั้นคือ "ไทม์" คือคอมไพเลอร์ JIT รวบรวมโปรแกรมตามที่มันเป็น ทำงานเป็นคอมไพเลอร์ทอทรวบรวมโปรแกรมก่อนที่จะทำงาน โปรดทราบว่าสิ่งนี้ต้องการให้คอมไพเลอร์ JIT จากภาษาXเป็นภาษาYต้องทำงานร่วมกับล่ามภาษาY อย่างใดมิฉะนั้นจะไม่มีทางเรียกใช้โปรแกรม (ตัวอย่างเช่นคอมไพเลอร์ JIT ที่รวบรวม JavaScript กับรหัสเครื่อง x86 ไม่สมเหตุสมผลหากไม่มี x86 CPU มันรวบรวมโปรแกรมในขณะที่กำลังทำงาน แต่ไม่มี CPU x86 โปรแกรมจะไม่ทำงาน)

โปรดทราบว่าความแตกต่างนี้ไม่สมเหตุสมผลสำหรับล่าม: ล่ามจะเรียกใช้โปรแกรม แนวคิดของล่าม AOT ที่รันโปรแกรมก่อนที่จะรันหรือล่าม JIT ที่รันโปรแกรมในขณะที่กำลังรันอยู่นั้นไร้สาระ

ดังนั้นเรามี:

  • คอมไพเลอร์ AOT: คอมไพล์ก่อนเรียกใช้
  • คอมไพเลอร์ JIT: คอมไพล์ขณะรัน
  • ล่าม: ทำงาน

คอมไพเลอร์ JIT

ภายในครอบครัวของคอมไพเลอร์ JIT ที่มีความแตกต่างกันจำนวนมากยังคงเป็นไปเมื่อว่าพวกเขารวบรวมความถี่ที่และสิ่งที่ละเอียด

คอมไพเลอร์ JIT ใน CLR ของ Microsoft เช่นรวบรวมรหัสเพียงครั้งเดียว (เมื่อโหลด) และรวบรวมแอสเซมบลีทั้งหมดในเวลาเดียวกัน คอมไพเลอร์อื่น ๆ อาจรวบรวมข้อมูลในขณะที่โปรแกรมกำลังทำงานและคอมไพล์รหัสซ้ำหลาย ๆ ครั้งเมื่อมีข้อมูลใหม่ที่จะช่วยให้พวกเขาสามารถปรับปรุงได้ดีขึ้น คอมไพเลอร์ JIT บางตัวมีความสามารถในการยกเลิกการออปติไมซ์โค้ด ตอนนี้คุณอาจถามตัวเองว่าทำไมคนเราถึงอยากทำเช่นนั้น? การเพิ่มประสิทธิภาพช่วยให้คุณสามารถดำเนินการเพิ่มประสิทธิภาพที่ก้าวร้าวมากซึ่งอาจไม่ปลอดภัยจริง ๆ : ถ้าปรากฎว่าคุณก้าวร้าวมากเกินไปคุณสามารถกลับออกมาอีกครั้งในขณะที่คอมไพเลอร์ JIT ที่ไม่สามารถยกเลิกการปรับให้เหมาะสม การเพิ่มประสิทธิภาพเชิงรุกในสถานที่แรก

คอมไพเลอร์ JIT อาจรวบรวมโค้ดสแตติกบางส่วนในหนึ่ง go (หนึ่งโมดูลหนึ่งคลาสหนึ่งฟังก์ชันหนึ่งเมธอด, …โดยทั่วไปเรียกว่าmethod-at-a-time JIT เป็นต้น) หรือพวกมันอาจติดตามไดนามิก การเรียกใช้โค้ดเพื่อค้นหาร่องรอยแบบไดนามิก(โดยทั่วไปคือลูป) ที่พวกเขาจะรวบรวม (สิ่งเหล่านี้เรียกว่าการติดตาม JIT)

การรวมล่ามและคอมไพเลอร์

ล่ามและคอมไพเลอร์อาจรวมกันเป็นเอ็นจินการประมวลผลภาษาเดียว มีสองสถานการณ์ทั่วไปที่ทำสิ่งนี้

รวมทอทคอมไพเลอร์จากXไปYกับล่ามสำหรับY ที่นี่โดยทั่วไปXเป็นภาษาระดับสูงกว่าบางส่วนได้รับการปรับให้เหมาะสมสำหรับการอ่านโดยมนุษย์ในขณะที่Yเป็นภาษาที่กะทัดรัด (มักจะเป็นแบบไบต์) เหมาะสำหรับการตีความโดยเครื่อง ตัวอย่างเช่นเอ็นจิ้นการทำงานของ CPython Python มีคอมไพเลอร์ AOT ที่รวบรวมซอร์สโค้ดของ Python กับ CPython bytecode และล่ามที่ตีความ CPTC bytecode เอ็นจินการเรียกใช้ YARV Ruby มีคอมไพเลอร์ AOT ที่คอมไพล์ซอร์สโค้ด Ruby กับ YARV bytecode และล่ามที่ตีความ YARV bytecode ทำไมคุณต้องการทำเช่นนั้น? ทับทิมและงูหลามมีทั้งมากระดับสูงและภาษาค่อนข้างซับซ้อนดังนั้นครั้งแรกที่เรารวบรวมไว้เป็นภาษาที่ง่ายต่อการแยกและง่ายต่อการตีความแล้วตีความว่าภาษา

อีกวิธีในการรวมล่ามและคอมไพเลอร์เป็นเอ็นจิ้นการประมวลผลแบบผสม ที่นี่เรา "ผสม" สอง "โหมด" ของการดำเนินการเดียวกันภาษาด้วยกันคือล่ามสำหรับXและเรียบเรียง JIT จากXไปY (ดังนั้นความแตกต่างที่นี่ก็คือในกรณีข้างต้นเรามี "ขั้นตอน" หลายอย่างพร้อมกับคอมไพเลอร์รวบรวมโปรแกรมแล้วป้อนผลลัพธ์เป็นล่ามที่นี่เรามีการทำงานสองแบบเคียงข้างกันในภาษาเดียวกัน ) รหัสที่คอมไพล์โดยคอมไพเลอร์มีแนวโน้มที่จะทำงานได้เร็วกว่าโค้ดที่ดำเนินการโดยล่าม แต่จริงๆแล้วการคอมไพล์โค้ดครั้งแรกต้องใช้เวลา (โดยเฉพาะอย่างยิ่งถ้าคุณต้องการปรับให้โค้ดทำงานหนักมากเร็วจริงๆใช้เวลาเยอะ ) ดังนั้นเพื่อเชื่อมโยงเวลานี้ที่ผู้รวบรวม JIT กำลังยุ่งอยู่กับการรวบรวมรหัสล่ามก็สามารถเริ่มต้นเรียกใช้รหัสแล้วและเมื่อ JIT รวบรวมเสร็จแล้วเราสามารถสลับการดำเนินการไปยังรหัสรวบรวม นี่หมายความว่าเราได้รับประสิทธิภาพที่ดีที่สุดเท่าที่จะเป็นไปได้ของโค้ดที่คอมไพล์ แต่เราไม่ต้องรอให้การคอมไพล์เสร็จสิ้นและแอปพลิเคชันของเราจะเริ่มทำงานทันที (แม้ว่าจะไม่เร็วเท่าที่ควร)

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

นอกจากนี้ยังเป็นไปได้และในความเป็นจริงเรื่องธรรมดาที่จะรวมทั้งสองวิธี: สองขั้นตอนแรกกับการเป็นคอมไพเลอร์ทอทที่รวบรวมXไปYและเฟสที่สองเป็นเครื่องยนต์แบบผสมว่าทั้งสองตีความYและรวบรวมYเพื่อZ เอ็นจินการดำเนินการ Rubinius Ruby ทำงานด้วยวิธีนี้ตัวอย่างเช่นมีคอมไพเลอร์ AOT ที่รวบรวมซอร์สโค้ด Ruby ไปยัง Rubinius bytecode และเอ็นจิ้นโหมดผสมที่ตีความคำสั่งแรกเริ่มของ Rubinius และเมื่อรวบรวมข้อมูลบางอย่างแล้ว รหัสเครื่อง

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


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

1
@SebastianRedl ด้วย CPython คุณสามารถเรียกใช้python -m compileall .หรือโหลดโมดูลครั้งเดียว แม้ในกรณีหลังเนื่องจากไฟล์ยังคงอยู่และถูกนำกลับมาใช้ใหม่หลังจากการเรียกใช้ครั้งแรกมันจึงดูเหมือนว่า AOT
พอลเดรเปอร์

คุณมีเอกสารอ้างอิงใด ๆ สำหรับการอ่านเพิ่มเติมหรือไม่? ฉันต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับ V8
Vince Panuccio

@VincePanuccio อ้างอิงโพสต์เต็มโค๊ดเจนและเพลาข้อเหวี่ยงคอมไพเลอร์ที่ได้รับตั้งแต่แทนที่ คุณสามารถค้นหาเกี่ยวกับพวกเขาออนไลน์
eush77

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