“ x <y <z” เร็วกว่า“ x <y และ y <z” หรือไม่


129

จากหน้านี้เรารู้ว่า:

การเปรียบเทียบแบบล่ามโซ่จะเร็วกว่าการใช้ตัวandดำเนินการ เขียนx < y < zแทนx < y and y < z.

อย่างไรก็ตามฉันได้รับผลลัพธ์ที่แตกต่างจากการทดสอบข้อมูลโค้ดต่อไปนี้:

$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y < z"
1000000 loops, best of 3: 0.322 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.8" "x < y and y < z"
1000000 loops, best of 3: 0.22 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y < z"
1000000 loops, best of 3: 0.279 usec per loop
$ python -m timeit "x = 1.2" "y = 1.3" "z = 1.1" "x < y and y < z"
1000000 loops, best of 3: 0.215 usec per loop

มันดูเหมือนว่าจะเร็วกว่าx < y and y < z ทำไม?x < y < z

หลังจากค้นหาโพสต์บางส่วนในเว็บไซต์นี้ (เช่นนี้ ) ฉันรู้ว่า "ประเมินเพียงครั้งเดียว" เป็นกุญแจสำคัญสำหรับx < y < zแต่ฉันก็ยังสับสน ในการศึกษาเพิ่มเติมฉันถอดฟังก์ชั่นทั้งสองนี้โดยใช้dis.dis:

import dis

def chained_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y < z

def and_compare():
        x = 1.2
        y = 1.3
        z = 1.1
        x < y and y < z

dis.dis(chained_compare)
dis.dis(and_compare)

และผลลัพธ์คือ:

## chained_compare ##

  4           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

  5           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

  6          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

  7          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 DUP_TOP
             25 ROT_THREE
             26 COMPARE_OP               0 (<)
             29 JUMP_IF_FALSE_OR_POP    41
             32 LOAD_FAST                2 (z)
             35 COMPARE_OP               0 (<)
             38 JUMP_FORWARD             2 (to 43)
        >>   41 ROT_TWO
             42 POP_TOP
        >>   43 POP_TOP
             44 LOAD_CONST               0 (None)
             47 RETURN_VALUE

## and_compare ##

 10           0 LOAD_CONST               1 (1.2)
              3 STORE_FAST               0 (x)

 11           6 LOAD_CONST               2 (1.3)
              9 STORE_FAST               1 (y)

 12          12 LOAD_CONST               3 (1.1)
             15 STORE_FAST               2 (z)

 13          18 LOAD_FAST                0 (x)
             21 LOAD_FAST                1 (y)
             24 COMPARE_OP               0 (<)
             27 JUMP_IF_FALSE_OR_POP    39
             30 LOAD_FAST                1 (y)
             33 LOAD_FAST                2 (z)
             36 COMPARE_OP               0 (<)
        >>   39 POP_TOP
             40 LOAD_CONST               0 (None)

ดูเหมือนว่ามีคำสั่งปิดบังน้อยกว่าx < y and y < z x < y < zควรพิจารณาx < y and y < zเร็วกว่าx < y < zไหม

ทดสอบด้วย Python 2.7.6 บน Intel (R) Xeon (R) CPU E5640 @ 2.67GHz


8
คำสั่งที่แยกสัญญาณออกมากขึ้นไม่ได้หมายความว่าจะซับซ้อนมากขึ้นหรือโค้ดที่ช้า อย่างไรก็ตามเมื่อเห็นtimeitการทดสอบของคุณฉันสนใจสิ่งนี้
Marco Bonelli

6
ฉันคิดว่าความแตกต่างของความเร็วจากการ "ประเมินครั้งเดียว" เกิดขึ้นเมื่อyใดไม่ใช่แค่การค้นหาตัวแปร แต่เป็นกระบวนการที่มีราคาแพงกว่าเช่นการเรียกฟังก์ชัน IE 10 < max(range(100)) < 15เร็วกว่า10 < max(range(100)) and max(range(100)) < 15เพราะmax(range(100))ถูกเรียกครั้งเดียวสำหรับการเปรียบเทียบทั้งสอง
zehnpaard

2
@MarcoBonelli จะเกิดขึ้นเมื่อโค้ดที่ถอดประกอบ 1) ไม่มีลูปและ 2) ทุกๆไบต์โค้ดนั้นเร็วมากเพราะ ณ จุดนั้นค่าใช้จ่ายของเมนลูปจะมีความสำคัญ
Bakuriu

2
การทำนายสาขาอาจทำให้การทดสอบของคุณยุ่งเหยิง
Corey Ogburn

2
@zehnpaard ฉันเห็นด้วยกับคุณ เมื่อ "y" เป็นมากกว่าค่าธรรมดา (เช่นการเรียกใช้ฟังก์ชันหรือการคำนวณ) ฉันคาดหวังว่า "y" จะได้รับการประเมินหนึ่งครั้งใน x <y <z จะมีผลกระทบที่เห็นได้ชัดเจนกว่ามาก ในกรณีที่นำเสนอเราอยู่ในแถบข้อผิดพลาด: ผลกระทบของการทำนายสาขา (ล้มเหลว) ไบต์โค้ดที่น้อยกว่าที่เหมาะสมและเอฟเฟกต์เฉพาะแพลตฟอร์ม / CPU อื่น ๆ ครอบงำ นั่นไม่ได้ทำให้กฎทั่วไปที่ว่าการประเมินครั้งเดียวเป็นโมฆะนั้นดีกว่า (และอ่านได้ง่ายขึ้น) แต่แสดงให้เห็นว่าสิ่งนี้อาจไม่ได้เพิ่มมูลค่ามากเท่าที่สุดขั้ว
MartyMacGyver

คำตอบ:


111

ความแตกต่างคือในx < y < z yจะได้รับการประเมินเพียงครั้งเดียว สิ่งนี้ไม่ได้สร้างความแตกต่างอย่างมากหาก y เป็นตัวแปร แต่จะเกิดขึ้นเมื่อเป็นการเรียกใช้ฟังก์ชันซึ่งต้องใช้เวลาในการคำนวณ

from time import sleep
def y():
    sleep(.2)
    return 1.3
%timeit 1.2 < y() < 1.8
10 loops, best of 3: 203 ms per loop
%timeit 1.2 < y() and y() < 1.8
1 loops, best of 3: 405 ms per loop

18
แน่นอนว่าอาจมีความแตกต่างทางความหมาย ไม่เพียง แต่ y () สามารถส่งคืนค่าที่แตกต่างกันสองค่า แต่ด้วยตัวแปรการประเมินตัวดำเนินการน้อยกว่าใน x <y สามารถเปลี่ยนค่าของ y ได้ ด้วยเหตุนี้จึงมักไม่ได้รับการปรับให้เหมาะสมที่สุดในรหัสไบต์ (ถ้า y เป็นตัวแปรที่ไม่ใช่โลคัลหรือตัวแปรที่มีส่วนร่วมในการปิด)
Random832

แค่อยากรู้ว่าทำไมคุณถึงต้องการsleep()ฟังก์ชั่นภายใน?

@Prof นั่นคือการจำลองฟังก์ชันซึ่งใช้เวลาพอสมควรในการคำนวณผลลัพธ์ หากฟังก์ชันกลับมาทันทีจะไม่มีความแตกต่างกันมากระหว่างผลลัพธ์ timeit ทั้งสอง
Rob

@Rob ทำไมจะไม่มีความแตกต่างมากนัก? มันจะเป็น 3ms เทียบกับ 205ms ซึ่งแสดงให้เห็นว่ามันดีพอหรือไม่?
ศ.

@Prof คุณไม่มีจุดที่คำนวณ y () สองครั้งนั่นคือ 2x200ms แทนที่จะเป็น 1x200ms ส่วนที่เหลือ (3/5 ms) เป็นเสียงรบกวนที่ไม่เกี่ยวข้องในการวัดเวลา
ร็อบ

22

bytecode ที่เหมาะสมที่สุดสำหรับทั้งสองฟังก์ชันที่คุณกำหนดไว้

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

เนื่องจากไม่ได้ใช้ผลของการเปรียบเทียบ มาทำให้สถานการณ์น่าสนใจยิ่งขึ้นโดยส่งคืนผลลัพธ์ของการเปรียบเทียบ นอกจากนี้ยังมีผลลัพธ์ที่ไม่สามารถทราบได้ในเวลารวบรวม

def interesting_compare(y):
    x = 1.1
    z = 1.3
    return x < y < z  # or: x < y and y < z

อีกครั้งการเปรียบเทียบทั้งสองเวอร์ชันมีความหมายเหมือนกันดังนั้นรหัสไบต์ที่เหมาะสมที่สุดจึงเหมือนกันสำหรับโครงสร้างทั้งสอง อย่างที่ดีที่สุดที่ฉันสามารถทำได้ก็จะมีลักษณะเช่นนี้ ฉันได้ใส่คำอธิบายประกอบแต่ละบรรทัดพร้อมเนื้อหาสแต็กก่อนและหลังแต่ละ opcode ในรูปแบบ Forth (ด้านบนของสแต็กทางขวา--หารก่อนและหลังการต่อท้าย?บ่งบอกถึงบางสิ่งที่อาจมีหรือไม่มีอยู่) โปรดทราบว่าRETURN_VALUEทิ้งทุกสิ่งที่เกิดขึ้นจะถูกทิ้งไว้บนสแต็กข้างใต้ค่าที่ส่งคืน

          0 LOAD_FAST                0 (y)    ;          -- y
          3 DUP_TOP                           ; y        -- y y
          4 LOAD_CONST               0 (1.1)  ; y y      -- y y 1.1
          7 COMPARE_OP               4 (>)    ; y y 1.1  -- y pred
         10 JUMP_IF_FALSE_OR_POP     19       ; y pred   -- y
         13 LOAD_CONST               1 (1.3)  ; y        -- y 1.3
         16 COMPARE_OP               0 (<)    ; y 1.3    -- pred
     >>  19 RETURN_VALUE                      ; y? pred  --

หากการใช้งานภาษา CPython, PyPy ไม่ว่าอะไรก็ตามจะไม่สร้าง bytecode นี้ (หรือลำดับการดำเนินการที่เทียบเท่ากันของตัวเอง) สำหรับทั้งสองรูปแบบซึ่งแสดงให้เห็นถึงคุณภาพที่ไม่ดีของคอมไพเลอร์ bytecodeนั้น การเดินทางจากลำดับ bytecode ที่คุณโพสต์ดังกล่าวข้างต้นเป็นปัญหาที่แก้ไขได้ (ผมคิดว่าสิ่งที่คุณต้องการสำหรับกรณีนี้คือการพับอย่างต่อเนื่อง , การกำจัดรหัสตายและการสร้างแบบจำลองที่ดีขึ้นของเนื้อหาของสแต็ค; กำจัด subexpression ทั่วไปนอกจากนี้ยังจะมีราคาถูกและมีคุณค่า ) และไม่มีข้อแก้ตัวจริงๆที่จะไม่ใช้ภาษาสมัยใหม่

ตอนนี้มันเกิดขึ้นที่การใช้งานภาษาทั้งหมดในปัจจุบันมีคอมไพเลอร์ bytecode ที่มีคุณภาพต่ำ แต่คุณควรเพิกเฉยในขณะที่เขียนโค้ด! การหลอกว่าคอมไพเลอร์ bytecode นั้นดีและเขียนโค้ดที่อ่านได้มากที่สุด มันอาจจะเร็วพออยู่แล้ว หากไม่เป็นเช่นนั้นให้มองหาการปรับปรุงอัลกอริทึมก่อนและลองใช้Cythonเป็นครั้งที่สองซึ่งจะช่วยปรับปรุงความพยายามเดียวกันได้ดีกว่าการปรับแต่งระดับนิพจน์ใด ๆ ที่คุณอาจนำไปใช้


สิ่งที่สำคัญที่สุดของการเพิ่มประสิทธิภาพทั้งหมดจะต้องเป็นไปได้ตั้งแต่แรก: และนั่นยังห่างไกลจาก "ปัญหาที่แก้ไขแล้ว" สำหรับภาษาไดนามิกที่อนุญาตให้เปลี่ยนการใช้งานฟังก์ชันแบบไดนามิก (ทำได้แม้ว่า - HotSpot สามารถทำสิ่งที่คล้ายกันและสิ่งต่างๆเช่น Graal กำลังดำเนินการเพื่อให้การเพิ่มประสิทธิภาพประเภทนี้พร้อมใช้งานสำหรับ Python และภาษาไดนามิกอื่น ๆ ) และเนื่องจากฟังก์ชั่นอาจถูกเรียกใช้จากโมดูลที่แตกต่างกัน (หรือการโทรอาจสร้างขึ้นแบบไดนามิก!) คุณจึงไม่สามารถทำการเพิ่มประสิทธิภาพเหล่านี้ได้ที่นั่น
Voo

1
@Voo รหัส bytecode ที่ปรับแต่งด้วยมือของฉันมีความหมายเหมือนกับต้นฉบับทุกประการแม้ว่าจะมีพลวัตตามอำเภอใจก็ตาม (ข้อยกเว้นประการหนึ่ง: สมมติว่า <b ≡ b> a) นอกจากนี้การซับในยังถูก overrated หากคุณทำมันมากเกินไป - และมันง่ายเกินไปที่จะทำมันมากเกินไปคุณจะระเบิด I-cache และสูญเสียทุกสิ่งที่คุณได้รับจากนั้นไปบางส่วน
zwol

คุณพูดถูกฉันคิดว่าคุณหมายถึงคุณต้องการปรับให้เหมาะสมinteresting_compareกับ bytecode ง่ายๆที่ด้านบน (ซึ่งจะใช้ได้เฉพาะกับ inlining) offtopic โดยสิ้นเชิง แต่: Inlining เป็นหนึ่งในการเพิ่มประสิทธิภาพที่สำคัญที่สุดสำหรับคอมไพเลอร์ใด ๆ คุณสามารถลองเรียกใช้เกณฑ์มาตรฐานบางอย่างด้วยการพูดว่า HotSpot ในโปรแกรมจริง (ไม่ใช่การทดสอบทางคณิตศาสตร์บางอย่างที่ใช้เวลา 99% ในการวนรอบเดียวที่ปรับให้เหมาะกับมือ [และด้วยเหตุนี้จึงไม่มีการเรียกใช้ฟังก์ชันอีกต่อไป]) เมื่อคุณปิดใช้งาน ความสามารถในการอินไลน์ทุกอย่าง - คุณจะเห็นการถดถอยขนาดใหญ่
Voo

@Voo ใช่ที่ bytecode ง่ายที่ด้านบนถูกควรจะเป็น "รุ่นที่ดีที่สุดของ" รหัสเดิมของ OP interesting_compareไม่
zwol

"หนึ่งข้อยกเว้น: a <b ≡ b> a ถูกสันนิษฐาน" →ซึ่งไม่เป็นความจริงใน Python นอกจากนี้ฉันคิดว่า CPython ไม่สามารถสรุปการดำเนินการได้อย่างแท้จริงว่าyอย่าเปลี่ยนสแต็กเนื่องจากมีเครื่องมือดีบักจำนวนมาก
Veedrac

8

เนื่องจากความแตกต่างในผลลัพธ์ดูเหมือนจะเกิดจากการขาดการปรับให้เหมาะสมฉันคิดว่าคุณควรเพิกเฉยต่อความแตกต่างนั้นในกรณีส่วนใหญ่ - อาจเป็นไปได้ว่าความแตกต่างจะหายไป ความแตกต่างเป็นเพราะyควรได้รับการประเมินเพียงครั้งเดียวและแก้ไขได้โดยการทำซ้ำบนสแต็กซึ่งต้องใช้เพิ่มเติมPOP_TOP- วิธีแก้ปัญหาที่จะใช้LOAD_FASTอาจเป็นไปได้

ข้อแตกต่างที่สำคัญคือในx<y and y<zวินาทีyควรได้รับการประเมินสองครั้งหากx<yประเมินเป็นจริงสิ่งนี้มีผลกระทบหากการประเมินของyใช้เวลามากหรือมีผลข้างเคียง

ในสถานการณ์ส่วนใหญ่คุณควรใช้x<y<zแม้ว่าจะค่อนข้างช้ากว่าก็ตาม


6

ก่อนอื่นการเปรียบเทียบของคุณไม่มีความหมายมากนักเนื่องจากโครงสร้างทั้งสองไม่ได้ถูกนำมาใช้เพื่อปรับปรุงประสิทธิภาพดังนั้นคุณจึงไม่ควรตัดสินใจว่าจะใช้โครงสร้างใดแบบหนึ่งแทนอีกแบบโดยพิจารณาจากสิ่งนั้น

x < y < zสร้าง:

  1. มีความชัดเจนและตรงกว่าในความหมาย
  2. ความหมายของมันคือสิ่งที่คุณคาดหวังจาก "ความหมายทางคณิตศาสตร์" ของการเปรียบเทียบ: evalute x, yและz ครั้งเดียวและตรวจสอบว่าสภาพทั้งถือ การใช้andการเปลี่ยนแปลงความหมายโดยการประเมินyหลาย ๆ ครั้งซึ่งสามารถเปลี่ยนแปลงผลลัพธ์ได้

ดังนั้นเลือกหนึ่งแทนอีกอันขึ้นอยู่กับความหมายที่คุณต้องการและถ้ามีค่าเท่ากันจะอ่านได้มากกว่าอีกอันหนึ่งหรือไม่

สิ่งนี้กล่าวว่า: โค้ดที่แยกชิ้นส่วนมากขึ้นไม่ได้หมายความถึงรหัสที่ช้าลง อย่างไรก็ตามการเรียกใช้การดำเนินการ bytecode มากขึ้นหมายความว่าแต่ละการดำเนินการนั้นง่ายกว่าและยังต้องมีการวนซ้ำของลูปหลัก ซึ่งหมายความว่าหากการดำเนินการที่คุณกำลังดำเนินการนั้นเร็วมาก (เช่นการค้นหาตัวแปรภายในเครื่องขณะที่คุณกำลังดำเนินการอยู่) ค่าใช้จ่ายในการดำเนินการตามรหัสที่มากขึ้นอาจมีความสำคัญ

แต่โปรดทราบว่าผลลัพธ์นี้ไม่ได้เกิดขึ้นในสถานการณ์ทั่วไปมากกว่าเฉพาะใน "กรณีที่เลวร้ายที่สุด" ที่คุณเกิดขึ้นกับโปรไฟล์ อย่างที่คนอื่น ๆ ตั้งข้อสังเกตถ้าคุณเปลี่ยนไปyเป็นสิ่งที่ต้องใช้เวลามากขึ้นอีกเล็กน้อยคุณจะเห็นว่าผลลัพธ์เปลี่ยนไปเพราะสัญกรณ์ที่ถูกล่ามไว้จะประเมินเพียงครั้งเดียว

สรุป:

  • พิจารณาความหมายก่อนการแสดง
  • คำนึงถึงความสามารถในการอ่าน
  • อย่าไว้วางใจมาตรฐานขนาดเล็ก สร้างโปรไฟล์ด้วยพารามิเตอร์ประเภทต่างๆเสมอเพื่อดูว่าเวลาของฟังก์ชัน / นิพจน์ทำงานอย่างไรที่สัมพันธ์กับพารามิเตอร์ดังกล่าวและพิจารณาว่าคุณวางแผนจะใช้มันอย่างไร

5
ฉันคิดว่าคำตอบของคุณไม่ได้รวมถึงข้อเท็จจริงที่ตรงไปตรงมาและตรงประเด็นซึ่งในกรณีเฉพาะของคำถาม - การเปรียบเทียบลอยนั้นผิด การเปรียบเทียบแบบล่ามโซ่ไม่ได้เร็วขึ้นอย่างที่เห็นในการวัดทั้งสองแบบและในรหัส bytecode ที่สร้างขึ้น
pvg

30
การตอบคำถามที่ติดแท็กประสิทธิภาพด้วย "บางทีคุณไม่ควรคิดมากเรื่องประสิทธิภาพ" ดูเหมือนจะไม่มีประโยชน์สำหรับฉัน คุณกำลังตั้งสมมติฐานเกี่ยวกับความเข้าใจของผู้ถามเกี่ยวกับหลักการเขียนโปรแกรมทั่วไปจากนั้นส่วนใหญ่จะพูดถึงพวกเขาแทนที่จะเป็นประเด็นที่อยู่ในมือ
Ben Millwood

@Veerdac คุณกำลังอ่านความคิดเห็นผิด การเพิ่มประสิทธิภาพที่เสนอในเอกสารต้นฉบับที่ OP ใช้นั้นไม่ถูกต้องในกรณีของการลอยอย่างน้อยที่สุด มันไม่เร็วขึ้น
pvg
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.