CẬP NHẬT: Tôi thích câu hỏi này rất nhiều, tôi đã biến nó thành chủ đề của blog của mình vào ngày 18 tháng 11 năm 2011 . Cảm ơn vì câu hỏi tuyệt vời của bạn!
Tôi đã luôn tự hỏi: mục đích của ngăn xếp là gì?
Tôi giả sử bạn có nghĩa là ngăn xếp đánh giá của ngôn ngữ MSIL chứ không phải ngăn xếp trên mỗi luồng thực tế khi chạy.
Tại sao có sự chuyển từ bộ nhớ sang ngăn xếp hoặc "tải?" Mặt khác, tại sao có sự chuyển từ ngăn xếp sang bộ nhớ hoặc "lưu trữ"? Tại sao không chỉ có tất cả chúng được đặt trong bộ nhớ?
MSIL là ngôn ngữ "máy ảo". Các trình biên dịch như trình biên dịch C # tạo CIL , và sau đó trong thời gian chạy, trình biên dịch khác gọi là trình biên dịch JIT (Just In Time) biến IL thành mã máy thực tế có thể thực thi.
Vì vậy, trước tiên hãy trả lời câu hỏi "tại sao lại có MSIL?" Tại sao không có trình biên dịch C # viết mã máy?
Bởi vì nó rẻ hơn để làm theo cách này. Giả sử chúng ta đã không làm theo cách đó; giả sử mỗi ngôn ngữ phải có trình tạo mã máy riêng. Bạn có hai mươi ngôn ngữ khác nhau: C #, JScript .NET , Visual Basic, IronPython , F # ... Và giả sử bạn có mười bộ xử lý khác nhau. Có bao nhiêu trình tạo mã bạn phải viết? 20 x 10 = 200 máy tạo mã. Đó là rất nhiều công việc. Bây giờ giả sử bạn muốn thêm một bộ xử lý mới. Bạn phải viết trình tạo mã cho nó hai mươi lần, một lần cho mỗi ngôn ngữ.
Hơn nữa, đó là công việc khó khăn và nguy hiểm. Viết các trình tạo mã hiệu quả cho các chip mà bạn không phải là chuyên gia là một công việc khó khăn! Các nhà thiết kế trình biên dịch là các chuyên gia về phân tích ngữ nghĩa của ngôn ngữ của họ, chứ không phải phân bổ đăng ký hiệu quả các bộ chip mới.
Bây giờ giả sử chúng ta làm theo cách CIL. Bạn phải viết bao nhiêu máy phát điện CIL? Một cho mỗi ngôn ngữ. Bạn phải viết bao nhiêu trình biên dịch JIT? Một cho mỗi bộ xử lý. Tổng cộng: 20 + 10 = 30 trình tạo mã. Hơn nữa, trình tạo ngôn ngữ-CIL dễ viết vì CIL là ngôn ngữ đơn giản và trình tạo mã CIL-to-machine-code cũng dễ viết vì CIL là ngôn ngữ đơn giản. Chúng tôi loại bỏ tất cả những điều phức tạp của C # và VB và không chú ý và "hạ thấp" mọi thứ thành một ngôn ngữ đơn giản, dễ viết jitter cho.
Có một ngôn ngữ trung gian làm giảm đáng kể chi phí sản xuất một trình biên dịch ngôn ngữ mới . Nó cũng làm giảm đáng kể chi phí hỗ trợ một con chip mới. Bạn muốn hỗ trợ một con chip mới, bạn tìm một số chuyên gia về con chip đó và nhờ họ viết một jitter CIL và bạn đã hoàn thành; sau đó bạn hỗ trợ tất cả các ngôn ngữ trên chip của bạn.
OK, vì vậy chúng tôi đã thiết lập lý do tại sao chúng tôi có MSIL; bởi vì có một ngôn ngữ trung gian làm giảm chi phí. Tại sao ngôn ngữ là "máy xếp"?
Bởi vì các máy stack là khái niệm rất đơn giản cho các nhà văn biên dịch ngôn ngữ để đối phó. Ngăn xếp là một cơ chế đơn giản, dễ hiểu để mô tả các tính toán. Các máy stack cũng về mặt khái niệm rất dễ dàng cho các nhà văn trình biên dịch JIT đối phó. Sử dụng một ngăn xếp là một sự trừu tượng đơn giản hóa, và do đó, một lần nữa, nó làm giảm chi phí của chúng tôi .
Bạn hỏi "tại sao lại có một chồng?" Tại sao không làm mọi thứ trực tiếp ra khỏi bộ nhớ? Chà, hãy nghĩ về điều đó. Giả sử bạn muốn tạo mã CIL cho:
int x = A() + B() + C() + 10;
Giả sử chúng ta có quy ước "thêm", "gọi", "lưu trữ" và cứ thế, luôn lấy các đối số của chúng ra khỏi ngăn xếp và đặt kết quả của chúng (nếu có) vào ngăn xếp. Để tạo mã CIL cho C # này, chúng ta chỉ cần nói một số thứ như:
load the address of x // The stack now contains address of x
call A() // The stack contains address of x and result of A()
call B() // Address of x, result of A(), result of B()
add // Address of x, result of A() + B()
call C() // Address of x, result of A() + B(), result of C()
add // Address of x, result of A() + B() + C()
load 10 // Address of x, result of A() + B() + C(), 10
add // Address of x, result of A() + B() + C() + 10
store in address // The result is now stored in x, and the stack is empty.
Bây giờ giả sử chúng tôi đã làm nó mà không có một ngăn xếp. Chúng tôi sẽ thực hiện theo cách của bạn, trong đó mọi opcode sẽ lấy địa chỉ của toán hạng của nó và địa chỉ mà nó lưu kết quả của nó :
Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...
Bạn thấy điều này diễn ra như thế nào? Mã của chúng tôi đang trở nên rất lớn bởi vì chúng tôi phải phân bổ rõ ràng tất cả lưu trữ tạm thời mà thông thường theo quy ước chỉ cần đi vào ngăn xếp . Tồi tệ hơn, bản thân các mã của chúng ta đang trở nên to lớn bởi vì tất cả chúng bây giờ phải lấy làm đối số cho địa chỉ mà chúng sẽ ghi kết quả của chúng vào và địa chỉ của mỗi toán hạng. Một lệnh "thêm" biết rằng nó sẽ lấy hai thứ ra khỏi ngăn xếp và đặt một thứ lên có thể là một byte đơn. Một hướng dẫn thêm có hai địa chỉ toán hạng và địa chỉ kết quả sẽ rất lớn.
Chúng tôi sử dụng opcodes dựa trên ngăn xếp vì ngăn xếp giải quyết vấn đề phổ biến . Cụ thể: Tôi muốn phân bổ một số lưu trữ tạm thời, sử dụng nó rất sớm và sau đó loại bỏ nó một cách nhanh chóng khi tôi hoàn thành . Bằng cách đưa ra giả định rằng chúng ta có một ngăn xếp theo ý của chúng ta, chúng ta có thể làm cho các opcode rất nhỏ và mã rất ngắn gọn.
CẬP NHẬT: Một số suy nghĩ bổ sung
Ngẫu nhiên, ý tưởng giảm chi phí mạnh mẽ bằng cách (1) chỉ định một máy ảo, (2) trình biên dịch viết nhắm vào ngôn ngữ VM và (3) viết các triển khai VM trên nhiều loại phần cứng, hoàn toàn không phải là một ý tưởng mới . Nó không bắt nguồn từ MSIL, LLVM, Java bytecode hoặc bất kỳ cơ sở hạ tầng hiện đại nào khác. Việc thực hiện sớm nhất của chiến lược này mà tôi biết là máy pcode từ năm 1966.
Cá nhân tôi lần đầu tiên nghe về khái niệm này là khi tôi tìm hiểu cách những người triển khai Infocom quản lý để Zork chạy trên rất nhiều máy khác nhau rất tốt. Họ đã chỉ định một máy ảo được gọi là máy Z và sau đó tạo trình giả lập máy Z cho tất cả phần cứng mà họ muốn chạy trò chơi của họ. Điều này có thêm lợi ích to lớn mà họ có thể thực hiện quản lý bộ nhớ ảo trên các hệ thống 8 bit nguyên thủy; một trò chơi có thể lớn hơn phù hợp với bộ nhớ vì họ chỉ có thể trang mã từ đĩa khi họ cần và loại bỏ nó khi họ cần tải mã mới.