ผมทำงานในระดับที่เรียบง่ายที่ขยายdict
และฉันรู้ว่าการค้นหาที่สำคัญและการใช้pickle
มีมากช้า
ฉันคิดว่ามันเป็นปัญหากับชั้นเรียนของฉันดังนั้นฉันจึงทำการทดสอบเล็กน้อย:
(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
ผลลัพธ์เป็นสิ่งที่น่าประหลาดใจจริงๆ ในขณะที่การค้นหาที่สำคัญคือ 2x ช้าpickle
เป็น5xช้าลง
สิ่งนี้จะเป็นอย่างไร วิธีการอื่น ๆ เช่นget()
, __eq__()
และ__init__()
และย้ำกว่าkeys()
, values()
และเป็นเร็วที่สุดเท่าที่items()
dict
แก้ไข : ฉันได้ดูรหัสที่มาของงูใหญ่ 3.9 และในObjects/dictobject.c
ดูเหมือนว่าวิธีการที่ดำเนินการโดย__getitem__()
dict_subscript()
และdict_subscript()
ทำให้คลาสย่อยช้าลงหากคีย์หายไปเนื่องจากคลาสย่อยสามารถใช้งานได้__missing__()
และจะพยายามดูว่ามีอยู่หรือไม่ แต่เกณฑ์มาตรฐานก็คือรหัสที่มีอยู่
แต่ผมสังเกตเห็นบางสิ่งบางอย่างที่ถูกกำหนดให้กับธง__getitem__()
METH_COEXIST
และ__contains__()
อีกวิธีที่ช้ากว่า 2x นั้นมีค่าสถานะเดียวกัน จากเอกสารอย่างเป็นทางการ :
วิธีการจะถูกโหลดแทนคำจำกัดความที่มีอยู่ หากไม่มี METH_COEXIST ค่าดีฟอลต์คือข้ามคำจำกัดความซ้ำ ตั้งแต่ห่อสล็อตโหลดก่อนตารางวิธีการดำรงอยู่ของช่องเสียบ sq_contains เช่นจะสร้างวิธีการห่อชื่อ มี () และดักคอโหลด PyCFunction ที่เกี่ยวข้องที่มีชื่อเดียวกัน ด้วยการกำหนดค่าสถานะ PyCFunction จะถูกโหลดแทนที่วัตถุ wrapper และจะอยู่ร่วมกับสล็อต สิ่งนี้มีประโยชน์เนื่องจากการเรียก PyCFunctions ได้รับการปรับให้เหมาะสมมากกว่าการเรียกอ็อบเจ็กต์ wrapper
ดังนั้นหากฉันเข้าใจถูกต้องตามทฤษฎีแล้วMETH_COEXIST
ควรเร่งสิ่งต่าง ๆ แต่ดูเหมือนว่าจะมีผลตรงกันข้าม ทำไม?
แก้ไข 2 : ฉันค้นพบบางสิ่งเพิ่มเติม
__getitem__()
และ__contains()__
ถูกตั้งค่าสถานะเป็นMETH_COEXIST
เนื่องจากมีการประกาศใน PyDict_Type สองครั้ง
พวกเขาทั้งสองปัจจุบันครั้งหนึ่งในช่องtp_methods
ที่พวกเขามีการประกาศอย่างชัดเจนว่าและ__getitem__()
__contains()__
แต่เอกสารที่เป็นทางการกล่าวว่าtp_methods
จะไม่ได้รับการถ่ายทอดจาก subclasses
ดังนั้น subclass ของdict
ไม่เรียก__getitem__()
แต่เรียก mp_subscript
subslot แท้จริงแล้วmp_subscript
มีอยู่ในสล็อตtp_as_mapping
ที่อนุญาตให้คลาสย่อยรับช่วงย่อย
ปัญหาคือทั้งสอง__getitem__()
และmp_subscript
ใช้ฟังก์ชั่นเดียวกัน , dict_subscript
. เป็นไปได้ไหมว่ามันเป็นเพียงวิธีการสืบทอดที่ทำให้ช้าลง?
len()
ตัวอย่างเช่นไม่ช้ากว่า 2x แต่มีความเร็วเท่ากัน?
len
ควรมีเส้นทางที่รวดเร็วสำหรับชุดลำดับในตัว ฉันไม่คิดว่าฉันสามารถตอบคำถามของคุณได้อย่างถูกต้อง แต่เป็นคำถามที่ดีดังนั้นหวังว่าใครบางคนที่มีความรู้เกี่ยวกับ Python ภายในมากกว่าที่ฉันจะตอบ
__contains__
sq_contains
dict
และถ้าเป็นเช่นนั้นเรียกการใช้งาน C โดยตรงแทนที่จะมองหา__getitem__
วิธีจาก คลาสของวัตถุ ดังนั้นโค้ดของคุณจะทำการค้นหาแบบ Dict สองครั้งอันแรกสำหรับคีย์'__getitem__'
ในพจนานุกรมของA
สมาชิกคลาสดังนั้นจึงคาดว่าจะช้ากว่าประมาณสองเท่าpickle
คำอธิบายอาจจะค่อนข้างคล้าย