Dường như có ít nhất hai câu hỏi khác nhau có thể có ở đây. Một là thực sự về trình biên dịch nói chung, với Java về cơ bản chỉ là một ví dụ về thể loại này. Cái khác là cụ thể hơn đối với Java các mã byte cụ thể mà nó sử dụng.
Trình biên dịch nói chung
Trước tiên chúng ta hãy xem xét câu hỏi chung: tại sao trình biên dịch sẽ sử dụng một số biểu diễn trung gian trong quá trình biên dịch mã nguồn để chạy trên một số bộ xử lý cụ thể?
Giảm độ phức tạp
Một câu trả lời khá đơn giản: nó chuyển đổi một vấn đề O (N * M) thành vấn đề O (N + M).
Nếu chúng tôi cung cấp các ngôn ngữ nguồn N và các mục tiêu M và mỗi trình biên dịch hoàn toàn độc lập, thì chúng tôi cần trình biên dịch N * M để dịch tất cả các ngôn ngữ nguồn đó sang tất cả các mục tiêu đó (trong đó "mục tiêu" là một sự kết hợp của một bộ xử lý và HĐH).
Tuy nhiên, nếu tất cả các trình biên dịch đồng ý về một biểu diễn trung gian chung, thì chúng ta có thể có các giao diện N của trình biên dịch dịch các ngôn ngữ nguồn sang biểu diễn trung gian và các trình biên dịch M kết thúc dịch đại diện trung gian sang một mục tiêu phù hợp cho một mục tiêu cụ thể.
Phân đoạn vấn đề
Vẫn tốt hơn, nó phân tách vấn đề thành hai hoặc nhiều miền độc quyền. Những người biết / quan tâm đến thiết kế ngôn ngữ, phân tích cú pháp và những thứ như thế có thể tập trung vào giao diện trình biên dịch, trong khi những người biết về tập lệnh, thiết kế bộ xử lý và những thứ như thế có thể tập trung vào mặt sau.
Vì vậy, ví dụ, được cung cấp một cái gì đó như LLVM, chúng tôi có rất nhiều giao diện cho các ngôn ngữ khác nhau. Chúng tôi cũng có back-end cho rất nhiều bộ xử lý khác nhau. Một anh chàng ngôn ngữ có thể viết một giao diện mới cho ngôn ngữ của mình và nhanh chóng hỗ trợ rất nhiều mục tiêu. Một anh chàng bộ xử lý có thể viết một back-end mới cho mục tiêu của mình mà không phải xử lý thiết kế ngôn ngữ, phân tích cú pháp, v.v.
Tách các trình biên dịch thành một mặt trước và mặt sau, với một biểu diễn trung gian để giao tiếp giữa hai trình biên dịch không phải là bản gốc với Java. Đó là cách làm khá phổ biến trong một thời gian dài (dù sao trước khi Java xuất hiện).
Mô hình phân phối
Trong phạm vi mà Java đã thêm bất cứ điều gì mới về mặt này, thì đó là trong mô hình phân phối. Đặc biệt, mặc dù các trình biên dịch đã được tách thành các phần đầu và cuối trong nội bộ trong một thời gian dài, chúng thường được phân phối dưới dạng một sản phẩm. Ví dụ: nếu bạn đã mua trình biên dịch Microsoft C, bên trong nó có "C1" và "C2", tương ứng là mặt trước và mặt sau - nhưng thứ bạn mua chỉ là "Microsoft C" bao gồm cả hai các phần (với một "trình điều khiển trình biên dịch" phối hợp các hoạt động giữa hai phần). Mặc dù trình biên dịch được xây dựng thành hai phần, nhưng đối với một nhà phát triển bình thường sử dụng trình biên dịch thì đó chỉ là một thứ duy nhất được dịch từ mã nguồn sang mã đối tượng, không có gì hiển thị ở giữa.
Thay vào đó, Java đã phân phối front-end trong Bộ công cụ phát triển Java và back-end trong Máy ảo Java. Mọi người dùng Java đều có trình biên dịch back-end để nhắm mục tiêu bất kỳ hệ thống nào anh ta đang sử dụng. Các nhà phát triển Java đã phân phối mã ở định dạng trung gian, vì vậy khi người dùng tải nó, JVM đã làm bất cứ điều gì cần thiết để thực thi nó trên máy cụ thể của họ.
Tiền lệ
Lưu ý rằng mô hình phân phối này cũng không hoàn toàn mới. Ví dụ, hệ thống P của UCSD hoạt động tương tự: mặt trước của trình biên dịch tạo ra mã P và mỗi bản sao của hệ thống P bao gồm một máy ảo thực hiện những gì cần thiết để thực thi mã P trên mục tiêu cụ thể 1 đó .
Mã byte Java
Mã byte Java khá giống với mã P. Đó là hướng dẫn cơ bản cho một máy khá đơn giản. Máy đó được dự định là một bản tóm tắt của các máy hiện có, do đó khá dễ dàng để dịch nhanh chóng sang hầu hết mọi mục tiêu cụ thể. Dễ dịch là từ rất quan trọng vì mục đích ban đầu là giải thích mã byte, giống như P-System đã thực hiện (và, vâng, đó chính xác là cách các triển khai ban đầu hoạt động).
Điểm mạnh
Mã byte Java dễ dàng cho một giao diện người biên dịch sản xuất. Nếu (ví dụ) bạn có một cây khá điển hình đại diện cho một biểu thức, nó thường khá dễ dàng để duyệt qua cây và tạo mã khá trực tiếp từ những gì bạn tìm thấy ở mỗi nút.
Mã byte Java khá nhỏ gọn - trong hầu hết các trường hợp, nhỏ gọn hơn nhiều so với mã nguồn hoặc mã máy cho hầu hết các bộ xử lý điển hình (và, đặc biệt đối với hầu hết các bộ xử lý RISC, như SPARC mà Sun bán khi họ thiết kế Java). Điều này đặc biệt quan trọng vào thời điểm đó, bởi vì một mục đích chính của Java là hỗ trợ các applet - mã được nhúng trong các trang web sẽ được tải xuống trước khi thực thi - tại thời điểm hầu hết mọi người truy cập chúng tôi qua modem qua các đường dây điện thoại vào khoảng 28.8 kilobit mỗi giây (mặc dù, tất nhiên, vẫn còn khá nhiều người sử dụng modem cũ hơn, chậm hơn).
Những điểm yếu
Điểm yếu lớn của mã byte Java là chúng không đặc biệt biểu cảm. Mặc dù chúng có thể diễn đạt các khái niệm hiện diện trong Java khá tốt, nhưng chúng không hoạt động gần như tốt để thể hiện các khái niệm không phải là một phần của Java. Tương tự như vậy, trong khi thật dễ dàng để thực thi mã byte trên hầu hết các máy, thì điều đó lại khó hơn nhiều theo cách tận dụng tối đa bất kỳ máy cụ thể nào.
Ví dụ, một thói quen khá thú vị là nếu bạn thực sự muốn tối ưu hóa mã byte Java, về cơ bản, bạn thực hiện một số kỹ thuật đảo ngược để dịch ngược chúng từ biểu diễn giống như mã máy và biến chúng trở lại thành các lệnh SSA (hoặc một cái gì đó tương tự) 2 . Sau đó, bạn thao tác các hướng dẫn SSA để thực hiện tối ưu hóa, sau đó dịch từ đó sang thứ gì đó nhắm vào kiến trúc mà bạn thực sự quan tâm. Tuy nhiên, ngay cả với quy trình khá phức tạp này, một số khái niệm xa lạ với Java cũng khó diễn đạt đến mức khó dịch từ một số ngôn ngữ nguồn sang mã máy chạy (thậm chí gần) tối ưu trên hầu hết các máy điển hình.
Tóm lược
Nếu bạn đang hỏi về lý do sử dụng các biểu diễn trung gian nói chung, hai yếu tố chính là:
- Giảm vấn đề O (N * M) thành vấn đề O (N + M) và
- Phá vỡ vấn đề thành nhiều phần dễ quản lý hơn.
Nếu bạn đang hỏi về các chi tiết cụ thể của mã byte Java và lý do tại sao họ chọn đại diện cụ thể này thay vì một số khác, thì tôi sẽ nói rằng câu trả lời chủ yếu trở lại với mục đích ban đầu của họ và các hạn chế của web tại thời điểm đó , dẫn đến các ưu tiên sau:
- Đại diện nhỏ gọn.
- Nhanh chóng và dễ dàng để giải mã và thực hiện.
- Nhanh chóng và dễ dàng để thực hiện trên hầu hết các máy phổ biến.
Có thể đại diện cho nhiều ngôn ngữ hoặc thực hiện tối ưu trên nhiều mục tiêu khác nhau là các ưu tiên thấp hơn nhiều (nếu chúng được coi là ưu tiên).
- Vậy tại sao hệ thống P chủ yếu bị lãng quên? Chủ yếu là một tình huống giá cả. Hệ thống P được bán khá nhiều trên Apple II, Commodore Super, v.v. Khi PC của IBM ra mắt, hệ thống P là một hệ điều hành được hỗ trợ, nhưng MS-DOS có giá thấp hơn (theo quan điểm của hầu hết mọi người, về cơ bản được ném vào miễn phí) và nhanh chóng có sẵn nhiều chương trình hơn, vì đó là những gì Microsoft và IBM (trong số những người khác) đã viết cho.
- Ví dụ, đây là cách Soot hoạt động.