Có phải x x y <z <nhanh hơn so với ăn x <y và y <z không?


129

Từ trang này , chúng tôi biết rằng:

So sánh xích được nhanh hơn so với sử dụng andtoán tử. Viết x < y < zthay cho x < y and y < z.

Tuy nhiên, tôi đã nhận được một kết quả khác khi kiểm tra các đoạn mã sau:

$ 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

Có vẻ như x < y and y < zlà nhanh hơn x < y < z. Tại sao?

Sau khi tìm kiếm một số bài đăng trong trang web này (như bài này ) tôi biết rằng "chỉ được đánh giá một lần" là chìa khóa x < y < z, tuy nhiên tôi vẫn bối rối. Để nghiên cứu thêm, tôi đã phân tách hai chức năng này bằng cách sử dụng 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)

Và đầu ra là:

## 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)

Có vẻ như các x < y and y < zlệnh có ít sự khác biệt hơn x < y < z. Tôi có nên xem xét x < y and y < znhanh hơn x < y < z?

Đã thử nghiệm với Python 2.7.6 trên CPU Intel (R) Xeon (R) E5640 @ 2.67GHz.


8
Các lệnh được phân tách nhiều hơn không có nghĩa là phức tạp hơn hoặc mã chậm hơn. Tuy nhiên nhìn thấy timeitbài kiểm tra của bạn, tôi đã quan tâm đến điều này.
Marco Bonelli

6
Tôi giả sử sự khác biệt về tốc độ so với "được đánh giá một lần" xuất hiện khi ykhông chỉ là một tra cứu biến đổi, mà là một quá trình tốn kém hơn như một cuộc gọi hàm? Tức 10 < max(range(100)) < 15là nhanh hơn 10 < max(range(100)) and max(range(100)) < 15max(range(100))được gọi một lần cho cả hai so sánh.
zehnpaard

2
@MarcoBonelli Nó thực hiện khi mã được phân tách 1) không chứa các vòng lặp và 2) mỗi mã byte đơn rất nhanh, vì tại thời điểm đó, phần trên của mainloop trở nên quan trọng.
Bakuriu

2
Dự đoán chi nhánh có thể làm rối các bài kiểm tra của bạn.
Corey Ogburn

2
@zehnpaard Tôi đồng ý với bạn. Khi "y" lớn hơn một giá trị đơn giản (ví dụ: gọi hàm hoặc tính toán) tôi mong muốn thực tế là "y" được đánh giá một lần trong x <y <z có tác động đáng chú ý hơn nhiều. Trong trường hợp được trình bày, chúng tôi nằm trong các thanh lỗi: ảnh hưởng của dự đoán nhánh (không thành công), mã byte kém tối ưu và các hiệu ứng nền tảng / CPU cụ thể khác chiếm ưu thế. Điều đó không làm mất hiệu lực quy tắc chung rằng đánh giá một lần là tốt hơn (cũng như dễ đọc hơn), nhưng cho thấy rằng điều này có thể không thêm nhiều giá trị ở các thái cực.
MartyMacGyver

Câu trả lời:


111

Sự khác biệt là trong x < y < z ychỉ được đánh giá một lần. Điều này không tạo ra sự khác biệt lớn nếu y là một biến, nhưng nó thực hiện khi nó là một lệnh gọi hàm, cần một chút thời gian để tính toán.

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
Tất nhiên, cũng có thể có một sự khác biệt về ngữ nghĩa. Không chỉ y () có thể trả về hai giá trị khác nhau, mà với một biến, việc đánh giá toán tử nhỏ hơn x <y có thể thay đổi giá trị của y. Đây là lý do tại sao nó thường không được tối ưu hóa trong mã byte (ví dụ: nếu y là biến không cục bộ hoặc một biến tham gia vào một bao đóng chẳng hạn)
Random832

Chỉ tò mò, tại sao bạn cần một sleep()chức năng bên trong?
Giáo sư

@Prof Đó là mô phỏng một hàm cần một chút thời gian để tính kết quả. Nếu các hàm trả về ngay lập tức, sẽ không có nhiều khác biệt giữa hai kết quả thời gian.
Cướp

@Rob Tại sao sẽ không có nhiều sự khác biệt? Nó sẽ là 3ms so với 205ms, điều đó chứng tỏ nó đủ tốt phải không?
Giáo sư

@Prof Bạn đang thiếu điểm y () được tính hai lần, do đó là 2x200ms thay vì 1x200ms. Phần còn lại (3/5 ms) là tiếng ồn không liên quan trong phép đo thời gian.
Cướp

22

byte tối ưu cho cả hai hàm bạn đã xác định sẽ là

          0 LOAD_CONST               0 (None)
          3 RETURN_VALUE

bởi vì kết quả so sánh không được sử dụng Hãy làm cho tình huống thú vị hơn bằng cách trả về kết quả so sánh. Chúng ta cũng có kết quả không thể biết được tại thời điểm biên dịch.

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

Một lần nữa, hai phiên bản so sánh giống hệt nhau về mặt ngữ nghĩa, do đó, mã byte tối ưu giống nhau cho cả hai cấu trúc. Tốt nhất tôi có thể làm việc đó, nó sẽ trông như thế này. Tôi đã chú thích từng dòng với nội dung ngăn xếp trước và sau mỗi opcode, trong ký hiệu Forth (đỉnh ngăn xếp ở bên phải, --chia trước và sau, dấu vết ?chỉ ra thứ gì đó có thể có hoặc không có ở đó). Lưu ý rằng RETURN_VALUEloại bỏ mọi thứ xảy ra còn lại trên ngăn xếp bên dưới giá trị được trả về.

          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  --

Nếu việc triển khai ngôn ngữ, CPython, PyPy, bất cứ điều gì, không tạo ra mã byte này (hoặc chuỗi hoạt động tương đương của chính nó) cho cả hai biến thể, điều đó chứng tỏ chất lượng kém của trình biên dịch mã byte đó . Bắt đầu từ các chuỗi mã byte mà bạn đã đăng ở trên là một vấn đề đã được giải quyết (Tôi nghĩ rằng tất cả những gì bạn cần cho trường hợp này là liên tục gấp , loại bỏ mã chết và mô hình hóa tốt hơn các nội dung của ngăn xếp; loại bỏ phổ biến phụ cũng sẽ rẻ và có giá trị ), và thực sự không có lý do gì để không thực hiện nó trong một triển khai ngôn ngữ hiện đại.

Bây giờ, tất cả các triển khai ngôn ngữ hiện tại đều có trình biên dịch mã byte chất lượng kém. Nhưng bạn nên bỏ qua điều đó trong khi mã hóa! Giả sử trình biên dịch mã byte là tốt và viết mã dễ đọc nhất . Nó có thể sẽ đủ nhanh dù sao đi nữa. Nếu không, trước tiên hãy tìm các cải tiến thuật toán và thử Cython lần thứ hai - điều đó sẽ mang lại nhiều cải tiến hơn cho cùng một nỗ lực so với bất kỳ điều chỉnh mức biểu thức nào bạn có thể áp dụng.


Vâng, điều quan trọng nhất trong tất cả các tối ưu hóa sẽ phải có thể ở nơi đầu tiên: nội tuyến. Và đó là một "vấn đề được giải quyết" đối với các ngôn ngữ động cho phép thay đổi việc triển khai chức năng một cách linh hoạt (mặc dù có thể thực hiện được - HotSpot có thể thực hiện những điều tương tự và những thứ như Graal đang làm việc để cung cấp các loại tối ưu hóa này cho Python và các ngôn ngữ động khác ). Và vì chức năng có thể được gọi từ các mô-đun khác nhau (hoặc một cuộc gọi có thể được tạo động!), Bạn thực sự không thể thực hiện những tối ưu hóa này ở đó.
Voo

1
@Voo Mã byte được tối ưu hóa bằng tay của tôi có chính xác ngữ nghĩa giống như bản gốc ngay cả khi có sự năng động tùy ý (một ngoại lệ: a <b ≡ b> a được giả sử). Ngoài ra, nội tuyến được đánh giá cao. Nếu bạn làm quá nhiều - và quá dễ để làm quá nhiều - bạn thổi I-cache và mất mọi thứ bạn có được và sau đó là một số.
zwol

Bạn nói đúng, tôi nghĩ bạn có nghĩa là bạn muốn tối ưu hóa interesting_comparemã byte đơn giản ở trên cùng (chỉ hoạt động với nội tuyến). Hoàn toàn không chính thức nhưng: Inlining là một trong những tối ưu hóa cần thiết nhất cho bất kỳ trình biên dịch nào. Bạn có thể thử chạy một số điểm chuẩn với HotSpot trên các chương trình thực (không phải một số bài kiểm tra toán dành 99% thời gian của chúng trong một vòng lặp nóng được tối ưu hóa [và do đó không có bất kỳ chức năng nào gọi nữa]) khi bạn vô hiệu hóa khả năng nội tuyến bất cứ điều gì - bạn sẽ thấy hồi quy lớn.
Voo

@Voo Vâng, mã byte đơn giản ở trên cùng được cho là "phiên bản tối ưu" của mã gốc của OP, chứ không phải interesting_compare.
zwol

"một ngoại lệ: a <b ≡ b> a được giả sử" → đơn giản là không đúng trong Python. Thêm vào đó, tôi nghĩ CPython thậm chí không thể thực sự giả định các hoạt động trên ykhông thay đổi ngăn xếp, vì nó có rất nhiều công cụ gỡ lỗi.
Veedrac

8

Vì sự khác biệt trong đầu ra dường như là do thiếu tối ưu hóa, tôi nghĩ bạn nên bỏ qua sự khác biệt đó trong hầu hết các trường hợp - có thể là sự khác biệt sẽ biến mất. Sự khác biệt là bởi vì ychỉ nên được đánh giá một lần và điều đó được giải quyết bằng cách sao chép nó trên ngăn xếp cần thêm POP_TOP- giải pháp sử dụng LOAD_FASTcó thể là có thể.

Sự khác biệt quan trọng là trong x<y and y<zlần thứ hai ynên được đánh giá hai lần nếu x<yđánh giá là đúng, điều này có ý nghĩa nếu việc đánh giá ymất nhiều thời gian hoặc có tác dụng phụ.

Trong hầu hết các kịch bản bạn nên sử dụng x<y<zmặc dù thực tế nó hơi chậm hơn.


6

Trước hết, so sánh của bạn khá vô nghĩa vì hai cấu trúc khác nhau không được giới thiệu để cải thiện hiệu suất, vì vậy bạn không nên quyết định có nên sử dụng cái này thay cho cái kia hay không.

Cấu x < y < ztrúc:

  1. Rõ ràng hơn và trực tiếp hơn trong ý nghĩa của nó.
  2. Ngữ nghĩa của nó là những gì bạn mong đợi từ "ý nghĩa toán học" của so sánh: đánh giá x, yz một lần và kiểm tra xem toàn bộ điều kiện có đúng không. Sử dụng andthay đổi ngữ nghĩa bằng cách đánh giá ynhiều lần, có thể thay đổi kết quả .

Vì vậy, chọn một cái thay cho cái khác tùy thuộc vào ngữ nghĩa mà bạn muốn và, nếu chúng tương đương nhau, liệu cái này có dễ đọc hơn cái kia không.

Điều này nói rằng: mã phân tách nhiều hơn không ngụ ý mã chậm hơn. Tuy nhiên, thực hiện nhiều hoạt động mã byte hơn có nghĩa là mỗi hoạt động đơn giản hơn và nó yêu cầu lặp lại vòng lặp chính. Điều này có nghĩa là nếu các hoạt động bạn đang thực hiện cực kỳ nhanh chóng (ví dụ: tra cứu biến cục bộ như bạn đang thực hiện ở đó), thì chi phí thực hiện nhiều hoạt động mã byte có thể quan trọng hơn.

Nhưng lưu ý rằng kết quả này không giữ được tình huống chung chung hơn, chỉ xảy ra với "trường hợp xấu nhất" mà bạn xảy ra với hồ sơ. Như những người khác đã lưu ý, nếu bạn thay đổi ythành thứ gì đó thậm chí mất thêm một chút thời gian, bạn sẽ thấy kết quả thay đổi, bởi vì ký hiệu chuỗi chỉ đánh giá nó một lần.

Tóm tắt:

  • Hãy xem xét ngữ nghĩa trước khi thực hiện.
  • Hãy tính đến khả năng đọc.
  • Đừng tin tưởng điểm chuẩn vi mô. Luôn cấu hình với các loại tham số khác nhau để xem thời gian của hàm / biểu thức hoạt động liên quan đến các tham số đã nói và xem xét cách bạn dự định sử dụng nó.

5
Tôi nghĩ rằng câu trả lời của bạn không bao gồm sự thật đơn giản và có liên quan rằng trang được trích dẫn, trong trường hợp cụ thể của câu hỏi - so sánh số float, chỉ đơn giản là sai. Việc so sánh chuỗi không nhanh hơn như đã thấy trong cả hai phép đo và mã byte được tạo.
pvg

30
Trả lời một câu hỏi được gắn thẻ hiệu suất với "có lẽ bạn không nên nghĩ về hiệu suất quá nhiều" dường như không hữu ích với tôi. Bạn đang đưa ra các giả định có khả năng bảo trợ về việc nắm bắt các nguyên tắc lập trình chung của người hỏi và sau đó chủ yếu nói về chúng thay vì vấn đề hiện tại.
Ben Millwood

@Veerdac bạn đang đọc sai nhận xét. Tối ưu hóa đề xuất trong tài liệu gốc mà OP đã dựa vào là sai, trong trường hợp nổi ở mức tối thiểu. Nó không nhanh hơn.
pvg
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.