Hướng dẫn what executes python byte code? - cái gì thực thi mã byte python?
Đối tượng mã được biên dịch có thể được lưu bằng cách sử dụng 6
Các byte có thể được chuyển đổi trở lại thành một đối tượng mã
Tệp 7 là một đối tượng mã thu được với tiêu đềĐối với Python3, tiêu đề là 16 byte cần được bỏ qua, dữ liệu còn lại có thể được đọc qua 8.Xem bài đăng trên blog của Ned Batchelder:
Lưu ý, liên kết tham chiếu Python2, nhưng nó gần như giống nhau trong Python3, kích thước tiêu đề 7 chỉ là 16 thay vì 8 byte. Chúng tôi đã bắt đầu loạt bài này với một cái nhìn tổng quan về VM Cpython. Chúng tôi đã học được rằng để chạy một chương trình Python, Cpython trước tiên biên dịch nó thành mã byte và chúng tôi đã nghiên cứu cách trình biên dịch hoạt động trong phần hai. Lần trước chúng tôi đã bước qua mã nguồn cpython bắt đầu với hàm 0 cho đến khi chúng tôi đạt được vòng lặp đánh giá, một nơi mà mã byte Python được thực thi. Lý do chính tại sao chúng tôi dành thời gian nghiên cứu những điều này là để chuẩn bị cho cuộc thảo luận mà chúng tôi bắt đầu ngày hôm nay. Mục tiêu của cuộc thảo luận này là để hiểu cách CPython làm những gì chúng ta bảo nó làm, đó là cách nó thực thi mã byte mà mã chúng ta viết biên dịch.Lưu ý: Trong bài đăng này, tôi đang đề cập đến CPython 3.9. Một số chi tiết thực hiện chắc chắn sẽ thay đổi khi CPython phát triển. Tôi sẽ cố gắng theo dõi các thay đổi quan trọng và thêm ghi chú cập nhật.: In this post I'm referring to CPython 3.9. Some implementation details will certainly change as CPython evolves. I'll try to keep track of important changes and add update notes. Điểm khởi đầuHãy nhớ lại ngắn gọn những gì chúng ta đã học được trong các phần trước. Chúng tôi nói với Cpython phải làm gì bằng cách viết mã Python. Tuy nhiên, VM Cpython chỉ hiểu mã Python byte. Đây là công việc của trình biên dịch để dịch mã python sang mã byte. Trình biên dịch lưu trữ mã byte trong một đối tượng mã, là một cấu trúc mô tả đầy đủ những gì một khối mã, như một mô -đun hoặc một hàm, làm. Để thực thi một đối tượng mã, trước tiên, CPython tạo ra trạng thái thực thi cho nó được gọi là đối tượng khung. Sau đó, nó chuyển một đối tượng khung cho chức năng đánh giá khung để thực hiện tính toán thực tế. Hàm đánh giá khung mặc định là 1 được xác định trong Python/ceval.c. Hàm này thực hiện lõi của VM Cpython. Cụ thể, nó thực hiện logic để thực hiện mã byte Python. Vì vậy, chức năng này là những gì chúng ta sẽ nghiên cứu ngày hôm nay.Để hiểu cách thức hoạt động của 1, điều quan trọng là phải có ý tưởng về đầu vào của nó, một đối tượng khung, là gì. Một đối tượng khung là một đối tượng Python được xác định bởi cấu trúc C sau:
Trường 3 của đối tượng khung chỉ vào một đối tượng mã. Một đối tượng mã cũng là một đối tượng Python. Đây là định nghĩa của nó:
Trường quan trọng nhất của đối tượng mã là 4. Đó là một con trỏ tới đối tượng Byte Python đại diện cho mã byte. Mã byte là một chuỗi các hướng dẫn hai byte: một byte cho một opcode và một byte cho một đối số.Đừng lo lắng nếu một số thành viên của các cấu trúc trên vẫn là một bí ẩn đối với bạn. Chúng ta sẽ thấy những gì chúng được sử dụng khi chúng ta tiến lên trong nỗ lực của chúng ta để hiểu cách thức VM CPython thực thi mã byte. Tổng quan về vòng lặp đánh giáVấn đề thực hiện mã Bytepy Python có vẻ không có trí tuệ đối với bạn. Thật vậy, tất cả các VM phải làm là lặp lại các hướng dẫn và hành động theo chúng. Và đây là những gì về cơ bản 1 làm. Nó chứa một vòng lặp 6 vô hạn mà chúng tôi gọi là vòng đánh giá. Bên trong vòng lặp đó có một tuyên bố khổng lồ 7 trên tất cả các mã hóa có thể. Mỗi opcode có một khối 8 tương ứng chứa mã để thực thi opcode đó. Mã byte được biểu thị bằng một mảng các số nguyên không dấu 16 bit, một số nguyên cho mỗi hướng dẫn. VM theo dõi hướng dẫn tiếp theo được thực thi bằng biến 9, đây là một con trỏ tới mảng hướng dẫn. Khi bắt đầu mỗi lần lặp của vòng lặp đánh giá, VM tính toán opcode tiếp theo và đối số của nó bằng cách sử dụng byte ít có ý nghĩa nhất và quan trọng nhất của lệnh tiếp theo và tăng 9. Hàm 1 dài gần 3000 dòng, nhưng bản chất của nó có thể được ghi lại bằng phiên bản đơn giản hóa sau:
Để có được một bức tranh thực tế hơn, hãy thảo luận về một số tác phẩm bị bỏ qua chi tiết hơn. lý do để đình chỉ vòng lặpThỉnh thoảng, luồng hiện đang chạy dừng thực thi mã byte để làm việc khác hoặc không làm gì cả. Điều này có thể xảy ra do một trong bốn lý do:
CPython có các chỉ số cho mỗi sự kiện này. Biến chỉ ra rằng có những người xử lý để gọi là thành viên của 8, đó là một cấu trúc 9:
Các chỉ số khác là thành viên của 0 là một cấu trúc 1:
Kết quả của việc cung cấp tất cả các chỉ số cùng nhau được lưu trữ trong biến 2. Nó cho biết liệu có bất kỳ lý do nào cho chủ đề đang chạy hiện đang dừng thực thi mã byte bình thường của nó hay không. Mỗi lần lặp của vòng đánh giá bắt đầu với việc kiểm tra xem 2 có đúng không. Nếu đó là sự thật, luồng kiểm tra các chỉ số để xác định chính xác nó được yêu cầu làm gì, có phải điều đó và tiếp tục thực thi mã byte.tính toán gotosMã cho vòng đánh giá có đầy đủ các macro như 4 và 5. Đây không phải là phương tiện để làm cho mã nhỏ gọn hơn. Chúng mở rộng sang mã khác nhau tùy thuộc vào việc tối ưu hóa nhất định, được gọi là "gotos được tính toán" (a.k.a. "Mã luồng"), được sử dụng. Mục tiêu của chúng là tối ưu hóa này là tăng tốc độ thực thi mã byte bằng cách viết mã theo cách đó, để CPU có thể sử dụng cơ chế dự đoán nhánh của nó để dự đoán mã hóa tiếp theo.Sau khi thực hiện bất kỳ lệnh nào đã cho, VM thực hiện một trong ba điều:
Để xem vấn đề với câu lệnh 3 đơn giản, chúng ta cần hiểu những gì 7 biên dịch. Opcode là một số nguyên từ 0 đến 255. Vì phạm vi dày đặc, trình biên dịch có thể tạo một bảng nhảy lưu trữ các địa chỉ của các khối 8 và sử dụng các opcode làm chỉ số vào bảng đó. Các trình biên dịch hiện đại thực sự làm điều đó, vì vậy việc gửi các trường hợp được thực hiện một cách hiệu quả như một bước nhảy gián tiếp. Đây là một cách hiệu quả để thực hiện 7. Tuy nhiên, việc đặt 7 bên trong vòng lặp và thêm câu lệnh 3 tạo ra hai sự thiếu hiệu quả:
Ý tưởng về việc tối ưu hóa là đặt một bước nhảy riêng biệt vào cuối mỗi khối 8 không hoàn trả. Đầu tiên, nó tiết kiệm một bước nhảy. Thứ hai, CPU có thể dự đoán opcode tiếp theo là mã opcode có thể xảy ra nhất sau mã hiện tại.Việc tối ưu hóa có thể được bật hoặc tắt. Nó phụ thuộc vào việc trình biên dịch có hỗ trợ tiện ích mở rộng GCC C được gọi là "nhãn là giá trị" hay không. Hiệu quả của việc cho phép tối ưu hóa là các macro nhất định, chẳng hạn như 4, 5 và 6, mở rộng theo cách khác nhau. Các macro này được sử dụng rộng rãi trong toàn bộ mã của vòng đánh giá. Mỗi biểu thức trường hợp có dạng 7, trong đó 8 là một macro cho số nguyên theo nghĩa đen đại diện cho một opcode. Và mỗi khối 8 không hoàn nguyên kết thúc bằng macro 5 hoặc 6. Trước tiên, chúng ta hãy nhìn vào những gì các macro này mở rộng đến khi tối ưu hóa bị vô hiệu hóa:
Macro 6 được sử dụng cho một số opcode khi không mong muốn để treo vòng lặp đánh giá sau khi thực hiện opcode đó. Nếu không, việc thực hiện rất đơn giản.Nếu trình biên dịch hỗ trợ tiện ích mở rộng "Nhãn là giá trị", chúng ta có thể sử dụng toán tử 3 Unary trên nhãn để lấy địa chỉ của nó. Nó có giá trị loại 4, vì vậy chúng tôi có thể lưu trữ nó trong một con trỏ:Sau đó chúng ta có thể vào nhãn bằng cách phân tích con trỏ: Phần mở rộng này cho phép thực hiện một bảng nhảy trong C dưới dạng một mảng các con trỏ nhãn. Và đó là những gì Cpython làm:
Đây là phiên bản tối ưu của vòng lặp đánh giá trông như thế nào:
Phần mở rộng được hỗ trợ bởi các trình biên dịch GCC và Clang. Vì vậy, khi bạn chạy 5, bạn có thể đã bật tối ưu hóa. Câu hỏi, tất nhiên, là nó ảnh hưởng đến hiệu suất như thế nào. Ở đây, tôi sẽ dựa vào nhận xét từ mã nguồn:
Phần này sẽ cho chúng ta một ý tưởng về cách VM CPython đi từ hướng dẫn này sang hướng dẫn tiếp theo và những gì nó có thể làm ở giữa. Bước hợp lý tiếp theo là nghiên cứu sâu hơn về cách VM thực hiện một hướng dẫn duy nhất. CPython 3.9 có 119 mã hóa khác nhau. Tất nhiên, chúng tôi sẽ không nghiên cứu việc triển khai từng opcode trong bài đăng này. Thay vào đó, chúng tôi sẽ tập trung vào các nguyên tắc chung mà VM sử dụng để thực hiện chúng. Giá trị ngăn xếpĐiều quan trọng nhất và may mắn thay, rất đơn giản về VM CPython là dựa trên ngăn xếp. Điều này có nghĩa là để tính toán mọi thứ, các giá trị VM POP (hoặc peek) từ ngăn xếp, thực hiện tính toán trên chúng và đẩy kết quả trở lại. Đây là một số ví dụ:
Ngăn xếp giá trị nằm trong một đối tượng khung. Nó được triển khai như một phần của mảng được gọi là 0. Mảng được chia thành một số phần để lưu trữ những thứ khác nhau, nhưng chỉ phần cuối cùng được sử dụng cho ngăn xếp giá trị. Sự khởi đầu của phần này là đáy của ngăn xếp. Trường 1 của một đối tượng khung chỉ vào nó. Để xác định vị trí đỉnh của ngăn xếp, CPython giữ biến cục bộ 2, chỉ vào vị trí tiếp theo sau đỉnh của ngăn xếp. Các yếu tố của mảng 0 là con trỏ đến các đối tượng Python và con trỏ đến các đối tượng Python là những gì VM Cpython thực sự hoạt động.Xử lý lỗi và ngăn chặn ngăn chặnKhông phải tất cả các tính toán được thực hiện bởi VM đều thành công. Giả sử chúng ta cố gắng thêm một số vào một chuỗi như 4. Trình biên dịch tạo ra opcode 9 để thêm hai đối tượng. Khi VM thực thi mã hóa này, nó gọi 6 để tính toán kết quả: 0Điều quan trọng đối với chúng tôi bây giờ không phải là cách 6 được thực hiện, mà là cuộc gọi đến nó dẫn đến một lỗi. Lỗi có nghĩa là hai điều:
9 là một chỉ số cho một lỗi. VM nhìn thấy nó và đi đến nhãn 6 ở cuối vòng đánh giá. Điều gì xảy ra tiếp theo phụ thuộc vào việc chúng tôi có thiết lập bất kỳ trình xử lý ngoại lệ nào hay không. Nếu chúng ta không có, VM đạt được câu lệnh 7 và chức năng đánh giá trả về 9 với ngoại lệ được đặt trên trạng thái luồng. Cpython in các chi tiết của ngoại lệ và thoát. Chúng tôi nhận được kết quả dự kiến: 1Nhưng giả sử rằng chúng ta đặt cùng một mã bên trong mệnh đề 9 của câu lệnh 00. Trong trường hợp này, mã bên trong mệnh đề 01 cũng được thực thi: 2Làm thế nào VM có thể tiếp tục thực thi sau khi lỗi đã xảy ra? Chúng ta hãy xem mã byte được tạo bởi trình biên dịch cho câu lệnh 00: 3
Lưu ý các opcodes 03 và 04. Cái đầu tiên thiết lập trình xử lý ngoại lệ và cái thứ hai loại bỏ nó. Nếu xảy ra lỗi trong khi VM thực hiện các hướng dẫn giữa chúng, việc thực thi tiếp tục với lệnh ở Offset 22, đó là khởi đầu của mệnh đề 01. Mặt khác, mệnh đề 01 được thực thi sau mệnh đề 9. Trong cả hai trường hợp, mã byte cho mệnh đề 01 gần như giống hệt nhau. Sự khác biệt duy nhất là người xử lý tái tạo ngoại lệ được đặt trong mệnh đề 9.Một trình xử lý ngoại lệ được triển khai dưới dạng cấu trúc C đơn giản được gọi là khối: 4VM giữ các khối trong ngăn xếp khối. Để thiết lập một trình xử lý ngoại lệ có nghĩa là đẩy một khối mới vào ngăn xếp khối. Đây là những gì opcodes như 03 làm. Nhãn 6 trỏ đến một đoạn mã cố gắng xử lý lỗi bằng cách sử dụng các khối trên ngăn xếp khối. VM thư giãn ngăn xếp khối cho đến khi nó tìm thấy khối trên cùng của loại 03. Nó khôi phục mức của ngăn xếp giá trị về mức được chỉ định bởi trường 13 của khối và tiếp tục thực thi mã byte với lệnh tại Offset 14. Đây về cơ bản là cách CPYThon thực hiện các tuyên bố như 15, 00 và 17.Có một điều nữa để nói về xử lý ngoại lệ. Hãy nghĩ về những gì xảy ra khi xảy ra lỗi trong khi VM xử lý một ngoại lệ: 5Đúng như dự đoán, Cpython in ngoại lệ ban đầu. Để thực hiện hành vi đó, khi CPython xử lý một ngoại lệ bằng cách sử dụng khối 03, nó sẽ thiết lập một khối khác của loại 19. Nếu xảy ra lỗi khi một khối loại này nằm trên ngăn xếp khối, VM sẽ nhận được ngoại lệ ban đầu từ ngăn xếp giá trị và đặt nó thành hiện tại. Cpython đã từng có các loại khối khác nhau nhưng bây giờ chỉ là 03 và 19.Ngăn xếp khối được triển khai dưới dạng mảng 22 trong một đối tượng khung. Kích thước của mảng được xác định tĩnh là 20. Vì vậy, nếu bạn làm tổ hơn 20 mệnh đề 9, bạn sẽ nhận được 24.Bản tóm tắtHôm nay, chúng tôi đã học được rằng CPython VM thực thi các hướng dẫn mã byte từng cái một trong một vòng lặp vô hạn. Vòng lặp chứa một câu lệnh 7 trên tất cả các opcodes có thể. Mỗi opcode được thực thi trong khối 8 tương ứng. Hàm đánh giá chạy trong một luồng và đôi khi luồng đó treo vòng lặp để làm một cái gì đó khác. Ví dụ, một luồng có thể cần phải phát hành GIL, để luồng khác có thể lấy nó và tiếp tục thực thi mã byte của nó. Để tăng tốc độ thực thi mã byte, CPython sử dụng tối ưu hóa cho phép sử dụng cơ chế dự đoán nhánh của CPU. Một nhận xét nói rằng nó làm cho CPython nhanh hơn 15-20%.Chúng tôi cũng đã xem xét hai cấu trúc dữ liệu quan trọng cho việc thực thi mã byte:
Kết luận quan trọng nhất từ bài viết là: Nếu bạn muốn nghiên cứu việc thực hiện một số khía cạnh của Python, vòng lặp đánh giá là một nơi hoàn hảo để bắt đầu. Bạn muốn biết điều gì xảy ra khi bạn viết 27? Hãy xem mã cho opcode 9. Bạn muốn biết tuyên bố 17 được thực hiện như thế nào? Xem 30. Quan tâm đến ngữ nghĩa chính xác của một cuộc gọi chức năng? Opcode 31 là những gì bạn đang tìm kiếm. Chúng tôi sẽ áp dụng phương pháp này vào lần tới khi nghiên cứu cách các biến được thực hiện trong CPython.Nếu bạn có bất kỳ câu hỏi, nhận xét hoặc đề xuất nào, vui lòng liên hệ với tôi tại Cập nhật từ ngày 10 tháng 11 năm 2020: Tôi đã được chỉ ra trong các bình luận về HN rằng 6 and 7 opcodes peek the value on top of the stack and replace it with the result instead of popping the value and pushing the result. This is indeed so. Semantically, the two approaches are equivalent. The only difference is that the pop/push approach first decrements and then increments 2. CPython avoids these redundant operations.
Mã byte được thực thi bởi những gì?Chấp hành. Một chương trình ByteCode có thể được thực thi bằng cách phân tích cú pháp và thực hiện trực tiếp các hướng dẫn, mỗi lần một. Loại thông dịch viên byte này rất di động. Một số hệ thống, được gọi là Trình dịch động hoặc trình biên dịch chỉ trong thời gian (JIT), dịch mã byte thành mã máy khi cần thiết trong thời gian chạy.parsing and directly executing the instructions, one at a time. This kind of bytecode interpreter is very portable. Some systems, called dynamic translators, or just-in-time (JIT) compilers, translate bytecode into machine code as necessary at runtime.
Làm thế nào là mã byte python được tạo ra?Mã byte được tự động tạo trong cùng một thư mục với tệp .py, khi một mô -đun Python lần đầu tiên được nhập hoặc khi nguồn gần đây hơn tệp được biên dịch hiện tại. Lần tới, khi chương trình được chạy, người thông báo Python sử dụng tệp này để bỏ qua bước biên dịch.automatically created in the same directory as . py file, when a module of python is imported for the first time, or when the source is more recent than the current compiled file. Next time, when the program is run, python interpretator use this file to skip the compilation step.
Tại sao Python sử dụng mã byte?Python chậm hơn so với ngôn ngữ lập trình khác, nhưng quá trình chuyển đổi mã Python thành mã byte giúp truy cập nhanh hơn mỗi lần sau khi mã được giải thích một lần.Mã byte này được lưu trong tệp có tên giống như tệp nguồn nhưng với một tiện ích mở rộng khác có tên là PYC PYC.makes it faster to access each time after the code is interpreted once. This bytecode is saved in the file named same as the source file but with a different extension named as “pyc”.
Python có được biên dịch cho mã byte không?Python là một ngôn ngữ được giải thích, có nghĩa là mã nguồn của chương trình Python được chuyển đổi thành mã byte sau đó được thực hiện bởi máy ảo Python.Python khác với các ngôn ngữ được biên dịch chính, chẳng hạn như C và C + +, vì mã Python không bắt buộc phải được xây dựng và liên kết như mã cho các ngôn ngữ này.the source code of a Python program is converted into bytecode that is then executed by the Python virtual machine. Python is different from major compiled languages, such as C and C + +, as Python code is not required to be built and linked like code for these languages. |