Điều gì với bộ nhớ cache số nguyên được trình thông dịch duy trì?


82

Sau khi đi sâu vào mã nguồn của Python, tôi phát hiện ra rằng nó duy trì một mảng gồm PyInt_Objectcác loại từ int(-5)đến int(256)(@ src / Objects / intobject.c)

Một thí nghiệm nhỏ đã chứng minh điều đó:

>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False

Nhưng nếu tôi chạy những đoạn mã đó cùng nhau trong một tệp py (hoặc nối chúng bằng dấu chấm phẩy), kết quả sẽ khác:

>>> a = 257; b = 257; a is b
True

Tôi tò mò tại sao chúng vẫn là cùng một đối tượng, vì vậy tôi đi sâu hơn vào cây cú pháp và trình biên dịch, tôi đã đưa ra một hệ thống phân cấp gọi được liệt kê bên dưới:

PyRun_FileExFlags() 
    mod = PyParser_ASTFromFile() 
        node *n = PyParser_ParseFileFlagsEx() //source to cst
            parsetoke() 
                ps = PyParser_New() 
                for (;;)
                    PyTokenizer_Get() 
                    PyParser_AddToken(ps, ...)
        mod = PyAST_FromNode(n, ...)  //cst to ast
    run_mod(mod, ...)
        co = PyAST_Compile(mod, ...) //ast to CFG
            PyFuture_FromAST()
            PySymtable_Build()
            co = compiler_mod()
        PyEval_EvalCode(co, ...)
            PyEval_EvalCodeEx()

Sau đó, tôi đã thêm một số mã gỡ lỗi vào PyInt_FromLongvà trước / sau PyAST_FromNodevà thực thi một test.py:

a = 257
b = 257
print "id(a) = %d, id(b) = %d" % (id(a), id(b))

đầu ra trông giống như:

DEBUG: before PyAST_FromNode
name = a
ival = 257, id = 176046536
name = b
ival = 257, id = 176046752
name = a
name = b
DEBUG: after PyAST_FromNode
run_mod
PyAST_Compile ok
id(a) = 176046536, id(b) = 176046536
Eval ok

Điều này có nghĩa rằng trong thời gian csttới astbiến đổi, hai khác nhau PyInt_Objects được tạo ra (trên thực tế nó được thực hiện trong các ast_for_atom()chức năng), nhưng sau đó họ được sáp nhập.

Tôi cảm thấy khó hiểu nguồn trong PyAST_CompilePyEval_EvalCode, vì vậy tôi ở đây để yêu cầu trợ giúp, tôi sẽ đánh giá cao nếu ai đó đưa ra gợi ý?


2
Bạn chỉ đang cố gắng hiểu cách hoạt động của nguồn Python hay bạn đang cố gắng hiểu phần bổ sung dành cho mã được viết bằng Python là gì? Bởi vì kết quả cho mã được viết bằng Python là "đây là chi tiết triển khai, đừng bao giờ dựa vào nó xảy ra hoặc không xảy ra".
BrenBarn

Tôi sẽ không dựa vào chi tiết triển khai. Tôi chỉ tò mò và cố gắng xâm nhập vào mã nguồn.
felix021


@Blckknght cảm ơn. Tôi đã biết câu trả lời của câu hỏi đó, và tôi còn đi xa hơn thế.
felix021

Câu trả lời:


103

Python lưu trữ các số nguyên trong phạm vi [-5, 256], vì vậy dự kiến ​​rằng các số nguyên trong phạm vi đó cũng giống hệt nhau.

Những gì bạn thấy là trình biên dịch Python tối ưu hóa các ký tự giống hệt nhau khi một phần của cùng một văn bản.

Khi nhập trong Python shell, mỗi dòng là một câu lệnh hoàn toàn khác nhau, được phân tích cú pháp trong một thời điểm khác nhau, do đó:

>>> a = 257
>>> b = 257
>>> a is b
False

Nhưng nếu bạn đặt cùng một mã vào một tệp:

$ echo 'a = 257
> b = 257
> print a is b' > testing.py
$ python testing.py
True

Điều này xảy ra bất cứ khi nào trình phân tích cú pháp có cơ hội phân tích nơi các ký tự được sử dụng, ví dụ: khi xác định một hàm trong trình thông dịch tương tác:

>>> def test():
...     a = 257
...     b = 257
...     print a is b
... 
>>> dis.dis(test)
  2           0 LOAD_CONST               1 (257)
              3 STORE_FAST               0 (a)

  3           6 LOAD_CONST               1 (257)
              9 STORE_FAST               1 (b)

  4          12 LOAD_FAST                0 (a)
             15 LOAD_FAST                1 (b)
             18 COMPARE_OP               8 (is)
             21 PRINT_ITEM          
             22 PRINT_NEWLINE       
             23 LOAD_CONST               0 (None)
             26 RETURN_VALUE        
>>> test()
True
>>> test.func_code.co_consts
(None, 257)

Lưu ý cách mã đã biên dịch chứa một hằng số duy nhất cho 257.

Tóm lại, trình biên dịch mã bytecode của Python không thể thực hiện tối ưu hóa lớn (như ngôn ngữ kiểu tĩnh), nhưng nó làm được nhiều hơn bạn nghĩ. Một trong những điều này là phân tích cách sử dụng các nghĩa đen và tránh sao chép chúng.

Lưu ý rằng điều này không liên quan đến bộ đệm ẩn, vì nó cũng hoạt động đối với các phao, không có bộ đệm:

>>> a = 5.0
>>> b = 5.0
>>> a is b
False
>>> a = 5.0; b = 5.0
>>> a is b
True

Đối với các chữ phức tạp hơn, như bộ giá trị, nó "không hoạt động":

>>> a = (1,2)
>>> b = (1,2)
>>> a is b
False
>>> a = (1,2); b = (1,2)
>>> a is b
False

Nhưng các ký tự bên trong tuple được chia sẻ:

>>> a = (257, 258)
>>> b = (257, 258)
>>> a[0] is b[0]
False
>>> a[1] is b[1]
False
>>> a = (257, 258); b = (257, 258)
>>> a[0] is b[0]
True
>>> a[1] is b[1]
True

Về lý do tại sao bạn thấy rằng hai PyInt_Objectđược tạo ra, tôi đoán rằng điều này được thực hiện để tránh so sánh theo nghĩa đen. ví dụ: số 257có thể được biểu thị bằng nhiều chữ:

>>> 257
257
>>> 0x101
257
>>> 0b100000001
257
>>> 0o401
257

Trình phân tích cú pháp có hai lựa chọn:

  • Chuyển đổi các ký tự thành một số cơ sở chung trước khi tạo số nguyên và xem liệu các ký tự có tương đương hay không. sau đó tạo một đối tượng số nguyên duy nhất.
  • Tạo các đối tượng số nguyên và xem chúng có bằng nhau không. Nếu có, chỉ giữ một giá trị duy nhất và gán nó cho tất cả các ký tự, nếu không, bạn đã có các số nguyên để gán.

Có lẽ trình phân tích cú pháp Python sử dụng cách tiếp cận thứ hai, tránh viết lại mã chuyển đổi và cũng dễ mở rộng hơn (ví dụ: nó cũng hoạt động với float).


Đọc Python/ast.ctệp, hàm phân tích cú pháp tất cả các số parsenumber, hàm này gọi PyOS_strtoulđể lấy giá trị số nguyên (đối với số nguyên) và cuối cùng gọi PyLong_FromString:

    x = (long) PyOS_strtoul((char *)s, (char **)&end, 0);
    if (x < 0 && errno == 0) {
        return PyLong_FromString((char *)s,
                                 (char **)0,
                                 0);
    }

Như bạn có thể thấy ở đây, trình phân tích cú pháp không kiểm tra xem nó đã tìm thấy một số nguyên với giá trị đã cho hay chưa và do đó, điều này giải thích tại sao bạn thấy rằng hai đối tượng int được tạo và điều này cũng có nghĩa là suy đoán của tôi là đúng: trình phân tích cú pháp đầu tiên tạo ra các hằng số và chỉ sau đó tối ưu hóa bytecode để sử dụng cùng một đối tượng cho các hằng số bằng nhau.

Mã thực hiện kiểm tra này phải ở đâu đó trong Python/compile.choặc Python/peephole.c, vì đây là các tệp biến AST thành mã bytecode.

Đặc biệt, compiler_add_ochức năng có vẻ là một trong những chức năng đó. Có bình luận này trong compiler_lambda:

/* Make None the first constant, so the lambda can't have a
   docstring. */
if (compiler_add_o(c, c->u->u_consts, Py_None) < 0)
    return 0;

Vì vậy, nó có vẻ như compiler_add_ođược sử dụng để chèn các hằng số cho các hàm / lambdas, v.v. compiler_add_oHàm lưu trữ các hằng số vào một dictđối tượng và từ đó ngay lập tức các hằng số bằng nhau sẽ rơi vào cùng một vị trí, dẫn đến một hằng số duy nhất trong bytecode cuối cùng.


Cảm ơn. Tôi biết tại sao intepreter lại làm điều này, và tôi cũng đã thử nghiệm các chuỗi trước đây, hoạt động giống như int và float, và tôi cũng đã in cây cú pháp bằng compiler.parse () hiển thị hai Const (257). Tôi chỉ tự hỏi khi nào và như thế nào trong mã nguồn ... Hơn nữa, kiểm tra tôi đã làm ở trên cho thấy rằng intepreter đã tạo ra hai PyInt_Object cho a và b, vì vậy việc hợp nhất chúng thực sự rất ít ý nghĩa (ngoài việc tiết kiệm bộ nhớ).
felix021

@ felix021 Tôi đã cập nhật lại câu trả lời của mình. Tôi đã tìm thấy hai int được tạo ở đâu và tôi biết việc tối ưu hóa xảy ra trong tệp nào, mặc dù tôi vẫn không tìm thấy dòng mã chính xác xử lý điều đó.
Bakuriu

Cảm ơn rất nhiều! Tôi đã cẩn thận xem qua compile.c, chuỗi gọi là compiler_visit_stmt -> VISIT (c, expr, e) -> compiler_visit_expr (c, e) -> ADDOP_O (c, LOAD_CONST, e-> v.Num.n, consts) -> compiler_addop_o (c, LOAD_CONSTS, c-> u-> u_consts, e-> v.Num.n) -> compiler_add_o (c, c-> u-> u_consts, e-> v.Num.n). trong compoler_add_o (), python sẽ cố gắng if-not-find-then-set PyTuple (PyIntObject n, PyInt_Type) làm khóa vào c-> u-> u_consts và trong khi tính toán băm của tuple đó, chỉ int thực tế giá trị được sử dụng, vì vậy chỉ một PyInt_Object sẽ được chèn vào u_consts dict.
felix021,

Tôi nhận được Falsethực hiện a = 5.0; b = 5.0; print (a is b)cả với py2 và py3 trên win7
zhangxaochen

1
@zhangxaochen Bạn đã viết hai câu lệnh trên cùng một dòng hay trên các dòng khác nhau trong trình thông dịch tương tác? Dù sao, các phiên bản khác nhau của python có thể tạo ra các hành vi khác nhau. Trên máy tính của tôi nó không kết quả trong True(chỉ cần kiểm tra lại bây giờ). Các tối ưu hóa không đáng tin cậy vì chúng chỉ là chi tiết triển khai, vì vậy điều đó không làm mất hiệu lực của điểm tôi muốn đưa ra trong câu trả lời của mình. Cũng compile('a=5.0;b=5.0', '<stdin>', 'exec')).co_constscho thấy rằng chỉ có một 5.0hằng số (trong python3.3 trên linux).
Bakuriu
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.