Kết quả của tôi tương tự như của bạn: mã sử dụng các biến trung gian khá nhất quán nhanh hơn ít nhất 10-20% trong Python 3.4. Tuy nhiên, khi tôi sử dụng IPython trên cùng một trình thông dịch Python 3.4, tôi nhận được những kết quả sau:
In [1]: %timeit -n10000 -r20 tuple(range(2000)) == tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop
In [2]: %timeit -n10000 -r20 a = tuple(range(2000)); b = tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop
Đáng chú ý, tôi không bao giờ đạt được thậm chí gần với 74,2 µs cho trước đây khi tôi sử dụng -mtimeit
từ dòng lệnh.
Vì vậy, Heisenbug này hóa ra là một cái gì đó khá thú vị. Tôi quyết định chạy lệnh với strace
và thực sự có điều gì đó khó hiểu đang xảy ra:
% strace -o withoutvars python3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = tuple(range(2000)); b = tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149
Bây giờ đó là một lý do chính đáng cho sự khác biệt. Mã không sử dụng biến khiến lệnh mmap
gọi hệ thống được gọi nhiều hơn gần 1000 lần so với mã sử dụng biến trung gian.
Các withoutvars
đầy mmap
/munmap
cho một vùng 256k; những dòng này được lặp đi lặp lại nhiều lần:
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
Cuộc mmap
gọi dường như đến từ hàm _PyObject_ArenaMmap
from Objects/obmalloc.c
; những obmalloc.c
cũng chứa các vĩ mô ARENA_SIZE
, mà là #define
d là (256 << 10)
(có nghĩa là 262144
); tương tự như vậymunmap
trận đấu với_PyObject_ArenaMunmap
from obmalloc.c
.
obmalloc.c
nói rằng
Trước Python 2.5, các đấu trường chưa bao giờ được chỉnh sửa free()
. Bắt đầu với Python 2.5, chúng tôi cố gắngfree()
đấu trường và sử dụng một số chiến lược kinh nghiệm nhẹ để tăng khả năng các đấu trường cuối cùng có thể được giải phóng.
Do đó, những kinh nghiệm này và thực tế là trình cấp phát đối tượng Python giải phóng các vùng trống này ngay khi chúng được làm trống dẫn đến việc python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
kích hoạt hành vi bệnh lý trong đó một vùng bộ nhớ 256 kiB được cấp phát lại và giải phóng nhiều lần; và phân bổ này xảy ra với mmap
/ munmap
, tương đối tốn kém khi chúng là các lệnh gọi hệ thống - hơn nữa,mmap
vớiMAP_ANONYMOUS
yêu cầu rằng các trang mới được ánh xạ phải được đánh dấu bằng 0 - mặc dù Python sẽ không quan tâm.
Hành vi không xuất hiện trong mã sử dụng các biến trung gian, vì nó đang sử dụng nhiều bộ nhớ hơn một chút và không có vùng bộ nhớ nào có thể được giải phóng vì một số đối tượng vẫn được cấp phát trong đó. Đó là bởi vì timeit
nó sẽ làm cho nó thành một vòng lặp không giống như
for n in range(10000)
a = tuple(range(2000))
b = tuple(range(2000))
a == b
Bây giờ hành vi là cả hai a
và b
sẽ tiếp tục bị ràng buộc cho đến khi chúng * được gán lại, vì vậy trong lần lặp thứ hai, tuple(range(2000))
sẽ phân bổ một bộ thứ 3 và việc gán a = tuple(...)
sẽ làm giảm số lượng tham chiếu của bộ cũ, khiến nó được giải phóng và tăng số tham chiếu của bộ tuple mới; thì điều tương tự cũng xảy ra với b
. Do đó, sau lần lặp đầu tiên luôn có ít nhất 2 trong số các bộ giá trị này, nếu không phải là 3, vì vậy việc ném không xảy ra.
Đáng chú ý nhất là không thể đảm bảo rằng mã sử dụng các biến trung gian luôn nhanh hơn - thực sự trong một số thiết lập, việc sử dụng các biến trung gian sẽ dẫn đến các mmap
cuộc gọi bổ sung , trong khi mã so sánh trực tiếp các giá trị trả về có thể tốt.
Có người hỏi rằng tại sao điều này lại xảy ra, khi timeit
tắt tính năng thu gom rác. Nó thực sự là sự thật rằng timeit
có phải nó :
Ghi chú
Theo mặc định, timeit()
tạm thời tắt tính năng thu gom rác trong thời gian. Ưu điểm của phương pháp này là nó làm cho các thời gian độc lập có thể so sánh được với nhau. Nhược điểm này là GC có thể là một thành phần quan trọng của hiệu suất của chức năng được đo. Nếu vậy, GC có thể được bật lại dưới dạng câu lệnh đầu tiên trong chuỗi thiết lập. Ví dụ:
Tuy nhiên, trình thu gom rác của Python chỉ ở đó để thu hồi rác theo chu kỳ , tức là tập hợp các đối tượng có tham chiếu tạo thành chu trình. Nó không phải là trường hợp ở đây; thay vào đó các đối tượng này được giải phóng ngay lập tức khi số lượng tham chiếu giảm xuống không.
dis.dis("tuple(range(2000)) == tuple(range(2000))")
vớidis.dis("a = tuple(range(2000)); b = tuple(range(2000)); a==b")
. Trong cấu hình của tôi, đoạn mã thứ hai thực sự chứa tất cả mã bytecode từ đoạn mã đầu tiên và một số hướng dẫn bổ sung. Thật khó tin rằng nhiều lệnh bytecode hơn dẫn đến việc thực thi nhanh hơn. Có thể đó là một số lỗi trong phiên bản Python cụ thể?