Câu trả lời:
Có, biên dịch sang mã byte Java dễ hơn biên dịch mã máy. Điều này một phần là do chỉ có một định dạng để nhắm mục tiêu (như Mandrill đề cập, mặc dù điều này chỉ làm giảm độ phức tạp của trình biên dịch, không phải thời gian biên dịch), một phần vì JVM là một máy đơn giản hơn và thuận tiện hơn để lập trình so với CPU thực - vì nó được thiết kế trong song song với ngôn ngữ Java, hầu hết các thao tác Java ánh xạ tới chính xác một thao tác mã byte theo cách rất đơn giản. Một lý do rất quan trọng khác là thực tế khôngtối ưu hóa diễn ra. Hầu như tất cả các mối quan tâm về hiệu quả đều được dành cho trình biên dịch JIT (hoặc toàn bộ JVM), do đó toàn bộ phần giữa của các trình biên dịch thông thường sẽ biến mất. Về cơ bản, nó có thể đi qua AST một lần và tạo các chuỗi mã byte sẵn sàng cho mỗi nút. Có một số "chi phí quản trị" của việc tạo các bảng phương thức, các nhóm hằng số, v.v. nhưng điều đó không là gì so với sự phức tạp của LLVM.
Trình biên dịch đơn giản là một chương trình lấy 1 tệp văn bản có thể đọc được của con người và chuyển chúng thành các hướng dẫn nhị phân cho máy. Nếu bạn lùi lại một bước và suy nghĩ về câu hỏi của bạn từ góc độ lý thuyết này, thì sự phức tạp là gần như nhau. Tuy nhiên, ở mức độ thực tế hơn, trình biên dịch mã byte đơn giản hơn.
Những bước rộng nào phải xảy ra để biên dịch một chương trình?
Chỉ có hai sự khác biệt thực sự giữa hai.
Nói chung, một chương trình có nhiều đơn vị biên dịch yêu cầu liên kết khi biên dịch thành mã máy và thường không có mã byte. Người ta có thể chia tóc về việc liên kết là một phần của việc biên dịch trong bối cảnh của câu hỏi này. Nếu vậy, việc biên dịch mã byte sẽ đơn giản hơn một chút. Tuy nhiên, sự phức tạp của liên kết được tạo ra trong thời gian chạy khi nhiều mối quan tâm liên kết được xử lý bởi VM (xem ghi chú của tôi dưới đây).
Trình biên dịch mã byte có xu hướng không tối ưu hóa nhiều vì VM có thể thực hiện việc này tốt hơn một cách nhanh chóng (trình biên dịch JIT là một bổ sung khá chuẩn cho máy ảo hiện nay).
Từ đó tôi kết luận rằng các trình biên dịch mã byte có thể bỏ qua sự phức tạp của hầu hết các tối ưu hóa và tất cả các liên kết, trì hoãn cả hai điều này với thời gian chạy VM. Trình biên dịch mã byte đơn giản hơn trong thực tế vì chúng chuyển nhiều phức tạp lên VM mà trình biên dịch mã máy tự đảm nhận.
1 Không tính ngôn ngữ bí truyền
Tôi muốn nói rằng đơn giản hóa thiết kế trình biên dịch vì quá trình biên dịch luôn là Java thành mã máy ảo chung. Điều đó cũng có nghĩa là bạn chỉ cần biên dịch mã một lần và nó sẽ chạy trên bất kỳ plataform nào (thay vì phải biên dịch trên mỗi máy). Tôi không chắc chắn nếu thời gian biên dịch sẽ thấp hơn bởi vì bạn có thể coi máy ảo giống như một máy chuẩn.
Mặt khác, mỗi máy sẽ phải tải Máy ảo Java để có thể hiểu "mã byte" (là mã máy ảo được tạo từ quá trình biên dịch mã java), dịch nó sang mã máy thực tế và chạy nó .
Imo này tốt cho các chương trình rất lớn nhưng rất tệ cho các chương trình nhỏ (vì máy ảo là một sự lãng phí bộ nhớ).
Độ phức tạp của quá trình biên dịch phụ thuộc phần lớn vào khoảng cách ngữ nghĩa giữa ngôn ngữ nguồn và ngôn ngữ đích và mức độ tối ưu hóa bạn muốn áp dụng trong khi thu hẹp khoảng cách này.
Ví dụ, việc biên dịch mã nguồn Java thành mã byte JVM tương đối đơn giản, vì có một tập hợp con cốt lõi của Java ánh xạ trực tiếp khá nhiều vào một tập hợp con của mã byte JVM. Có một số khác biệt: Java có các vòng lặp nhưng không GOTO
, JVM có GOTO
nhưng không có các vòng lặp, Java có các tổng quát, JVM thì không, nhưng chúng có thể dễ dàng xử lý (việc chuyển đổi từ các vòng lặp sang các bước nhảy có điều kiện là tầm thường, loại bỏ một chút vì vậy, nhưng vẫn có thể quản lý được). Có sự khác biệt khác nhưng ít nghiêm trọng hơn.
Việc biên dịch mã nguồn Ruby thành mã byte JVM có liên quan nhiều hơn (đặc biệt là trước đây invokedynamic
và MethodHandles
được giới thiệu trong Java 7, hay chính xác hơn là trong Phiên bản thứ 3 của đặc tả JVM). Trong Ruby, các phương thức có thể được thay thế trong thời gian chạy. Trên JVM, đơn vị mã nhỏ nhất có thể được thay thế trong thời gian chạy là một lớp, do đó, các phương thức Ruby phải được biên dịch không phải là các phương thức JVM mà là các lớp JVM. Công văn phương thức Ruby không khớp với công văn phương thức JVM và trước đó invokedynamic
, không có cách nào để đưa cơ chế gửi phương thức của riêng bạn vào JVM. Ruby có các phần tiếp theo và coroutines, nhưng JVM thiếu các phương tiện để thực hiện chúng. (Của JVMGOTO
bị hạn chế để nhảy các mục tiêu trong phương thức.) Dòng điều khiển duy nhất mà JVM có, đủ mạnh để thực hiện các phần tiếp theo là ngoại lệ và để thực hiện các luồng coroutines, cả hai đều rất nặng, trong khi toàn bộ mục đích của coroutines là rất nhẹ
OTOH, biên dịch mã nguồn Ruby thành mã byte Rubinius hoặc mã byte YARV một lần nữa là tầm thường, vì cả hai đều được thiết kế rõ ràng làm mục tiêu biên dịch cho Ruby (mặc dù Rubinius cũng đã được sử dụng cho các ngôn ngữ khác như CoffeeScript và nổi tiếng nhất là Fancy) .
Tương tự, việc biên dịch mã gốc x86 thành mã byte JVM không đơn giản, một lần nữa, có một khoảng cách ngữ nghĩa khá lớn.
Haskell là một ví dụ điển hình khác: với Haskell, có một số trình biên dịch sẵn sàng sản xuất sức mạnh công nghiệp hiệu suất cao, sản xuất mã máy x86 riêng, nhưng cho đến ngày nay, không có trình biên dịch làm việc nào cho JVM hoặc CLI, vì ngữ nghĩa khoảng cách quá lớn đến nỗi rất phức tạp để thu hẹp nó. Vì vậy, đây là một ví dụ trong đó việc biên dịch thành mã máy gốc thực sự ít phức tạp hơn so với biên dịch sang mã byte JVM hoặc CIL. Điều này là do mã máy gốc có các nguyên hàm cấp thấp hơn ( GOTO
, con trỏ, Trực tiếp) có thể dễ dàng "ép buộc" hơn để làm những gì bạn muốn hơn là sử dụng các nguyên hàm cấp cao hơn như gọi phương thức hoặc ngoại lệ.
Vì vậy, người ta có thể nói rằng ngôn ngữ đích càng cao thì càng phải phù hợp với ngữ nghĩa của ngôn ngữ nguồn để giảm độ phức tạp của trình biên dịch.
Trong thực tế, hầu hết các JVM ngày nay đều là phần mềm rất phức tạp, thực hiện quá trình biên dịch JIT (do đó mã byte được dịch tự động sang mã máy bởi JVM).
Vì vậy, trong khi việc biên dịch từ mã nguồn Java (hoặc mã nguồn Clojure) sang mã byte JVM thực sự đơn giản hơn, thì chính JVM đang thực hiện dịch mã phức tạp sang mã máy.
Thực tế là bản dịch JIT bên trong JVM này là động cho phép JVM tập trung vào các phần có liên quan nhất của mã byte. Nói một cách thực tế, hầu hết JVM tối ưu hóa nhiều phần nóng nhất (ví dụ: các phương thức được gọi nhiều nhất hoặc các khối cơ bản được thực thi nhiều nhất) của mã byte JVM.
Tôi không chắc rằng độ phức tạp kết hợp của trình biên dịch JVM + Java với mã byte là ít hơn đáng kể so với độ phức tạp của các trình biên dịch trước thời hạn.
Cũng lưu ý rằng hầu hết các trình biên dịch truyền thống (như GCC hoặc Clang / LLVM ) đang chuyển đổi mã nguồn đầu vào C (hoặc C ++ hoặc Ada, ...) thành một biểu diễn bên trong ( Gimple cho GCC, LLVM cho Clang) khá giống với một số mã byte. Sau đó, họ đang chuyển đổi các biểu diễn bên trong (đầu tiên tối ưu hóa nó thành chính nó, tức là hầu hết các tối ưu hóa GCC đều lấy Gimple làm đầu vào và tạo ra Gimple làm đầu ra; sau đó phát ra trình biên dịch mã hoặc mã máy từ nó) thành mã đối tượng.
BTW, với cơ sở hạ tầng GCC (đáng chú ý là libgccjit ) và LLVM gần đây , bạn có thể sử dụng chúng để biên dịch một số ngôn ngữ khác (hoặc của riêng bạn) thành các biểu diễn Gimple hoặc LLVM bên trong của chúng, sau đó thu lợi từ nhiều khả năng tối ưu hóa của trung cấp & back- phần cuối của các trình biên dịch.