2013-09-25 46 views
6

Chúng tôi hiện đang làm việc trên phần biên dịch JIT của việc thực thi Java Virtual Machine của chính chúng ta. Ý tưởng của chúng tôi bây giờ là làm một bản dịch đơn giản của bytecode Java đã cho thành các mã opcodes, ghi chúng vào bộ nhớ thực thi và GỌI ngay để bắt đầu phương thức.Biên dịch trong thời gian thực của Java bytecode

Giả sử mã Java được sẽ là:

int a = 13372338; 
int b = 32 * a; 
return b; 

Bây giờ, các phương pháp sau đây đã được thực hiện (giả định rằng bộ nhớ được bắt đầu từ 0x1000 & giá trị lợi nhuận kỳ vọng trong eax):

0x1000: first local variable - accessible via [eip - 8] 
0x1004: second local variable - accessible via [eip - 4] 
0x1008: start of the code - accessible via [eip] 

Java bytecode | Assembler code (NASM syntax) 
--------------|------------------------------------------------------------------ 
       | // start 
       | mov edx, eip 
       | push ebx 
       |   
       | // method content 
ldc   | mov eax, 13372338 
       | push eax 
istore_0  | pop eax 
       | mov [edx - 8], eax 
bipush  | push 32 
iload_0  | mov eax, [edx - 8] 
       | push eax 
imul   | pop ebx 
       | pop eax 
       | mul ebx 
       | push eax 
istore_1  | pop eax 
       | mov [edx - 4], eax 
iload_1  | mov eax, [edx - 4] 
       | push eax 
ireturn  | pop eax 
       |   
       | // end 
       | pop ebx 
       | ret 

Điều này chỉ đơn giản là sử dụng ngăn xếp giống như máy ảo tự nó. Các câu hỏi liên quan đến giải pháp này là:

  • Phương pháp này có khả thi không?
  • Thậm chí có thể triển khai tất cả các hướng dẫn Java theo cách này không? Làm thế nào có thể những thứ như athrow/instanceof và các lệnh tương tự được dịch?
+1

Điều này phải làm gì với C++? –

+0

Vâng máy ảo và do đó việc biên dịch thực tế và các cuộc gọi của các phương thức được tạo ra được thực hiện trong C++, có thể không phải là thẻ phù hợp nhất nhưng cũng quan trọng. – maxdev

+0

Không có mã nào được đăng là C++. Bạn đang hỏi làm thế nào để thực hiện Java bytecode trong assembly. –

Trả lời

5

Phương pháp biên dịch này dễ dàng chuẩn bị và hoạt động, đồng thời ít nhất phải loại bỏ phí giải thích. Nhưng nó dẫn đến số lượng mã khá lớn và hiệu suất khá khủng khiếp. Một vấn đề lớn là nó dịch chuyển các hoạt động ngăn xếp 1: 1, mặc dù máy đích (x86) là một máy tính đăng ký . Như bạn có thể thấy trong đoạn trích mà bạn đã đăng (cũng như bất kỳ mã nào khác), điều này luôn dẫn đến một số thao tác ngăn xếp cho mỗi hoạt động duy nhất, vì vậy nó sử dụng thanh ghi - toàn bộ ISA - về không hiệu quả nhất có thể.

Bạn có thể cũng hỗ trợ luồng điều khiển phức tạp như ngoại lệ. Nó không phải là rất khác nhau từ việc thực hiện nó trong một thông dịch viên. Nếu bạn muốn có hiệu suất tốt, bạn không muốn thực hiện công việc mỗi khi bạn nhập hoặc thoát khỏi khối try. Có các lược đồ để tránh điều này, được sử dụng bởi cả C++ và các JVM khác (từ khóa: xử lý ngoại lệ bằng không hoặc chi phí bảng). Đây là khá phức tạp và phức tạp để thực hiện, hiểu và gỡ lỗi, vì vậy bạn nên đi với một lựa chọn đơn giản hơn đầu tiên. Hãy ghi nhớ điều đó.

Đối với mã được tạo: Tối ưu hóa đầu tiên, bạn sẽ cần chuyển đổi hoạt động ngăn xếp thành ba mã địa chỉ hoặc một số biểu diễn khác sử dụng thanh ghi. Có một số giấy tờ về điều này và triển khai thực hiện điều này, vì vậy tôi sẽ không xây dựng trừ khi bạn muốn tôi. Sau đó, tất nhiên, bạn cần ánh xạ các thanh ghi ảo này lên thanh ghi vật lý. Đăng ký cấp phát là một trong những chủ đề được nghiên cứu nhiều nhất trong các cấu trúc trình biên dịch, và có ít nhất nửa tá chẩn đoán hợp lý có hiệu quả và đủ nhanh để sử dụng trong trình biên dịch JIT. Một ví dụ trên đỉnh đầu của tôi là phân bổ đăng ký quét tuyến tính (đặc biệt tạo ra để biên dịch JIT). Ngoài ra, hầu hết các trình biên dịch JIT tập trung vào hiệu suất của mã được tạo ra (trái ngược với biên dịch nhanh) sử dụng một hoặc nhiều định dạng trung gian và tối ưu hóa các chương trình trong biểu mẫu này. Điều này về cơ bản là chạy bộ tối ưu hóa trình biên dịch mill, bao gồm các cựu chiến binh như tuyên truyền liên tục, đánh số giá trị, tái kết hợp, chuyển đổi mã bất biến vòng lặp, v.v. - những thứ này không chỉ đơn giản để hiểu và thực hiện, chúng cũng được mô tả trong ba mươi năm văn học đến và bao gồm sách giáo khoa và Wikipedia.

Mã bạn sẽ nhận được ở trên sẽ khá tốt đối với mã straigt-line sử dụng nguyên thủy, mảng và trường đối tượng. Tuy nhiên, bạn sẽ không thể tối ưu hóa các cuộc gọi phương thức. Mỗi phương thức là ảo, có nghĩa là nội tuyến hoặc thậm chí là các cuộc gọi phương thức di chuyển (ví dụ trong vòng lặp) về cơ bản là không thể ngoại trừ trong các trường hợp rất đặc biệt. Bạn đã đề cập rằng đây là cho một hạt nhân. Nếu bạn có thể chấp nhận bằng cách sử dụng một tập hợp con của Java mà không cần nạp lớp động, bạn có thể làm tốt hơn (nhưng nó sẽ không chuẩn) bằng cách giả sử JIT biết tất cả các lớp. Sau đó, bạn có thể, ví dụ, phát hiện các lớp lá (hoặc các phương pháp thông thường hơn mà không bao giờ được overriden) và nội tuyến những người.

Nếu bạn cần tải lớp động, nhưng mong đợi nó hiếm, bạn cũng có thể làm tốt hơn, mặc dù nó cần nhiều công việc hơn. Ưu điểm là cách tiếp cận này tổng quát hóa những thứ khác, như loại bỏ hoàn toàn các câu lệnh khai thác gỗ. Ý tưởng cơ bản là chuyên mã dựa trên một số giả định (ví dụ: rằng static này không thay đổi hoặc không có lớp mới nào được tải), sau đó tối ưu hóa nếu các giả định đó bị vi phạm. Điều này có nghĩa là đôi khi bạn sẽ phải biên dịch lại mã khi đang chạy (đây là cứng, nhưng không phải là không thể).

Nếu bạn đi xa hơn con đường này, kết luận lôgic của nó là biên dịch JIT theo dấu vết, mà được áp dụng cho Java, nhưng AFAIK không vượt trội so với các trình biên dịch JIT dựa trên phương thức. Nó hiệu quả hơn khi bạn phải thực hiện hàng chục hoặc hàng trăm giả định để có được mã tốt, vì nó xảy ra với các ngôn ngữ rất năng động.

+0

+1, Câu trả lời tuyệt vời! Trong một phút, tôi muốn bắt đầu viết một Jitter :). Hai câu hỏi mặc dù - bạn nói rằng đề xuất của OP vẫn còn tốt hơn so với giải thích. Điều đó khác gì so với thông dịch viên thực sự làm gì? Thứ hai, bạn đề cập đến const-tuyên truyền, LICM, vv - bạn có thể trỏ đến một danh sách các tối ưu hóa JIT chỉ có sẵn @runtime? những gì sẽ làm cho JIT thực sự tỏa sáng trên các trình biên dịch tĩnh? – Leeor

+0

@ Đề xuất của OP sẽ tốt hơn một trình thông dịch đơn giản ở chỗ nó loại bỏ công văn bytecode và mã liên quan mà thường phải được thực thi giữa các thao tác riêng lẻ. Những hoạt động đó, tức là kết thúc bận rộn của mã, phần thực sự * làm điều gì đó *, rất giống như trong một thông dịch viên. – delnan

+0

@Leeor Câu hỏi thứ hai của bạn là chủ đề của một cuộc chiến thánh thiện, và khá khó trả lời nói chung.Những gì nhiều hơn hoặc ít hơn chắc chắn là trong * một số trường hợp, hầu hết các kiến ​​thức cần thiết để tối ưu hóa ngay cả * speculatively * (có, AOT trình biên dịch có thể làm điều đó quá) là không có sẵn tại thời gian biên dịch nhưng có sẵn tại thời gian chạy. Việc tối ưu hóa nào bị ảnh hưởng bởi điều này và tầm quan trọng của việc này, thay đổi và phải chịu tranh luận nóng. – delnan

2

Một số ý kiến ​​về trình biên dịch JIT của bạn (Tôi hy vọng tôi không viết những điều "delnan" đã viết):

comments Generic

Tôi chắc chắn "thật" trình biên dịch JIT làm việc tương tự như của bạn một. Tuy nhiên bạn có thể làm một số tối ưu hóa (ví dụ: "mov eax, nnn" và "push eax" có thể được thay thế bằng "push nnn").

Bạn nên lưu trữ các biến cục bộ trên ngăn xếp; thường "ebp" được sử dụng làm con trỏ cục bộ:

push ebx 
push ebp 
sub esp, 8 // 2 variables with 4 bytes each 
mov ebp, esp 
// Now local variables are addressed using [ebp+0] and [ebp+4] 
    ... 
pop ebp 
pop ebx 
ret 

Điều này là cần thiết vì chức năng có thể đệ quy. Việc lưu trữ một biến tại một vị trí cố định (tương đối so với EIP) sẽ làm cho các biến hoạt động giống như các biến "tĩnh". (Tôi giả sử bạn không biên dịch một hàm này nhiều lần trong trường hợp của một hàm đệ quy.)

try/catch

Thực hiện try/catch trình biên dịch JIT của bạn không chỉ phải nhìn vào Java Bytecode mà còn ở thông tin Try/Catch được lưu trữ trong một thuộc tính riêng biệt trong lớp Java. Try/catch có thể được thực hiện theo cách sau:

// push all useful registers (= the ones, that must not be destroyed) 
push eax 
push ebp 
    ... 
    // push the "catch" pointers 
push dword ptr catch_pointer 
push dword ptr catch_stack 
    // set the "catch" pointers 
mov catch_stack,esp 
mov dword ptr catch_pointer, my_catch 
    ... // some code 
    // Here some "throw" instruction... 
push exception 
jmp dword ptr catch_pointer 
    ... //some code 
    // End of the "try" section: Pop all registers 
pop dword_ptr catch_stack 
    ... 
pop eax 
    ... 
    // The "catch" block 
my_catch: 
pop ecx // pop the Exception from the stack 
mov esp, catch_stack // restore the stack 
    // Now restore all registers (same as at the end of the "try" section) 
pop dword_ptr catch_stack 
    ... 
pop eax 
push ecx // push the Exception to the stack 

Trong một môi trường multi-thread mỗi thread đòi hỏi riêng catch_stack và catch_pointer biến của nó!

loại ngoại lệ cụ thể có thể được xử lý bằng cách sử dụng một "instanceof" theo cách sau:

try { 
    // some code 
} catch(MyException1 ex) { 
    // code 1 
} catch(MyException2 ex) { 
    // code 2 
} 

... thực sự là biên soạn như thế này ...:

try { 
    // some code 
} catch(Throwable ex) { 
    if(ex instanceof MyException1) { 
     // code 1 
    } 
    else if(ex instanceof MyException2) { 
     // code 2 
    } 
    else throw(ex); // not handled! 
} 

Objects

Một trình biên dịch JIT của một máy ảo Java đơn giản không hỗ trợ các đối tượng (và mảng) sẽ là khá dễ dàng nhưng các đối tượng trong Java làm cho máy ảo rất phức tạp.

Đối tượng được lưu trữ đơn giản dưới dạng con trỏ tới đối tượng trên ngăn xếp hoặc trong các biến cục bộ. Thông thường các trình biên dịch JIT sẽ được thực hiện như thế này: Đối với mỗi lớp, một phần của bộ nhớ tồn tại chứa thông tin về lớp (ví dụ: các phương thức nào tồn tại và tại đó địa chỉ mã lắp ráp của phương thức nằm ở vị trí vv). Một đối tượng là một phần của bộ nhớ chứa tất cả các biến mẫu và một con trỏ tới bộ nhớ chứa thông tin về lớp đó.

"Instanceof" và "checkcast" có thể được triển khai bằng cách xem con trỏ tới bộ nhớ chứa thông tin về lớp. Thông tin này có thể chứa danh sách tất cả các lớp cha và các giao diện được triển khai.

Vấn đề chính của đối tượng là quản lý bộ nhớ trong Java: Không giống như C++, có một "mới" nhưng không "xóa". Bạn phải kiểm tra tần suất một đối tượng được sử dụng. Nếu một đối tượng không còn được sử dụng, nó phải được xóa khỏi bộ nhớ và hàm hủy phải được gọi. Các vấn đề ở đây là các biến cục bộ (cùng một biến cục bộ có thể chứa một đối tượng hoặc một số) và khối try/catch (khối "catch" phải quan tâm đến các biến cục bộ và ngăn xếp (!) Chứa các đối tượng trước đó khôi phục con trỏ ngăn xếp).

Các vấn đề liên quan