Tôi đã làm việc trên một lớp đơn giản mà kéo dài dict
, và tôi nhận ra rằng chìa khóa tra cứu và sử dụng pickle
là rất chậm.
Tôi nghĩ rằng đó là một vấn đề với lớp học của tôi, vì vậy tôi đã làm một số điểm chuẩn tầm thường:
(venv) marco@buzz:~/sources/python-frozendict/test$ python --version
Python 3.9.0a0
(venv) marco@buzz:~/sources/python-frozendict/test$ sudo pyperf system tune --affinity 3
[sudo] password for marco:
Tune the system configuration to run benchmarks
Actions
=======
CPU Frequency: Minimum frequency of CPU 3 set to the maximum frequency
System state
============
CPU: use 1 logical CPUs: 3
Perf event: Maximum sample rate: 1 per second
ASLR: Full randomization
Linux scheduler: No CPU is isolated
CPU Frequency: 0-3=min=max=2600 MHz
CPU scaling governor (intel_pstate): performance
Turbo Boost (intel_pstate): Turbo Boost disabled
IRQ affinity: irqbalance service: inactive
IRQ affinity: Default IRQ affinity: CPU 0-2
IRQ affinity: IRQ affinity: IRQ 0,2=CPU 0-3; IRQ 1,3-17,51,67,120-131=CPU 0-2
Power supply: the power cable is plugged
Advices
=======
Linux scheduler: Use isolcpus=<cpu list> kernel parameter to isolate CPUs
Linux scheduler: Use rcu_nocbs=<cpu list> kernel parameter (with isolcpus) to not schedule RCU on isolated CPUs
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
x = {0:0, 1:1, 2:2, 3:3, 4:4}
' 'x[4]'
.........................................
Mean +- std dev: 35.2 ns +- 1.8 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
class A(dict):
pass
x = A({0:0, 1:1, 2:2, 3:3, 4:4})
' 'x[4]'
.........................................
Mean +- std dev: 60.1 ns +- 2.5 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
x = {0:0, 1:1, 2:2, 3:3, 4:4}
' '5 in x'
.........................................
Mean +- std dev: 31.9 ns +- 1.4 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python -m pyperf timeit --rigorous --affinity 3 -s '
class A(dict):
pass
x = A({0:0, 1:1, 2:2, 3:3, 4:4})
' '5 in x'
.........................................
Mean +- std dev: 64.7 ns +- 5.4 ns
(venv) marco@buzz:~/sources/python-frozendict/test$ python
Python 3.9.0a0 (heads/master-dirty:d8ca2354ed, Oct 30 2019, 20:25:01)
[GCC 9.2.1 20190909] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from timeit import timeit
>>> class A(dict):
... def __reduce__(self):
... return (A, (dict(self), ))
...
>>> timeit("dumps(x)", """
... from pickle import dumps
... x = {0:0, 1:1, 2:2, 3:3, 4:4}
... """, number=10000000)
6.70694484282285
>>> timeit("dumps(x)", """
... from pickle import dumps
... x = A({0:0, 1:1, 2:2, 3:3, 4:4})
... """, number=10000000, globals={"A": A})
31.277778962627053
>>> timeit("loads(x)", """
... from pickle import dumps, loads
... x = dumps({0:0, 1:1, 2:2, 3:3, 4:4})
... """, number=10000000)
5.767975459806621
>>> timeit("loads(x)", """
... from pickle import dumps, loads
... x = dumps(A({0:0, 1:1, 2:2, 3:3, 4:4}))
... """, number=10000000, globals={"A": A})
22.611666693352163
Kết quả thực sự là một bất ngờ. Trong khi tra cứu quan trọng là 2x chậm hơn, pickle
là 5x chậm hơn.
Làm sao có thể? Các phương thức khác, như get()
, __eq__()
và __init__()
, lặp đi lặp lại keys()
, values()
và items()
nhanh như dict
.
EDIT : Tôi đã xem mã nguồn của Python 3.9 và Objects/dictobject.c
dường như __getitem__()
phương thức này được thực hiện bởi dict_subscript()
. Và dict_subscript()
làm chậm các lớp con chỉ khi khóa bị thiếu, vì lớp con có thể thực hiện __missing__()
và nó cố gắng xem nó có tồn tại không. Nhưng điểm chuẩn là với một khóa hiện có.
Nhưng tôi nhận thấy một điều: __getitem__()
được định nghĩa với cờ METH_COEXIST
. Ngoài ra __contains__()
, phương pháp khác chậm hơn gấp 2 lần, có cùng một cờ. Từ tài liệu chính thức :
Phương pháp sẽ được tải thay cho các định nghĩa hiện có. Không có METH_COEXIST, mặc định là bỏ qua các định nghĩa lặp lại. Do các trình bao bọc vị trí được tải trước bảng phương thức, ví dụ, sự tồn tại của vị trí sq_contains sẽ tạo ra một phương thức được bao bọc có tên chứa () và loại trừ việc tải một PyCFunction tương ứng có cùng tên. Với cờ được xác định, PyCFunction sẽ được tải thay cho đối tượng trình bao bọc và sẽ cùng tồn tại với vị trí. Điều này rất hữu ích vì các lệnh gọi đến PyCFifts được tối ưu hóa nhiều hơn các lệnh gọi đối tượng trình bao bọc.
Vì vậy, nếu tôi hiểu chính xác, trên lý thuyết METH_COEXIST
sẽ tăng tốc mọi thứ, nhưng dường như nó có tác dụng ngược lại. Tại sao?
EDIT 2 : Tôi phát hiện ra một cái gì đó nhiều hơn.
__getitem__()
và __contains()__
được gắn cờ là METH_COEXIST
, bởi vì chúng được khai báo trong PyDict_Type hai lần.
Cả hai đều có mặt, một lần, trong khe tp_methods
, nơi chúng được tuyên bố rõ ràng là __getitem__()
và __contains()__
. Tuy nhiên, tài liệu chính thức nói rằng tp_methods
đang không được thừa kế bởi các lớp con.
Vì vậy, một lớp con dict
không gọi __getitem__()
, nhưng gọi lớp con mp_subscript
. Thật vậy, mp_subscript
được chứa trong khe tp_as_mapping
, cho phép một lớp con kế thừa các tập con của nó.
Vấn đề là cả hai __getitem__()
và mp_subscript
sử dụng cùng một chức năng , dict_subscript
. Có thể đó chỉ là cách nó được thừa kế làm nó chậm lại?
len()
, ví dụ, không phải là chậm hơn 2 lần mà có cùng tốc độ?
len
nên có một đường dẫn nhanh cho các loại trình tự tích hợp. Tôi không nghĩ rằng tôi có thể đưa ra một câu trả lời thích hợp cho câu hỏi của bạn, nhưng đó là một câu hỏi hay, vì vậy hy vọng ai đó hiểu biết nhiều hơn về nội bộ Python hơn tôi sẽ trả lời nó.
__contains__
thực hiện rõ ràng là chặn logic được sử dụng để kế thừa sq_contains
.
dict
và nếu vậy, hãy gọi trực tiếp triển khai C thay vì tìm kiếm__getitem__
phương thức từ lớp của đối tượng. Do đó, mã của bạn thực hiện hai tra cứu chính tả, mã đầu tiên cho khóa'__getitem__'
trong từ điển củaA
các thành viên của lớp , do đó, nó có thể được dự kiến sẽ chậm hơn khoảng hai lần. Lờipickle
giải thích có lẽ khá giống nhau.