ทำไมผลตอบแทนต้นช้ากว่าอย่างอื่น?


180

นี่คือคำถามที่ติดตามคำตอบผมให้ไม่กี่วันกลับ แก้ไข:ดูเหมือนว่า OP ของคำถามนั้นได้ใช้รหัสที่ฉันโพสต์ไปหาเขาเพื่อถามคำถามเดียวกันแต่ฉันไม่รู้ตัว ขอโทษ. คำตอบที่ให้นั้นแตกต่างกัน!

ฉันสังเกตเห็นว่า:

>>> def without_else(param=False):
...     if param:
...         return 1
...     return 0
>>> def with_else(param=False):
...     if param:
...         return 1
...     else:
...         return 0
>>> from timeit import Timer as T
>>> T(lambda : without_else()).repeat()
[0.3011460304260254, 0.2866089344024658, 0.2871549129486084]
>>> T(lambda : with_else()).repeat()
[0.27536892890930176, 0.2693932056427002, 0.27011704444885254]
>>> T(lambda : without_else(True)).repeat()
[0.3383951187133789, 0.32756996154785156, 0.3279120922088623]
>>> T(lambda : with_else(True)).repeat()
[0.3305950164794922, 0.32186388969421387, 0.3209099769592285]

... หรือกล่าวอีกนัยหนึ่ง: การมีส่วนelseคำสั่งเร็วขึ้นโดยไม่คำนึงถึงifเงื่อนไขที่ถูกกระตุ้นหรือไม่

ฉันคิดว่ามันเกี่ยวข้องกับ bytecode ที่สร้างขึ้นโดยสองคนนี้ แต่ทุกคนสามารถยืนยัน / อธิบายโดยละเอียดได้หรือไม่?

แก้ไข:ดูเหมือนว่าทุกคนไม่สามารถกำหนดเวลาของฉันได้ดังนั้นฉันคิดว่ามันอาจมีประโยชน์ในการให้ข้อมูลบางอย่างในระบบของฉัน ฉันใช้ Ubuntu 11.10 64 บิตโดยติดตั้งไพ ธ อนเริ่มต้นไว้ pythonสร้างข้อมูลรุ่นต่อไปนี้:

Python 2.7.2+ (default, Oct  4 2011, 20:06:09) 
[GCC 4.6.1] on linux2

นี่คือผลลัพธ์ของการถอดแยกชิ้นส่วนใน Python 2.7:

>>> dis.dis(without_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  4     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
>>> dis.dis(with_else)
  2           0 LOAD_FAST                0 (param)
              3 POP_JUMP_IF_FALSE       10

  3           6 LOAD_CONST               1 (1)
              9 RETURN_VALUE        

  5     >>   10 LOAD_CONST               2 (0)
             13 RETURN_VALUE        
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        

1
มีคำถามเหมือนกันดังนั้นฉันไม่สามารถหาได้ในตอนนี้ พวกเขาตรวจสอบรหัสไบต์ที่สร้างขึ้นและมีอีกหนึ่งขั้นตอน ความแตกต่างที่สังเกตได้นั้นขึ้นอยู่กับผู้ทดสอบ (เครื่องจักร, SO .. ) บางครั้งการค้นหาความแตกต่างเพียงเล็กน้อยเท่านั้น
joaquin

3
เมื่อวันที่ 3.x ทั้งการผลิตเหมือน bytecode ประหยัดสำหรับบางคนที่ไม่สามารถเข้าถึงรหัส ( LOAD_CONST(None); RETURN_VALUE- แต่ตามที่ระบุไว้ก็ไม่ถึง) with_elseในตอนท้ายของ ฉันสงสัยรหัสตายทำให้ฟังก์ชั่นเร็วขึ้น มีคนให้dis2.7 หรือไม่?

4
ฉันไม่สามารถทำซ้ำสิ่งนี้ ทำงานกับelseและFalseช้าที่สุดของพวกเขาทั้งหมด (152ns) เร็วที่สุดที่สองคือTrueไม่มีelse(143ns) และอีกสองคนนั้นเหมือนกัน (137ns และ 138ns) ฉันไม่ได้ใช้พารามิเตอร์เริ่มต้นและวัดด้วย%timeitใน iPython
rplnt

ฉันไม่สามารถกำหนดเวลาเหล่านั้นซ้ำได้บางครั้ง with_else จะเร็วขึ้นบางครั้งนี่เป็นรุ่น without_else ดูเหมือนว่าพวกเขาจะคล้ายกันมากสำหรับฉัน ...
Cédric Julien

1
เพิ่มผลลัพธ์ของการถอดชิ้นส่วน ฉันใช้ Ubuntu 11.10, 64-bit, stock Python 2.7 - การกำหนดค่าเช่นเดียวกับ @mac ฉันยังเห็นพ้องว่าwith_elseเป็น observably ได้เร็วขึ้น
Chris Morgan

คำตอบ:


387

นี่คือการคาดเดาที่บริสุทธิ์และฉันไม่ได้คิดวิธีง่ายๆในการตรวจสอบว่ามันถูกต้องหรือไม่ แต่ฉันมีทฤษฎีสำหรับคุณ

ฉันลองใช้โค้ดของคุณและรับผลลัพธ์แบบเดียวกันwithout_else()ซ้ำช้ากว่าwith_else():

>>> T(lambda : without_else()).repeat()
[0.42015745017874906, 0.3188967452567226, 0.31984281521812363]
>>> T(lambda : with_else()).repeat()
[0.36009842032996175, 0.28962249392031936, 0.2927151355828528]
>>> T(lambda : without_else(True)).repeat()
[0.31709728471076915, 0.3172671387005721, 0.3285821242644147]
>>> T(lambda : with_else(True)).repeat()
[0.30939889008243426, 0.3035132258429485, 0.3046679117038593]

พิจารณาว่า bytecode เหมือนกันความแตกต่างเพียงอย่างเดียวคือชื่อของฟังก์ชัน โดยเฉพาะอย่างยิ่งการทดสอบเวลาจะค้นหาชื่อทั่วโลก ลองเปลี่ยนชื่อwithout_else()และความแตกต่างจะหายไป:

>>> def no_else(param=False):
    if param:
        return 1
    return 0

>>> T(lambda : no_else()).repeat()
[0.3359846013948413, 0.29025818923918223, 0.2921801513879245]
>>> T(lambda : no_else(True)).repeat()
[0.3810395594970828, 0.2969634408842694, 0.2960104566362247]

ฉันเดาว่าwithout_elseมีการชนกันอย่างยุ่งเหยิงกับสิ่งอื่นในglobals()การค้นหาชื่อทั่วโลกช้าลงเล็กน้อย

แก้ไข : พจนานุกรมที่มี 7 หรือ 8 ปุ่มอาจมี 32 สล็อตดังนั้นพื้นฐานดังwithout_elseกล่าวจึงมีการชนกันของแฮชกับ__builtins__:

>>> [(k, hash(k) % 32) for k in globals().keys() ]
[('__builtins__', 8), ('with_else', 9), ('__package__', 15), ('without_else', 8), ('T', 21), ('__name__', 25), ('no_else', 28), ('__doc__', 29)]

หากต้องการชี้แจงวิธีการแปลงแป้นพิมพ์ทำงาน:

__builtins__ แฮชที่ -1196389688 ซึ่งทำให้โมดูโล่ลดขนาดตาราง (32) หมายความว่ามันถูกเก็บไว้ในช่อง # 8 ของตาราง

without_elseแฮชที่ 505688136 ซึ่งลดโมดูโล 32 คือ 8 จึงมีการชน ในการแก้ไข Python จะคำนวณ:

เริ่มต้นด้วย:

j = hash % 32
perturb = hash

ทำซ้ำจนกว่าเราจะพบช่องฟรี:

j = (5*j) + 1 + perturb;
perturb >>= 5;
use j % 2**i as the next table index;

ซึ่งให้ 17 เพื่อใช้เป็นดัชนีถัดไป โชคดีที่ฟรีเพื่อวนซ้ำเพียงครั้งเดียว ขนาดตารางแฮชเป็นอำนาจของ 2 เพื่อให้2**iมีขนาดของตารางแฮชที่เป็นจำนวนบิตที่ใช้จากมูลค่ากัญชาij

โพรบแต่ละตัวในตารางสามารถค้นหาหนึ่งในเหล่านี้:

  • ช่องว่างเปล่าในกรณีนั้นการตรวจสอบจะหยุดและเรารู้ว่าค่าไม่ได้อยู่ในตาราง

  • สล็อตไม่ได้ใช้ แต่ถูกใช้ในอดีตซึ่งเราจะลองใช้ค่าถัดไปที่คำนวณตามข้างต้น

  • สล็อตเต็ม แต่ค่าแฮชแบบเต็มที่เก็บไว้ในตารางไม่เหมือนกับแฮชของคีย์ที่เรากำลังค้นหา (นั่นคือสิ่งที่เกิดขึ้นในกรณีของ__builtins__vs without_else)

  • สล็อตเต็มและมีค่าแฮชตามที่เราต้องการจากนั้น Python ตรวจสอบเพื่อดูว่าคีย์และวัตถุที่เราค้นหาเป็นวัตถุเดียวกันหรือไม่ (ซึ่งในกรณีนี้พวกเขาจะเป็นเพราะสตริงสั้น ๆ ที่อาจเป็นตัวระบุได้ ตัวระบุที่เหมือนกันใช้สตริงเดียวกันแน่นอน)

  • ในที่สุดเมื่อสล็อตเต็มแฮชจะจับคู่ แต่คีย์ไม่ใช่วัตถุที่เหมือนกันจากนั้น Python จะลองเปรียบเทียบมันเพื่อความเท่าเทียมกันเท่านั้น ค่อนข้างช้า แต่ในกรณีของการค้นหาชื่อไม่ควรเกิดขึ้นจริง


9
@Chris ไม่มีความยาวของสตริงไม่ควรสำคัญ ครั้งแรกที่คุณแฮชสตริงจะต้องใช้เวลาตามสัดส่วนกับความยาวสตริง แต่แฮชที่คำนวณได้จะถูกแคชในวัตถุสตริงดังนั้นแฮชที่ตามมาคือ O (1)
ดันแคน

1
อ่าฉันไม่ทราบเกี่ยวกับแคช แต่มันก็สมเหตุสมผล
Chris Eberle

9
ที่น่าสนใจ! ฉันขอเรียกคุณ Sherlock ได้ไหม ;) อย่างไรก็ตามฉันหวังว่าฉันจะไม่ลืมที่จะให้คะแนนเพิ่มเติมแก่คุณทันทีที่คำถามมีสิทธิ์
Voo

4
@mac ไม่มาก ฉันจะเพิ่มเล็กน้อยเกี่ยวกับการแก้ไขแฮช (กำลังจะบีบลงในความคิดเห็น แต่มันน่าสนใจกว่าที่ฉันคิด)
Duncan

6
@Duncan - ขอบคุณมากที่สละเวลาเพื่ออธิบายกระบวนการแฮช คำตอบที่ดีที่สุด! :)
แม็ค
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.