การรวบรวมที่สร้างรหัสไบต์ชั่วคราว (เช่นกับ Java) แทนที่จะไปที่ "ตลอดทาง" กับรหัสเครื่องมักจะมีความซับซ้อนน้อยลง (และอาจใช้เวลาน้อยลง)?
การรวบรวมที่สร้างรหัสไบต์ชั่วคราว (เช่นกับ Java) แทนที่จะไปที่ "ตลอดทาง" กับรหัสเครื่องมักจะมีความซับซ้อนน้อยลง (และอาจใช้เวลาน้อยลง)?
คำตอบ:
ใช่การรวบรวม Java bytecode นั้นง่ายกว่าการคอมไพล์ไปยังรหัสเครื่อง นี่เป็นเพียงบางส่วนเนื่องจากมีรูปแบบที่จะกำหนดเป้าหมายเพียงรูปแบบเดียว (ตามที่ Mandrill กล่าวถึงแม้ว่าจะเป็นการลดความซับซ้อนของคอมไพเลอร์เท่านั้นไม่ใช่เวลารวบรวม) ส่วนหนึ่งเป็นเพราะ JVM เป็นเครื่องจักรที่ง่ายกว่ามากและสะดวกกว่าโปรแกรมจริง ควบคู่ไปกับภาษา Java การดำเนินการ Java ส่วนใหญ่แมปการดำเนินการ bytecode เดียวในวิธีที่ง่ายมาก อีกเหตุผลที่สำคัญมากคือไม่จริงการเพิ่มประสิทธิภาพเกิดขึ้น ความกังวลเรื่องประสิทธิภาพเกือบทั้งหมดถูกทิ้งไว้ในคอมไพเลอร์ของ JIT (หรือ JVM โดยรวม) ดังนั้นคอมไพเลอร์กลางส่วนกลางทั้งหมดจึงหายไป โดยทั่วไปสามารถเดินผ่าน AST เพียงครั้งเดียวและสร้างลำดับ bytecode สำเร็จรูปสำหรับแต่ละโหนด มี "ค่าใช้จ่ายในการบริหาร" ในการสร้างตารางวิธี, พูลคงที่, ฯลฯ แต่ไม่มีอะไรเทียบกับความซับซ้อนของ, พูด, LLVM
คอมไพเลอร์เป็นเพียงโปรแกรมที่นำไฟล์ข้อความ1ไฟล์ที่มนุษย์สามารถอ่านได้และแปลมันเป็นคำสั่งเลขฐานสองสำหรับเครื่อง หากคุณย้อนกลับไปและคิดเกี่ยวกับคำถามของคุณจากมุมมองทางทฤษฎีความซับซ้อนนั้นค่อนข้างเหมือนกัน อย่างไรก็ตามในระดับการปฏิบัติมากขึ้นคอมไพเลอร์รหัสไบต์จะง่ายขึ้น
ต้องมีขั้นตอนอะไรบ้างในการรวบรวมโปรแกรม
มีเพียงสองความแตกต่างที่แท้จริงระหว่างสอง
โดยทั่วไปโปรแกรมที่มีชุดรวบรวมหลายชุดจะต้องทำการเชื่อมโยงเมื่อรวบรวมรหัสเครื่องและโดยทั่วไปจะไม่มีรหัสไบต์ เราสามารถแยกเส้นขนเกี่ยวกับว่าการเชื่อมโยงเป็นส่วนหนึ่งของการรวบรวมในบริบทของคำถามนี้หรือไม่ ถ้าเป็นเช่นนั้นการรวบรวมโค้ดไบต์จะง่ายกว่าเล็กน้อย อย่างไรก็ตามความซับซ้อนของการเชื่อมโยงถูกสร้างขึ้นในเวลาทำงานเมื่อ VM มีการจัดการกับข้อกังวลหลายประการ (ดูบันทึกย่อของฉันด้านล่าง)
คอมไพเลอร์โค้ดไบต์มีแนวโน้มที่จะไม่ปรับให้เหมาะสมเนื่องจาก VM สามารถทำสิ่งนี้ได้ดีกว่าในทันที (คอมไพเลอร์ JIT เป็นส่วนเสริมมาตรฐานของ VM ในปัจจุบัน)
จากนี้ฉันสรุปได้ว่าคอมไพเลอร์โค้ดไบต์สามารถละเว้นความซับซ้อนของการปรับให้เหมาะสมที่สุดและการเชื่อมโยงทั้งหมดโดยการชะลอทั้งสองไปยัง VM รันไทม์ การคอมไพล์โค้ดแบบไบท์นั้นง่ายกว่าในทางปฏิบัติเพราะมันสามารถทำการคอมไพล์ที่ซับซ้อนหลายอย่างลงบน VM ที่คอมไพเลอร์ของโค้ดเครื่องใช้ในตัวเอง
1 ไม่นับภาษาที่ลึกลับ
ฉันจะบอกว่าช่วยให้การออกแบบคอมไพเลอร์ง่ายขึ้นเนื่องจากการคอมไพล์มักเป็นจาวากับรหัสเครื่องเสมือนทั่วไป นั่นหมายความว่าคุณจะต้องรวบรวมรหัสเพียงครั้งเดียวและมันจะทำงานบน plataform ใด ๆ (แทนที่จะต้องคอมไพล์ในแต่ละเครื่อง) ฉันไม่แน่ใจว่าเวลารวบรวมจะลดลงเพราะคุณสามารถพิจารณาเครื่องเสมือนเช่นเดียวกับเครื่องมาตรฐาน
ในทางกลับกันเครื่องแต่ละเครื่องจะต้องโหลด Java Virtual Machine เพื่อให้สามารถตีความ "รหัสไบต์" (ซึ่งเป็นรหัสเครื่องเสมือนที่เกิดจากการรวบรวมรหัส java) แปลเป็นรหัสเครื่องจริงและเรียกใช้ .
Imo สิ่งนี้ดีสำหรับโปรแกรมที่มีขนาดใหญ่มาก แต่แย่มากสำหรับโปรแกรมขนาดเล็ก (เนื่องจากเครื่องเสมือนเสียหน่วยความจำ)
ความซับซ้อนของการรวบรวมขึ้นอยู่กับช่องว่างทางความหมายระหว่างภาษาต้นฉบับและภาษาเป้าหมายและระดับของการปรับให้เหมาะสมที่คุณต้องการใช้ขณะเชื่อมช่องว่างนี้
ตัวอย่างเช่นการคอมไพล์ซอร์สโค้ด Java ไปยังโค้ดไบต์ JVM ค่อนข้างตรงไปตรงมาเนื่องจากมีเซ็ตย่อยหลักของ Java ที่แม็พโดยตรงกับเซตย่อยของโค้ดไบต์ JVM มีความแตกต่างบางประการ: Java มีลูป แต่ไม่GOTO
, JVM มีGOTO
แต่ไม่มีลูป, Java มีชื่อทั่วไป, JVM ไม่ได้ แต่สิ่งเหล่านั้นสามารถจัดการได้ง่าย (การแปลงจากลูปเป็นการกระโดดแบบมีเงื่อนไขเป็นเรื่องไม่สำคัญ ดังนั้น แต่ก็ยังจัดการได้) มีความแตกต่างอื่น ๆ แต่รุนแรงน้อยกว่า
การคอมไพล์ซอร์สโค้ด Ruby กับโค้ดไบต์ JVM นั้นมีส่วนเกี่ยวข้องมากกว่า (โดยเฉพาะก่อนหน้านี้invokedynamic
และMethodHandles
ถูกนำมาใช้ใน Java 7 หรือมากกว่านั้นใน JVM ข้อมูลจำเพาะรุ่นที่ 3) ใน Ruby สามารถเปลี่ยนเมธอดได้ที่ runtime บน JVM หน่วยของโค้ดที่เล็กที่สุดที่สามารถถูกแทนที่ในขณะรันไทม์เป็นคลาสดังนั้นเมธอด Ruby ต้องถูกคอมไพล์ไม่ใช่เมธอด JVM แต่เป็นคลาส JVM การแจกจ่ายเมธอด Ruby ไม่ตรงกับการจัดส่งเมธอด JVM และก่อนหน้าinvokedynamic
นี้ไม่มีวิธีฉีดกลไกการจัดส่งเมธอดของคุณเองลงใน JVM ทับทิมมีการต่อเนื่องและ coroutines แต่ JVM ขาดสิ่งอำนวยความสะดวกในการดำเนินการเหล่านั้น (JVM ของGOTO
ถูก จำกัด ให้กระโดดข้ามเป้าหมายภายในเมธอด) JVM แบบดั้งเดิมเท่านั้นที่ควบคุมการไหลที่จะมีประสิทธิภาพเพียงพอที่จะใช้การดำเนินการต่อเนื่องเป็นข้อยกเว้นและใช้เธรด coroutines ซึ่งทั้งสองมีความหนามากในขณะที่วัตถุประสงค์ทั้งหมดของ coroutines คือ เบามาก
OTOH, การคอมไพล์ซอร์สโค้ด Ruby กับ Rubinius byte code หรือ YARV byte code เป็นเรื่องไม่สำคัญอีกครั้งเนื่องจากทั้งสองอย่างนั้นได้รับการออกแบบอย่างชัดเจนว่าเป็นเป้าหมายการรวบรวมสำหรับ Ruby (แม้ว่า Rubinius ยังถูกใช้สำหรับภาษาอื่นเช่น CoffeeScript และชื่อเสียงที่สุด) .
ในทำนองเดียวกันการคอมไพล์โค้ดเนทีฟ x86 ไปยังโค้ดไบต์ JVM ไม่ใช่การส่งต่อตรงอีกครั้งมีช่องว่างความหมายที่ค่อนข้างใหญ่
Haskell เป็นอีกตัวอย่างที่ดี: ด้วย Haskell มีคอมไพเลอร์พร้อมประสิทธิภาพการผลิตที่แข็งแกร่งและมีประสิทธิภาพสูงในอุตสาหกรรมซึ่งผลิตรหัสเครื่องพื้นเมือง x86 แต่จนถึงวันนี้ไม่มีคอมไพเลอร์ที่ใช้งานได้สำหรับ JVM หรือ CLI เพราะความหมาย ช่องว่างใหญ่มากจนซับซ้อนมากที่จะเชื่อมมัน ดังนั้นนี่คือตัวอย่างที่การคอมไพล์ไปยังรหัสเครื่องดั้งเดิมนั้นซับซ้อนน้อยกว่าการคอมไพล์โค้ด JVM หรือ CIL นี่เป็นเพราะรหัสเครื่องดั้งเดิมมีระดับพื้นฐานที่ต่ำกว่ามาก ( GOTO
, ตัวชี้, ... ) ที่สามารถ "บังคับ" เพื่อทำสิ่งที่คุณต้องการได้ง่ายกว่าการใช้วิธีดั้งเดิมหรือการเรียกใช้ข้อยกเว้น
ดังนั้นอาจกล่าวได้ว่าระดับภาษาเป้าหมายที่สูงกว่าคือยิ่งใกล้เคียงกับความหมายของภาษาต้นฉบับเพื่อลดความซับซ้อนของคอมไพเลอร์
ในทางปฏิบัติ JVM ส่วนใหญ่ในปัจจุบันเป็นซอฟต์แวร์ที่ซับซ้อนมากทำการรวบรวม JIT (ดังนั้น bytecode จึงถูกแปลเป็นรหัสเครื่องจักรโดย JVM แบบไดนามิก )
ดังนั้นในขณะที่การคอมไพล์จากซอร์สโค้ด Java (หรือซอร์สโค้ด Clojure) ถึงโค้ดไบต์ JVM นั้นง่ายกว่าจริง ๆ แต่ตัว JVM เองก็ทำการแปลที่ซับซ้อนเป็นรหัสเครื่อง
ความจริงที่ว่าการแปล JIT ภายใน JVM นั้นเป็นแบบไดนามิกช่วยให้ JVM สามารถมุ่งเน้นไปที่ส่วนที่เกี่ยวข้องที่สุดของไบต์ ในทางปฏิบัติแล้ว JVM ส่วนใหญ่จะเพิ่มประสิทธิภาพส่วนที่ร้อนแรงที่สุด (เช่นวิธีที่เรียกมากที่สุดหรือบล็อกพื้นฐานที่ถูกเรียกใช้มากที่สุด) ของ JVM bytecode
ฉันไม่แน่ใจว่าความซับซ้อนรวมของคอมไพเลอร์ JVM + Java to bytecode นั้นน้อยกว่าความซับซ้อนของคอมไพเลอร์ก่อนเวลาอย่างมาก
ขอให้สังเกตว่าคอมไพเลอร์แบบดั้งเดิมส่วนใหญ่ (เช่นGCCหรือClang / LLVM ) กำลังแปลงอินพุต C (หรือ C ++ หรือ Ada, ... ) เป็นซอร์สโค้ดภายใน ( Gimpleสำหรับ GCC, LLVMสำหรับ Clang) ซึ่งค่อนข้างคล้ายกับ bytecode บางส่วน จากนั้นพวกเขาก็เปลี่ยนรูปแบบการเป็นตัวแทนภายใน (การปรับให้เหมาะสมเป็นอันดับแรกนั่นคือการเพิ่มประสิทธิภาพ GCC ส่วนใหญ่จะใช้ Gimple เป็นอินพุตและสร้าง Gimple เป็นเอาท์พุทต่อมาเปล่งแอสเซมเบลอร์หรือรหัสเครื่องจักรจากมัน) เป็นรหัสวัตถุ
BTW ด้วย GCC ล่าสุด (โดยเฉพาะอย่างยิ่งlibgccjit ) และโครงสร้างพื้นฐาน LLVM คุณสามารถใช้ภาษาเหล่านี้เพื่อรวบรวมภาษาอื่น ๆ (หรือภาษาของคุณเอง) เป็นตัวแทน Gimple หรือ LLVM ภายในของพวกเขาจากนั้นได้รับประโยชน์จากความสามารถในการเพิ่มประสิทธิภาพมากมาย ส่วนท้ายของคอมไพเลอร์เหล่านี้