27. Vòng đánh giá
27. Vòng đánh giá
Vòng đánh giá là công cụ thực thi trung tâm của CPython. Nó lấy một đối tượng mã được biên dịch, thực thi các lệnh mã byte của nó và tạo ra một kết quả hoặc một ngoại lệ.
Ở mức độ cao, quá trình thực thi CPython trông như thế này:```text Python source ↓ tokens ↓ parser ↓ AST ↓ symbol table ↓ compiler ↓ code object ↓ frame ↓ evaluation loop ↓ Python result or exception
Vòng lặp đánh giá nằm trong quá trình triển khai trình thông dịch của CPython. Trong lịch sử, tập tin chính đã được`Python/ceval.c`, với máy thông dịch xung quanh trải rộng trên các tệp khác. CPython hiện đại cũng tạo ra một số mã trình thông dịch từ các định nghĩa mã byte. Các chi tiết di chuyển giữa các bản phát hành nhưng mô hình vẫn ổn định: một khung thực thi một đối tượng mã bằng cách gửi đi nhiều lần các lệnh mã byte. Hướng dẫn dành cho nhà phát triển của CPython chỉ ra tài liệu nội bộ và cây nguồn làm tài liệu tham khảo hiện tại vì mã này thay đổi giữa các phiên bản.
## 27.1 Công việc của vòng đánh giá
Vòng đánh giá không phân tích văn bản Python. Nó không xây dựng AST. Nó thường không quyết định phạm vi từ vựng. Những công việc đó đã được hoàn thành vào thời điểm bắt đầu thực hiện.
Công việc của nó hẹp hơn và máy móc hơn:```text
read the next bytecode instruction
decode its operand
perform the operation
update the frame
continue, jump, call, return, or raise
```Ví dụ: chức năng này:```python
def add(a, b):
return a + b
```được biên dịch thành một đối tượng mã. Đối tượng mã chứa mã byte. Khi`add(2, 3)`được gọi, CPython tạo hoặc khởi tạo khung cho lệnh gọi đó, lưu trữ các đối số trong các vị trí cục bộ nhanh, sau đó chạy khung đó qua vòng đánh giá.
Vòng lặp cuối cùng đạt tới lệnh return. Lệnh đó bật hoặc đọc giá trị trả về, mở khung và trả về con trỏ đối tượng cho người gọi.
Về mặt khái niệm:```text
call add(2, 3)
create frame
store a = 2
store b = 3
execute LOAD_FAST a
execute LOAD_FAST b
execute BINARY_OP +
execute RETURN_VALUE
return 5
```Việc triển khai thực tế phức tạp hơn nhưng đây là mô hình cốt lõi.
## 27.2 Các đối tượng thời gian chạy chính
Vòng đánh giá kết nối một số đối tượng thời gian chạy CPython.
| Đối tượng thời gian chạy | Vai trò |
|---|---|
| Đối tượng mã | Mã byte và siêu dữ liệu được biên dịch bất biến |
| Khung | Trạng thái thực thi có thể thay đổi cho một cuộc gọi |
| Trạng thái chủ đề | Trạng thái thực thi trình thông dịch trên mỗi luồng |
| Trạng thái thông dịch viên | Trạng thái thời gian chạy trên mỗi phiên dịch viên |
| Đối tượng Python | Giá trị thời gian chạy được thao tác theo hướng dẫn |
| Nhập đối tượng | Bảng hành vi thời gian chạy cho các đối tượng Python |
Một đối tượng mã mô tả những gì sẽ chạy.
Một khung lưu trữ một lần thực thi đang hoạt động của mã đó.
Vòng lặp đánh giá thực thi khung.
Sự khác biệt này quan trọng. Một đối tượng mã có thể được thực thi nhiều lần. Mỗi cuộc gọi có trạng thái khung riêng.```python
def f(x):
return x + 1
a = f(10)
b = f(20)
```Cả hai cuộc gọi đều sử dụng cùng một đối tượng mã, nhưng mỗi cuộc gọi có cục bộ, trạng thái ngăn xếp và giá trị trả về riêng biệt.
## 27.3 Đối tượng mã
Một đối tượng mã chứa biểu diễn được biên dịch của mã Python.
Bạn có thể kiểm tra một cái từ Python:```python
def f(x):
y = x + 1
return y
code = f.__code__
print(code.co_name)
print(code.co_varnames)
print(code.co_consts)
print(code.co_names)
print(code.co_stacksize)
```Các lĩnh vực điển hình bao gồm:
| Lĩnh vực | Ý nghĩa |
|---|---|
|`co_code`| Luồng mã byte, được hiển thị ở dạng phụ thuộc vào phiên bản |
|`co_consts`| Các hằng số theo nghĩa đen được mã sử dụng |
|`co_names`| Tên được tham chiếu theo mã byte |
|`co_varnames`| Tên biến cục bộ |
|`co_freevars`| Các biến miễn phí được ghi lại từ phạm vi bên ngoài |
|`co_cellvars`| Các biến được nắm bắt bởi phạm vi bên trong |
|`co_argcount`| Số đối số vị trí |
|`co_kwonlyargcount`| Số lượng đối số chỉ từ khóa |
|`co_stacksize`| Kích thước ngăn xếp giá trị bắt buộc |
|`co_flags`| Cờ mã |
|`co_filename`| Tên tệp nguồn |
|`co_name`| Tên hàm hoặc khối |
|`co_qualname`| Tên đủ điều kiện |
|`co_firstlineno`| Dòng nguồn đầu tiên |
| bảng dòng | Ánh xạ từ độ lệch mã byte sang dòng nguồn |
| bảng ngoại lệ | Siêu dữ liệu xử lý ngoại lệ có cấu trúc |
các`dis`module tồn tại đặc biệt để kiểm tra mã byte CPython. Tài liệu của nó lưu ý rằng mã byte CPython là một chi tiết triển khai và có thể thay đổi giữa các phiên bản Python.
## 27.4 Khung hình
Một khung là một bản ghi thực thi.
Khi một hàm chạy, CPython cần một nơi nào đó để lưu trữ:```text
the code object being executed
the instruction pointer
local variables
temporary stack values
globals dictionary
builtins dictionary
closure cells
exception state
return state
tracing and profiling state
```Cấu trúc đó là khung.
Một mô hình khung đơn giản trông như thế này:```text
frame
code object
globals
builtins
locals / fast locals
instruction pointer
value stack
block and exception state
previous frame / caller relation
```Chuỗi cuộc gọi Python tạo ra một chuỗi khung:```python
def a():
return b()
def b():
return c()
def c():
return 42
a()
```Về mặt khái niệm:```text
frame for a
frame for b
frame for c
```Tại bất kỳ thời điểm nào, trạng thái luồng hiện tại sẽ trỏ đến khung hiện đang thực thi hoặc biểu diễn khung bên trong tương đương.
## 27.5 Người dân địa phương nhanh
Các biến cục bộ của hàm thường không được lưu trữ trong từ điển thông thường trong quá trình thực thi.
CPython sử dụng bố cục giống mảng cho các biến cục bộ nhanh. Tên được phân giải tại thời điểm biên dịch thành chỉ mục cục bộ. Sau đó, các hướng dẫn mã byte có thể truy cập các biến cục bộ theo chỉ mục thay vì thực hiện tra cứu từ điển.
Ví dụ:```python
def f(a, b):
c = a + b
return c
```Trình biên dịch gán các vị trí cục bộ:
| Tên | Khe |
|---|---:|
|`a`| 0 |
|`b`| 1 |
|`c`| 2 |
Mã byte sau đó có thể sử dụng các hoạt động dựa trên vị trí:```text
LOAD_FAST 0 load a
LOAD_FAST 1 load b
BINARY_OP +
STORE_FAST 2 store c
LOAD_FAST 2 load c
RETURN_VALUE
```Đây là lý do tại sao truy cập biến cục bộ thường nhanh hơn truy cập biến toàn cục. Truy cập cục bộ có thể sử dụng khe khung trực tiếp. Quyền truy cập toàn cầu phải tìm kiếm từ điển và xử lý dự phòng nội trang.
## 27.6 Ngăn xếp giá trị
Mã byte CPython sử dụng mô hình ngăn xếp.
Hầu hết các hướng dẫn đọc và ghi vào ngăn xếp giá trị khung cục bộ. Ngăn xếp này tách biệt với ngăn xếp cuộc gọi C. Nó lưu trữ`PyObject *`các giá trị trong quá trình thực thi mã byte.
Đối với biểu thức này:```python
x = (a + b) * c
```Hành vi ngăn xếp đại khái là:```text
LOAD_FAST a stack: [a]
LOAD_FAST b stack: [a, b]
BINARY_OP + stack: [a_plus_b]
LOAD_FAST c stack: [a_plus_b, c]
BINARY_OP * stack: [product]
STORE_FAST x stack: []
```Ngăn xếp giá trị là trung tâm của thiết kế mã byte. Nó tránh việc cần mọi hướng dẫn để đặt tên cho các thanh ghi nguồn và đích rõ ràng. Thay vào đó, các hướng dẫn thống nhất về hiệu ứng ngăn xếp.
Một số hướng dẫn đẩy giá trị:```text
LOAD_CONST
LOAD_FAST
LOAD_GLOBAL
BUILD_LIST
```Một số hướng dẫn bật giá trị:```text
STORE_FAST
POP_TOP
RETURN_VALUE
```Một số làm cả hai:```text
BINARY_OP
CALL
LOAD_ATTR
COMPARE_OP
```## 27.7 Con trỏ lệnh
Khung theo dõi nơi thực thi nằm trong luồng mã byte.
Đối với mã đường thẳng, con trỏ lệnh di chuyển về phía trước sau mỗi lệnh.
Đối với các nhánh, vòng lặp, xử lý ngoại lệ và trả về, luồng điều khiển thay đổi hướng dẫn.
Ví dụ:```python
def sign(x):
if x < 0:
return -1
return 1
```Luồng mã byte khái niệm:```text
load x
load 0
compare <
jump if false to positive_return
load -1
return
positive_return:
load 1
return
```Con trỏ lệnh là thứ giúp thực hiện được điều này. Lệnh rẽ nhánh thay đổi lệnh tiếp theo sẽ được thực thi.
##27.8 Công văn
Gửi đi là hành động chọn cách triển khai C cho lệnh mã byte hiện tại.
Một vòng lặp thông dịch được đơn giản hóa trông như thế này:```c
for (;;) {
opcode = read_opcode(frame);
oparg = read_operand(frame);
switch (opcode) {
case LOAD_FAST:
/* load local variable */
break;
case LOAD_CONST:
/* load constant */
break;
case BINARY_OP:
/* perform binary operation */
break;
case RETURN_VALUE:
/* return from frame */
break;
}
}
```Đây chỉ là một mô hình giảng dạy. CPython hiện đại sử dụng các kỹ thuật điều phối được tối ưu hóa và mã trình thông dịch được tạo ở nhiều nơi. Tuy nhiên, hình dạng thiết yếu vẫn còn:```text
fetch
decode
dispatch
execute
repeat
```Vấn đề chi phí gửi hàng. Mọi lệnh mã byte Python đều đi qua công văn. Nếu một vòng lặp thực thi hàng triệu lệnh mã byte, chi phí điều phối sẽ hiển thị.
## 27.9 Hiệu ứng ngăn xếp
Mỗi lệnh bytecode đều có hiệu ứng ngăn xếp.
Hiệu ứng ngăn xếp mô tả số lượng giá trị mà một lệnh tiêu thụ và tạo ra.
Ví dụ:
| Hướng dẫn | Ngăn xếp đầu vào | Ngăn xếp đầu ra |
|---|---|---|
|`LOAD_CONST` | `[]` | `[const]` |
| `LOAD_FAST` | `[]` | `[local]` |
| `STORE_FAST` | `[value]` | `[]` |
| `BINARY_OP` | `[left, right]` | `[result]` |
| `RETURN_VALUE` | `[value]`| trả về từ khung |
Trình biên dịch phải biết hiệu ứng ngăn xếp để tính toán kích thước ngăn xếp tối đa mà đối tượng mã yêu cầu. Giá trị đó xuất hiện dưới dạng`co_stacksize`.
Vì:```python
def f(a, b, c):
return (a + b) * c
```Ngăn xếp không bao giờ cần chứa nhiều hơn hai hoặc ba giá trị tạm thời, tùy thuộc vào mã byte chính xác. CPython ghi lại độ sâu ngăn xếp cần thiết tối đa để khung có thể dành đủ không gian.
## 27.10 Toán hạng mã byte
Nhiều lệnh bytecode có toán hạng.
Toán hạng là một đối số nguyên nhỏ gắn liền với lệnh. Ý nghĩa phụ thuộc vào opcode.
Ví dụ:```text
LOAD_CONST 0 load co_consts[0]
LOAD_FAST 1 load fast local slot 1
STORE_FAST 2 store into fast local slot 2
LOAD_GLOBAL 3 load name from name table index 3
```Lệnh mã byte thường không lưu trữ một con trỏ đầy đủ tới tên đối tượng hoặc chuỗi. Nó lưu trữ một chỉ mục vào một bảng thuộc sở hữu của đối tượng mã.
Điều này giữ cho mã byte nhỏ gọn và tách biệt siêu dữ liệu bất biến khỏi trạng thái thực thi.
## 27.11 Chạy một hàm đơn giản
Hãy xem xét:```python
def add(a, b):
return a + b
```Việc tháo gỡ có thể khác nhau tùy theo phiên bản Python, nhưng trình tự hướng dẫn khái niệm là:```text
load local a
load local b
binary add
return value
```Quá trình thực hiện diễn ra như sau:
| Bước | Hướng dẫn | Xếp chồng trước | Xếp chồng sau |
|---:|---|---|---|
| 1 |`LOAD_FAST a` | `[]` | `[a]`|
| 2 |`LOAD_FAST b` | `[a]` | `[a, b]`|
| 3 |`BINARY_OP +` | `[a, b]` | `[a + b]`|
| 4 |`RETURN_VALUE` | `[a + b]`| trở lại |
Ở cấp độ C, mỗi phần tử ngăn xếp là một`PyObject *`.
Vì`add(2, 3)`, ngăn xếp chứa các con trỏ tới các đối tượng số nguyên Python. Hoạt động bổ sung gửi đi thông qua ngữ nghĩa đối tượng Python. Nó không trực tiếp phát ra phép cộng số nguyên CPU trong trường hợp chung.
## 27.12 Tại sao`a + b`Không chỉ là một lệnh CPU
Trong Python,`a + b`là năng động.
Các đối tượng có thể là số nguyên:```python
1 + 2
```Chúng có thể là chuỗi:```python
"hello " + "world"
```Chúng có thể là danh sách:```python
[1] + [2]
```Chúng có thể là các đối tượng do người dùng định nghĩa:```python
class X:
def __add__(self, other):
return "custom"
X() + X()
```Hướng dẫn mã byte để bổ sung phải tôn trọng mô hình dữ liệu của Python. Nó phải kiểm tra các loại toán hạng, tìm phép toán số hoặc chuỗi chính xác, gọi các phương thức đặc biệt khi cần, xử lý lỗi và trả về một đối tượng Python.
Vì vậy vòng đánh giá không thể xử lý`+`như phép cộng máy đơn giản. Đây là một hoạt động động trên các đối tượng Python.
CPython hiện đại giảm chi phí này khi có thể. Trình thông dịch thích ứng chuyên biệt có thể chuyên môn hóa các hoạt động sau khi quan sát hành vi thời gian chạy ổn định. PEP 659 mô tả đây là sự chuyên môn hóa trên các khu vực nhỏ với khả năng thích ứng nhanh chóng khi hành vi thay đổi.
## 27.13 Lệnh gọi hàm
Các lệnh gọi hàm là một trong những đường dẫn quan trọng nhất trong vòng đánh giá.
Vì:```python
result = f(x, y)
```Người phiên dịch phải:```text
load callable f
load arguments x and y
arrange call arguments
check callable type
enter optimized call path if possible
create or initialize callee frame if it is a Python function
execute callee frame
receive return value
continue caller frame
```Về mặt khái niệm:```text
caller frame
LOAD_FAST f
LOAD_FAST x
LOAD_FAST y
CALL 2
create callee frame
run callee frame
return object
STORE_FAST result
```CPython đã dành nỗ lực tối ưu hóa đáng kể cho các cuộc gọi vì các cuộc gọi diễn ra thường xuyên và tốn kém. Các cơ chế quan trọng bao gồm:```text
vectorcall
fast locals
specialized call bytecodes
inline caches
frame optimizations
reduced temporary tuple/dict creation
```Mục đích là để tránh việc đóng gói đối số không cần thiết. Trong lịch sử, nhiều lệnh gọi yêu cầu xây dựng bộ dữ liệu và từ điển cho các đối số. Đường dẫn cuộc gọi hiện đại cố gắng truyền đối số theo bố cục giống như mảng khi có thể.
## 27.14 Trở về từ một khung
Lệnh quay lại kết thúc khung hiện tại.
Vì:```python
def f():
return 42
```Lệnh return tạo ra một`PyObject *`kết quả và thư giãn khung.
Người gọi nhận đối tượng đó là kết quả của biểu thức cuộc gọi:```python
x = f()
```Về mặt khái niệm:```text
callee frame stack: [42]
RETURN_VALUE
pop result
finish callee frame
give result to caller
caller resumes with stack: [42]
STORE_FAST x
```Một khung có thể kết thúc bằng nhiều cách:
| Lối thoát | Ý nghĩa |
|---|---|
| Lợi nhuận bình thường | Hàm trả về một giá trị |
| Ngoại lệ | Chức năng thoát bằng cách raise |
| Năng suất máy phát điện | Khung tạm dừng và tiếp tục sau đó |
| Coroutine đang chờ đợi | Coroutine tạm dừng |
| Lỗi nghiêm trọng | Lỗi cấp độ thời gian chạy |
Vòng đánh giá phải xử lý tất cả các đường dẫn này.
## 27.15 Ngoại lệ
Các ngoại lệ là một phần của luồng điều khiển trình thông dịch thông thường.
Vì:```python
def div(a, b):
return a / b
```Nếu như`b`bằng 0, phép chia tăng lên`ZeroDivisionError`.
Lệnh bytecode không trả về kết quả bình thường. Thay vào đó, nó đặt trạng thái ngoại lệ và chuyển điều khiển sang logic xử lý ngoại lệ.
Về mặt khái niệm:```text
execute BINARY_OP /
operation fails
set current exception
search exception table
jump to handler or unwind frame
```CPython hiện đại sử dụng các bảng ngoại lệ có cấu trúc được liên kết với các đối tượng mã. Các bảng này mô tả các phạm vi và trình xử lý mã byte được bảo vệ. Điều này cho phép trình thông dịch tìm được trình xử lý chính xác khi xảy ra ngoại lệ.
Ví dụ:```python
try:
x = 1 / y
except ZeroDivisionError:
x = 0
```Vòng đánh giá phải biết phạm vi được bảo vệ ở đâu, trình xử lý bắt đầu ở đâu và trạng thái ngăn xếp nào được yêu cầu ở trình xử lý.
## 27.16 Vòng lặp và nhánh
Các vòng lặp Python biên dịch để nhảy.
Ví dụ:```python
def count(n):
i = 0
while i < n:
i += 1
return i
```Hình dạng mã byte khái niệm:```text
i = 0
loop_start:
load i
load n
compare <
jump if false to loop_end
load i
load 1
add
store i
jump to loop_start
loop_end:
load i
return
```Vòng lặp đánh giá không có vòng lặp while cấp C đặc biệt cho mỗi Python`while`. Nó thực thi các lệnh mã byte để thực hiện vòng lặp.
Do đó, vòng lặp Python là vòng lặp trình thông dịch bên trong vòng lặp trình thông dịch bên ngoài:```text
C evaluation loop
executes Python loop bytecode
jumps backward many times
```Đây là một lý do khiến vòng lặp Python chặt chẽ có thể đắt tiền. Mỗi lần lặp có thể thực thi nhiều lệnh mã byte và mỗi lệnh mã byte có chi phí điều phối và đối tượng động.
## 27.17 Lặp lại
A`for`vòng lặp sử dụng giao thức lặp.
Ví dụ:```python
for item in xs:
use(item)
```Thực hiện khái niệm:```text
iterator = iter(xs)
loop:
item = next(iterator)
if StopIteration:
exit loop
use(item)
jump loop
```Vòng đánh giá thực hiện các lệnh gọi`iter()`, gọi thao tác tiếp theo của iterator, xử lý`StopIteration`, và nhánh.
Điều này có nghĩa là cấp độ Python`for`các vòng lặp dựa trên giao thức. Chúng hoạt động với các danh sách, bộ dữ liệu, ký tự, tệp, trình tạo, trình vòng lặp tùy chỉnh và nhiều loại tiện ích mở rộng vì trình thông dịch gửi đi thông qua các khe giao thức đối tượng.
## 27.18 Truy cập thuộc tính
Truy cập thuộc tính cũng năng động.
Vì:```python
value = obj.name
```Trình thông dịch phải triển khai các quy tắc tra cứu thuộc tính của Python:```text
look at object type
handle descriptors
look in instance dictionary if applicable
look in class dictionary and base classes
call custom __getattribute__ if present
fall back to __getattr__ if applicable
raise AttributeError if missing
```Một biểu hiện trông đơn giản có thể liên quan đến máy móc quan trọng.
CPython hiện đại sử dụng bộ đệm nội tuyến và chuyên môn hóa để tăng tốc các mẫu truy cập thuộc tính phổ biến. Ví dụ: truy cập nhiều lần vào cùng một thuộc tính trên các đối tượng có hình dạng ổn định có thể tránh được một số công việc tra cứu lặp lại.
## 27.19 Tra cứu toàn cầu và tích hợp
Tra cứu toàn cầu đắt hơn tra cứu cục bộ.
Vì:```python
print(len(xs))
```Những cái tên như`print`Và`len`không phải là biến cục bộ trừ khi được gán cục bộ. CPython tra cứu chúng thông qua các không gian tên toàn cục và dựng sẵn.
Về mặt khái niệm:```text
look in globals dictionary
if missing, look in builtins dictionary
if missing, raise NameError
```Đây là lý do tại sao liên kết cục bộ có thể nhanh hơn trong các vòng lặp chặt chẽ:```python
def slow(xs):
for x in xs:
len(x)
def faster(xs):
local_len = len
for x in xs:
local_len(x)
```CPython hiện đại có thể chuyên môn hóa việc tra cứu toàn cầu, do đó, việc tối ưu hóa vi mô cũ này ít hữu ích hơn so với trước đây. Tuy nhiên, điểm khác biệt cơ bản vẫn là: các vị trí cục bộ đơn giản hơn việc tra cứu tên dựa trên từ điển.
## 27.20 GIL và Vòng đánh giá
Trong thời gian chạy CPython truyền thống, vòng đánh giá chạy trong khi luồng hiện tại giữ Khóa phiên dịch toàn cầu.
GIL bảo vệ trạng thái trình thông dịch, bao gồm số lượng tham chiếu và nhiều nội dung bên trong đối tượng. Vòng lặp đánh giá kiểm tra định kỳ xem nó có nên loại bỏ GIL, xử lý tín hiệu, xử lý các cuộc gọi đang chờ xử lý hay cho phép một luồng khác chạy hay không.
Điều này có nghĩa là việc thực thi mã byte là hợp tác ở cấp độ trình thông dịch. Một thread thường không giữ GIL mãi mãi. CPython có các lịch trình kiểm tra cho phép chuyển đổi giữa các luồng.
Hệ quả thực tế:```text
one thread executes Python bytecode at a time per traditional interpreter
I/O operations may release the GIL
C extensions may release the GIL around long native work
CPU-bound Python threads do not normally execute bytecode in parallel
```Công việc CPython mới hơn bao gồm các bản dựng theo luồng tự do và các thay đổi trên mỗi trình thông dịch, nhưng vòng đánh giá vẫn là vị trí trung tâm nơi gặp nhau trạng thái luồng, công việc đang chờ xử lý và thực thi mã byte.
## 27,21 Số lượng tham chiếu trong quá trình thực thi
Mọi giá trị trên ngăn xếp là một con trỏ đối tượng Python có quy tắc sở hữu.
Vòng đánh giá phải duy trì cẩn thận số lượng tham chiếu. Khi một lệnh đẩy một giá trị, lưu trữ một giá trị, thay thế một giá trị hoặc loại bỏ một giá trị, nó phải duy trì tuổi thọ của đối tượng một cách chính xác.
Ví dụ:```python
x = a + b
```Về mặt khái niệm:```text
load a obtain reference to object a
load b obtain reference to object b
add produce new reference to result
store x bind result to local slot
discard temporaries
```Quản lý tham chiếu không chính xác có thể gây rò rỉ hoặc phá hủy sớm.
Ở cấp độ C, điều này có nghĩa là các hoạt động được sắp xếp cẩn thận tương đương với:```c
Py_INCREF(obj);
Py_DECREF(obj);
```Việc triển khai chính xác thường sử dụng các macro chuyên dụng và quy ước về quyền sở hữu. Nhưng tính bất biến rất đơn giản: một đối tượng phải tồn tại trong khi nó vẫn có thể được sử dụng và nó phải được giải phóng khi trình thông dịch không còn sở hữu một tham chiếu nữa.
## 27.22 Báo lỗi
Hầu hết các hàm trợ giúp C trong CPython đều sử dụng quy ước chung:```text
return a valid pointer or success code on success
return NULL or error code on failure
set an exception on failure
```Vòng đánh giá kiểm tra các kết quả này.
Ví dụ đơn giản:```c
PyObject *result = PyNumber_Add(left, right);
if (result == NULL) {
goto error;
}
```các`NULL`return không tự nó mô tả ngoại lệ. Ngoại lệ được lưu trữ ở trạng thái luồng.
Mẫu này xuất hiện ở khắp mọi nơi:```text
call helper
if failed:
go to error path
else:
push or store result
```Vòng lặp đánh giá chứa nhiều lỗi thoát ra vì hầu hết mọi thao tác Python đều có thể thất bại:```text
allocation can fail
attribute lookup can fail
function call can fail
comparison can fail
iteration can fail
import can fail
descriptor code can fail
user-defined special method can fail
```## 27.23 Cuộc gọi đang chờ xử lý, tín hiệu và sự kiện không đồng bộ
Vòng đánh giá cũng hoạt động như một điểm kiểm tra an toàn cho công việc ở cấp độ thời gian chạy.
CPython không thể xử lý mọi tín hiệu hoặc sự kiện đang chờ xử lý ở ranh giới lệnh C tùy ý. Thay vào đó, nó ghi lại rằng có điều gì đó cần được chú ý và kiểm tra tại các điểm được kiểm soát trong quá trình đánh giá.
Ví dụ:```text
signal handling
pending calls from C APIs
thread switching requests
async exception injection
tracing and profiling hooks
monitoring hooks
interrupt checks
```Điều này giúp cho trình thông dịch có thể quản lý được. Vòng đánh giá trở thành nơi thực thi Python thông báo các sự kiện bên ngoài.
## 27.24 Truy tìm và lập hồ sơ
Python hỗ trợ theo dõi và lập hồ sơ thông qua các API như:```python
sys.settrace(...)
sys.setprofile(...)
```Những cái móc này yêu cầu sự hợp tác từ vòng đánh giá.
Vòng lặp phải phát ra các sự kiện như:```text
call
line
return
exception
opcode, when enabled
```Việc theo dõi khiến việc thực thi chậm hơn vì nó thêm các lệnh kiểm tra và gọi lại. Nhưng nó cho phép trình gỡ lỗi, công cụ bao quát, trình lập hồ sơ, công cụ giảng dạy và hệ thống quan sát.
Trình gỡ lỗi thực hiện các bước thông qua mã Python phụ thuộc vào khả năng của vòng đánh giá trong việc ánh xạ thực thi mã byte trở lại dòng nguồn.
## 27.25 Phiên dịch thích ứng chuyên dụng
Kể từ Python 3.11, CPython đã bao gồm một trình thông dịch thích ứng chuyên dụng dựa trên PEP 659. Ý tưởng là giữ cho ngữ nghĩa Python luôn năng động trong khi thực hiện các trường hợp ổn định phổ biến nhanh hơn. PEP 659 mô tả việc chuyên môn hóa mang tính tích cực trên các vùng nhỏ, có khả năng thích ứng khi mô hình thời gian chạy thay đổi.
Trình thông dịch bắt đầu với mã byte chung. Khi mã chạy, CPython quan sát hành vi và có thể thay thế hoặc tăng cường các hoạt động chung bằng các biểu mẫu chuyên biệt.
Ví dụ: một phép toán nhị phân chung có thể được tối ưu hóa cho các loại toán hạng phổ biến:```text
generic BINARY_OP
observed int + int repeatedly
↓
specialized integer-add path
```Để truy cập thuộc tính:```text
generic LOAD_ATTR
observed same attribute layout repeatedly
↓
cached attribute access path
```Để tra cứu toàn cầu:```text
generic LOAD_GLOBAL
observed stable globals and builtins dictionaries
↓
cached global lookup path
```Chuyên môn hóa phải luôn chính xác. Nếu giả định không thành công, trình thông dịch sẽ lùi lại hoặc điều chỉnh.
Điều này không giống với trình biên dịch JIT đầy đủ truyền thống. Nó vẫn hoạt động bên trong kiến trúc trình thông dịch. Nó chuyên hóa các đường dẫn thực thi ở cấp mã byte thay vì biên dịch toàn bộ hàm thành mã máy gốc trong trường hợp chung.
## 27,26 Bộ nhớ đệm nội tuyến
Bộ đệm nội tuyến là những phần lưu trữ bộ đệm nhỏ được liên kết với các hướng dẫn mã byte.
Thay vì tính toán lại thông tin tra cứu mỗi lần, trình thông dịch lưu trữ các sự kiện gần hướng dẫn cần chúng.
Thông tin bộ nhớ đệm mẫu có thể bao gồm:```text
type version
dictionary version
attribute offset
resolved descriptor
global dictionary version
builtin dictionary version
specialized call target
```Một mô hình bộ đệm thuộc tính đơn giản hóa:```text
LOAD_ATTR name
cache:
expected type = User
type version = 123
attribute offset = 2
```Trong lần thực hiện tiếp theo, CPython có thể kiểm tra xem đối tượng có còn khớp với các giả định được lưu trong bộ nhớ đệm hay không. Nếu có, nó sử dụng đường dẫn nhanh. Nếu không, nó sẽ quay trở lại đường dẫn chung.
Bộ nhớ đệm nội tuyến hoạt động tốt vì các hướng dẫn mã byte tại một vị trí nguồn nhất định thường lặp đi lặp lại các loại đối tượng giống nhau.
## 27.27 Tại sao chuyên môn lại an toàn
Python rất năng động nên tính chuyên môn hóa phải được bảo vệ.
Một đường dẫn chuyên biệt chỉ hợp lệ khi các giả định của nó vẫn đúng.
Ví dụ:```python
obj.x
```có thể được chuyên môn hóa nếu CPython quan sát bố cục đối tượng ổn định. Nhưng Python cho phép đột biến:```python
obj.__dict__["x"] = 10
type(obj).x = property(...)
obj.__class__ = OtherType
```Vì vậy CPython sử dụng thẻ phiên bản, bộ bảo vệ, bộ đếm và đường dẫn dự phòng.
Quy tắc an toàn là:```text
use fast path only if guards prove assumptions still hold
otherwise use generic Python semantics
```Đây là chiến lược rộng rãi tương tự được nhiều thời gian chạy ngôn ngữ động sử dụng, nhưng CPython giữ cho bộ máy tương đối gần với trình thông dịch mã byte.
## 27.28 Mã thông dịch được tạo
CPython hiện đại không coi mọi triển khai mã byte là các trường hợp chuyển đổi viết tay trong một tệp.
Các bộ phận của trình thông dịch được tạo ra từ các định nghĩa hướng dẫn. Điều này giúp giữ cho siêu dữ liệu mã byte, hiệu ứng ngăn xếp, thông tin chuyên môn và mã điều phối nhất quán hơn.
Ý tưởng rộng:```text
instruction definitions
↓
generated opcode metadata
↓
generated dispatch support
↓
interpreter execution
```Đối với người đọc, điều này có nghĩa là nguồn gốc của sự thật không phải lúc nào cũng chỉ là tệp C được tạo cuối cùng. Bạn thường cần kiểm tra các tệp định nghĩa lệnh, các tiêu đề được tạo và kết quả đầu ra của bản dựng.
Các tệp chính xác và quy trình tạo có thể thay đổi trên các bản phát hành CPython, vì vậy hãy sử dụng cây nguồn cho phiên bản bạn đang nghiên cứu.
## 27.29 Vòng đánh giá và lệnh gọi C
Vòng đánh giá thường xuyên gọi các hàm trợ giúp C.
Ví dụ:```text
PyNumber_Add
PyObject_GetAttr
PyObject_SetAttr
PyObject_Call
PyDict_GetItem
PyObject_RichCompare
PyIter_Next
```Những người trợ giúp này có thể gọi mã Python do người dùng xác định.
Ví dụ:```python
a + b
```có thể gọi:```python
a.__add__(b)
```Và:```python
obj.name
```có thể gọi:```python
obj.__getattribute__("name")
```Vì vậy, vòng đánh giá có thể nhập lại việc thực thi Python một cách gián tiếp. Lệnh mã byte có thể gọi mã trợ giúp C, mã này có thể gọi mã Python, tạo ra một khung khác, bắt đầu thực hiện vòng lặp đánh giá khác.
Về mặt khái niệm:```text
frame A
executes BINARY_OP
calls C helper
calls user __add__
frame B
evaluation loop
```Mô hình thực thi đệ quy này là trung tâm của tính linh hoạt của Python.
## 27.30 Đệ quy và Độ sâu cuộc gọi
Python bảo vệ chống lại sự đệ quy không kiểm soát được.
Ví dụ:```python
def f():
return f()
f()
```Mỗi cuộc gọi sẽ tạo một khung Python khác. CPython theo dõi độ sâu đệ quy và tăng`RecursionError`khi vượt quá giới hạn được cấu hình.
Vòng đánh giá và máy gọi phải hợp tác với việc kiểm tra này. Nếu không có nó, mã Python đệ quy có thể làm cạn kiệt ngăn xếp C hoặc xử lý bộ nhớ.
Bạn có thể kiểm tra và điều chỉnh giới hạn:```python
import sys
print(sys.getrecursionlimit())
sys.setrecursionlimit(2000)
```Việc nâng giới hạn đệ quy cần được thực hiện cẩn thận. Giới hạn Python tồn tại một phần để bảo vệ tài nguyên thời gian chạy cấp thấp hơn.
## 27.31 Máy phát điện
Máy phát điện thay đổi vòng đời của khung.
Lệnh gọi hàm bình thường sẽ chạy cho đến khi nó trả về hoặc tăng lên. Một máy phát điện có thể tạm dừng và tiếp tục.
Ví dụ:```python
def gen():
yield 1
yield 2
```Đang gọi`gen()`không ngay lập tức chạy phần thân hàm để hoàn thành. Nó tạo ra một đối tượng trình tạo sở hữu khung treo hoặc trạng thái thực thi tương đương.
Mỗi`next()`tiếp tục thực hiện:```text
first next()
enter frame
run until yield 1
suspend frame
second next()
resume frame
run until yield 2
suspend frame
third next()
resume frame
finish function
raise StopIteration
```Vòng đánh giá phải hỗ trợ việc tạm dừng. Nó không thể đơn giản phá hủy khung hình ở`yield`.
## 27.32 Coroutine và chờ đợi
Coroutines mở rộng mô hình hệ thống treo tương tự.
Ví dụ:```python
async def fetch():
data = await read()
return data
```MỘT`await`có thể tạm dừng coroutine cho đến khi một chương trình có thể chờ đợi khác hoàn thành.
Vòng đánh giá phải hỗ trợ:```text
coroutine frame creation
suspension at await
resumption with value
resumption with exception
final return
cancellation behavior
```Do đó, việc thực thi không đồng bộ không phải là một trình thông dịch riêng biệt. Nó được xây dựng trên cùng một khung và máy móc mã byte, với các hướng dẫn và giao thức cụ thể để tạm dừng và tiếp tục.
## 27.33 Thân lớp và Thân mô-đun
Vòng đánh giá không chỉ thực hiện các chức năng.
Nó cũng thực thi các thân mô-đun và thân lớp.
Một tập tin mô-đun:```python
x = 1
def f():
return x
```được biên dịch thành một đối tượng mã cấp mô-đun. Việc nhập hoặc chạy mô-đun sẽ thực thi đối tượng mã đó.
Một câu lệnh lớp cũng thực thi mã:```python
class C:
x = 1
def method(self):
return self.x
```Thân lớp chạy trong một không gian tên được chuẩn bị cho việc xây dựng lớp. Sau khi thực thi, CPython xây dựng đối tượng lớp từ không gian tên đó.
Vì vậy, vòng đánh giá thực hiện một số loại khối:
| Loại khối | Ví dụ |
|---|---|
| Mô-đun |`.py`nội dung tập tin |
| Chức năng |`def f(): ...`|
| Lớp cơ thể |`class C: ...`|
| Lambda |`lambda x: x + 1`|
| Hiểu |`[x * 2 for x in xs]`|
| Máy phát điện |`(x for x in xs)`|
| Coroutine |`async def f(): ...`|
## 27.34 Hiểu biết
Sự hiểu biết biên dịch thành các đối tượng mã của riêng chúng trong nhiều trường hợp.
Ví dụ:```python
ys = [x * 2 for x in xs if x > 0]
```Về mặt khái niệm:```text
create list
iterate xs
for each x:
if x > 0:
append x * 2
return list
```Điều này có nghĩa là quá trình hiểu thường chạy qua một khung lồng nhau hoặc đường dẫn thực thi nội bộ chuyên biệt. Chúng có hành vi phạm vi cục bộ của riêng mình, đó là lý do tại sao các biến vòng lặp bên trong khả năng hiểu danh sách không rò rỉ vào phạm vi xung quanh trong Python 3.
Vòng đánh giá coi việc thực thi hiểu là thực thi mã byte, không phải là một dạng cú pháp đặc biệt.
## 27.35 Thực thi nhập khẩu
Việc nhập cuối cùng cũng thực thi mã byte.
Khi Python nhập một`.py`mô-đun, hệ thống nhập sẽ tìm mô-đun, đọc mã nguồn hoặc mã byte được lưu trong bộ nhớ đệm, tạo đối tượng mô-đun, sau đó thực thi đối tượng mã mô-đun.
Về mặt khái niệm:```text
import module
find spec
create module object
compile or load code object
execute code object in module namespace
```Do đó, vòng đánh giá sẽ tham gia vào quá trình nhập khẩu. Nhập mô-đun có nghĩa là chạy mã.
Đây là lý do tại sao tác dụng phụ tại thời điểm nhập khẩu xảy ra:```python
# module.py
print("imported")
import module
```Bản in chạy vì việc thực thi phần thân mô-đun là việc thực thi mã thông thường.
## 27.36 Mẫu hiệu suất
Vòng đánh giá giải thích phần lớn hiệu năng của Python.
Một hoạt động Python thường có nhiều lớp chi phí:```text
bytecode dispatch
stack manipulation
reference count updates
dynamic type checks
dictionary lookup
descriptor protocol
function call overhead
allocation
error checks
```Ví dụ:```python
obj.x + y
```có thể yêu cầu:```text
LOAD_FAST obj
LOAD_ATTR x
LOAD_FAST y
BINARY_OP +
```Mỗi hướng dẫn đều có chi phí thông dịch viên.`LOAD_ATTR`có thể liên quan đến việc tra cứu mô tả.`BINARY_OP`có thể liên quan đến việc gửi số. Số lượng tham chiếu phải được duy trì. Các lỗi phải được kiểm tra.
Đây là lý do tại sao việc di chuyển các vòng lặp nóng vào phần mở rộng C, thư viện được vector hóa hoặc các hoạt động tích hợp có thể nhanh hơn nhiều. Chúng làm giảm số lượng lệnh mã byte và các công văn động được thực thi bởi vòng đánh giá.
## 27.37 Tích hợp sẵn dưới dạng các cửa thoát vòng lặp đánh giá
Các hoạt động tích hợp có thể thực hiện một lượng lớn công việc dưới mức mã byte.
Ví dụ:```python
sum(xs)
```Vòng lặp đánh giá thực hiện lệnh gọi tới`sum`, nhưng các phần tử lặp lại có thể chạy trong C bên trong quá trình triển khai tích hợp sẵn.
So sánh:```python
total = 0
for x in xs:
total += x
```Điều này đòi hỏi nhiều hướng dẫn mã byte cho mỗi lần lặp.
Tính năng tích hợp sẵn có thể giảm chi phí cho trình thông dịch vì phần lớn công việc lặp lại diễn ra trong C.
Đây là nguyên tắc hiệu suất phổ biến của Python:```text
fewer Python bytecode instructions in hot paths usually means better performance
```## 27.38 Kiểm tra vòng đánh giá từ Python
Bạn có thể nghiên cứu mã byte với`dis`:
```python
import dis
def f(a, b):
c = a + b
return c
dis.dis(f)
```Bạn có thể kiểm tra khung:```python
import inspect
def f():
frame = inspect.currentframe()
print(frame.f_code.co_name)
print(frame.f_locals)
f()
```Bạn có thể kiểm tra độ sâu cuộc gọi:```python
import sys
def f(n):
frame = sys._getframe()
print(n, frame.f_code.co_name)
if n:
f(n - 1)
f(3)
```Bạn có thể theo dõi việc thực hiện:```python
import sys
def trace(frame, event, arg):
print(event, frame.f_code.co_name, frame.f_lineno)
return trace
def f(x):
y = x + 1
return y
sys.settrace(trace)
f(10)
sys.settrace(None)
```Những công cụ này thể hiện một phần của máy móc mà vòng đánh giá duy trì nội bộ.
## 27.39 Vòng đánh giá được đơn giản hóa
Phiên bản giảng dạy của vòng lặp có thể trông như thế này:```c
PyObject *
eval_frame(Frame *frame)
{
for (;;) {
Instruction instr = next_instruction(frame);
switch (instr.opcode) {
case OP_LOAD_CONST: {
PyObject *value = frame->code->consts[instr.arg];
push(frame, value);
break;
}
case OP_LOAD_FAST: {
PyObject *value = frame->locals[instr.arg];
if (value == NULL) {
raise_unbound_local_error();
goto error;
}
push(frame, value);
break;
}
case OP_STORE_FAST: {
PyObject *value = pop(frame);
frame->locals[instr.arg] = value;
break;
}
case OP_BINARY_ADD: {
PyObject *right = pop(frame);
PyObject *left = pop(frame);
PyObject *result = PyNumber_Add(left, right);
if (result == NULL) {
goto error;
}
push(frame, result);
break;
}
case OP_RETURN_VALUE: {
PyObject *result = pop(frame);
return result;
}
}
}
error:
return NULL;
}
```Điều này bỏ qua hầu hết các chi tiết thực tế:```text
reference ownership
specialization
inline caches
exception tables
tracing
profiling
GIL checks
pending calls
signals
generators
coroutines
debug builds
statistics
opcode prediction
deoptimization
frame materialization
```Nhưng nó nắm bắt được ý tưởng thiết yếu.
## 27.40 Những hiểu lầm phổ biến
| Hiểu lầm | Đúng mẫu |
|---|---|
| CPython thực thi văn bản nguồn trực tiếp | CPython thực thi các đối tượng mã đã biên dịch |
| Biến Python lưu trữ giá trị thô | Tên và vị trí giữ tham chiếu đến đối tượng |
| Bytecode ổn định trên các phiên bản | Bytecode là chi tiết triển khai CPython |
|`a + b`là phép cộng máy đơn giản | Đó là công văn giao thức đối tượng động, trừ khi |
| Một khung chỉ là một đối tượng truy nguyên | Một khung đang ở trạng thái thực thi tích cực |
| GIL chỉ ảnh hưởng đến chủ đề của người dùng | Nó được kết nối sâu sắc với việc thực thi trình thông dịch và an toàn đối tượng |
| Ngoại lệ chỉ là những lối đi phụ hiếm hoi | Các ngoại lệ được tích hợp vào bộ máy điều khiển luồng thông thường |
| Máy phát điện chỉ có chức năng đặc biệt | Chúng là các khung thực thi có thể tiếp tục hoặc trạng thái tương đương |
## 27.41 Đọc Nguồn Thực
Khi đọc nguồn CPython thực, hãy sử dụng thứ tự này:
1. Bắt đầu với`dis`đầu ra cho một hàm Python nhỏ.
2. Xác định các hướng dẫn mã byte.
3. Tìm định nghĩa opcode tương ứng.
4. Tìm cách triển khai trình thông dịch được tạo hoặc viết tay.
5. Thực hiện theo các lệnh gọi trợ giúp cho các hoạt động của đối tượng.
6. Theo dõi quyền sở hữu tài liệu tham khảo.
7. Theo dõi hiệu ứng ngăn xếp.
8. Theo dõi đường dẫn lỗi.
9. Kiểm tra tính chuyên môn hóa và hoạt động của bộ nhớ đệm.
10. So sánh hành vi giữa các phiên bản Python.
Một chức năng nghiên cứu tốt là:```python
def example(obj, xs):
total = 0
for x in xs:
total += obj.value + x
return total
```Hàm này chạm vào nhiều đường dẫn thông dịch:```text
local variable access
loop iteration
attribute lookup
binary operation
in-place update semantics
jump instructions
return
```Tháo rời nó, sau đó ánh xạ từng hướng dẫn tới máy thông dịch.
## 27.42 Tóm tắt chương
Vòng đánh giá là nơi mã Python được biên dịch trở thành hành vi Python đang chạy. Nó thực thi các đối tượng mã thông qua các khung, sử dụng mô hình mã byte dựa trên ngăn xếp, gửi hướng dẫn, duy trì các tham chiếu, xử lý các ngoại lệ, gọi hàm, kiểm tra các sự kiện thời gian chạy và áp dụng chuyên môn hóa khi có thể.
Vòng lặp có ý nghĩa nhỏ nhưng hậu quả lại lớn. Nó nằm ở điểm giao nhau của gần như mọi hệ thống con CPython:```text
compiler
frames
objects
types
reference counting
garbage collection
exceptions
calls
imports
generators
coroutines
tracing
profiling
threading
optimization
```Để hiểu CPython, bạn phải hiểu vòng đánh giá. Đó là cái máy bên trong cái máy.