"x <y <z"가 "x <y and 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합니까?

2.67GHz @ Intel Xeon CPU E5640에서 Python 2.7.6으로 테스트되었습니다.


8
더 불쾌한 명령이 더 복잡 하거나 느린 코드를 의미하지는 않습니다 . 그러나 귀하의 timeit테스트를 보면 이것에 관심이 있습니다.
Marco Bonelli

6
"한 번 평가"와 속도 차이 y가 단순히 변수 조회가 아니라 함수 호출과 같은 더 비싼 프로세스 일 때 발생한다고 가정했습니다 . 즉 , 두 비교 모두에 대해 한 번 호출 되기 때문에 10 < max(range(100)) < 15보다 빠릅니다 . 10 < max(range(100)) and max(range(100)) < 15max(range(100))
zehnpaard

2
@MarcoBonelli 그것은 않습니다 디스 어셈블 된 코드 1) 그 시점에서 주회 돌이의 오버 헤드가 크게되기 때문에 모든 단일 바이트 코드는 매우 매우 빠르고) 루프를 포함하며이없는 경우.
Bakuriu

2
분기 예측은 테스트를 망칠 수 있습니다.
Corey Ogburn

2
@zehnpaard 동의합니다. "y"가 단순한 값 (예 : 함수 호출 또는 계산) 이상인 경우 "y"가 x <y <z에서 한 번 평가되어 훨씬 눈에 띄는 영향을 줄 것으로 예상됩니다. 제시된 경우 우리는 오류 막대 내에 있습니다. (실패한) 분기 예측의 효과, 최적보다 적은 바이트 코드 및 기타 플랫폼 / CPU 별 효과가 지배적입니다. 그것은 한 번 평가하는 것이 더 좋고 (가독성이 뛰어남) 일반적인 규칙을 무효화하지는 않지만 이것이 극단적으로 많은 가치를 부가하지 않을 수 있음을 보여줍니다.
MartyMacGyver

답변:


111

차이점은 in 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

@Prof 결과를 계산하는 데 시간이 걸리는 함수를 시뮬레이션하는 것입니다. 함수가 즉시 반환되면 두 timeit 결과 사이에는 큰 차이가 없습니다.
Rob

@Rob 왜 큰 차이가 없을까요? 그것은 3ms 대 205ms 일 것입니다.
Prof

@Prof y ()가 두 번 계산되어 1x200ms 대신 2x200ms라는 점이 없습니다. 나머지 (3 / 5ms)는 타이밍 측정에서 관련없는 노이즈입니다.
Rob

22

정의한 두 함수 모두에 대한 최적의 바이트 코드는

          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

다시, 두 버전의 비교는 의미 상 동일하므로 최적의 바이트 코드는 두 구문에서 동일합니다. 내가 해결할 수있는 최선의 방법은 다음과 같습니다. Forth 표기법에서 각 opcode 전후에 스택 내용으로 각 줄에 주석을 달았습니다 (오른쪽 스택의 맨 위, --전후의 구분, 후행 ?은있을 수도 있고 없을 수도 있음). 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 언어의 구현이 두 변형에 대해이 바이트 코드 (또는 그와 동등한 연산 시퀀스)를 생성하지 않으면 해당 바이트 코드 컴파일러의 품질이 떨어지는 것을 보여줍니다 . 위에서 게시 한 바이트 코드 시퀀스에서 얻는 것은 해결 된 문제입니다 (이 경우에 필요한 것은 모두 접기 , 죽은 코드 제거 및 스택 내용의 더 나은 모델링입니다. 공통 하위 표현 제거) 도 저렴하고 가치가 있습니다. ), 현대 언어 구현에서 그렇게하지 않는 것에 대한 변명이 없습니다.

현재 모든 언어 구현에는 품질이 낮은 바이트 코드 컴파일러가 있습니다. 그러나 코딩하는 동안 이를 무시 해야합니다 ! 바이트 코드 컴파일러가 좋은 척하고 가장 읽기 쉬운 코드를 작성하십시오 . 어쨌든 충분히 빠를 것입니다. 그렇지 않은 경우 알고리즘 개선을 먼저 확인하고 Cython 에 다시 시도해보십시오. 이는 적용 할 수있는 표현 수준 조정보다 동일한 노력으로 훨씬 더 향상된 기능을 제공합니다.


우선 모든 최적화 중 가장 중요한 것은 인라인에서 가능해야 할 것입니다. 그리고 그것은 동적으로 함수의 구현을 동적으로 변경할 수있게하는 동적 언어에 대한 "해결 된 문제"와는 거리가 멀다 (하지만 HotSpot은 비슷한 일을 할 수 있고 Graal과 같은 것들이 파이썬과 다른 동적 언어에 이러한 종류의 최적화를 제공하기 위해 노력하고있다 ). 함수 자체가 다른 모듈에서 호출되거나 호출이 동적으로 생성 될 수 있으므로 이러한 최적화를 실제로 수행 할 수 없습니다.
Voo

1
@Voo 내 손에 최적화 된 바이트 코드는 임의의 역 동성이 존재하더라도 원본과 정확히 동일한 의미를 갖습니다 (한 예외 : a <b b b> a로 가정). 또한 인라인이 과대 평가되었습니다. 너무 많은 일을하고 너무 많은 일을하는 것이 너무 쉬운 경우, I- 캐시를 날려 버리고 얻은 모든 것을 잃어 버립니다.
zwol

당신은 맞습니다. 당신이 interesting_compare상단의 간단한 바이트 코드 로 최적화하고 싶다는 것을 의미한다고 생각했습니다 (인라인으로 만 작동합니다). 완전히 토픽이지만 인라인은 모든 컴파일러에 가장 필수적인 최적화 중 하나입니다. 실제 프로그램에서 HotSpot을 사용하여 일부 벤치 마크를 실행할 수 있습니다 (수동으로 최적화 된 핫 루프에서 99 %의 시간을 소비하는 수학 테스트가 아님) (따라서 더 이상 함수 호출이 없습니다) 인라인 기능-큰 회귀를 볼 수 있습니다.
Voo

@Voo 예, 맨 위에있는 간단한 바이트 코드는 OP의 원본 코드가 아닌 "최적의 버전"이어야합니다 interesting_compare.
zwol

"하나의 예외 : a <b ≡ b> a가 가정됩니다"→ 이것은 파이썬에서 사실이 아닙니다. 또한 CPython은 y많은 디버그 도구가 있기 때문에 실제로 작업 이 스택을 변경하지 않는다고 가정 할 수 없다고 생각 합니다.
Veedrac

8

출력의 차이는 최적화 부족으로 인한 것으로 보이므로 대부분의 경우 해당 차이를 무시해야한다고 생각합니다. 차이가 사라질 수 있습니다. 차이점은 y한 번만 평가해야 하기 때문에 스택에 복제하여 추가로 POP_TOP해결해야하므로 해결해야합니다.LOAD_FAST 이 가능할 수 있습니다.

그러나 중요한 차이점 x<y and y<z은 두 번째 평가에서 true y로 평가되면 두 번 평가되어야한다는 것 x<y입니다.y 상당한 시간 걸리거나 부작용이있는 있습니다.

대부분의 시나리오에서는 x<y<z다소 느리다는 사실에도 불구하고 사용해야합니다 .


6

우선 , 성능 향상을 위해 두 개의 서로 다른 구성이 도입 되지 않았기 때문에 비교는 의미가 없습니다 .

x < y < z구조 :

  1. 그 의미에서 더 명확하고 직접적입니다.
  2. evalute을 : 그것의 의미는 당신이 비교의 "수학적 의미"에서 기대할 수있는 것입니다 x, y그리고 z 한 번 전체 상태가 보유하고 있는지 확인하세요. 를 사용하면 여러 번 and평가하여 의미가 변경되어 결과y변경 될 수 있습니다 .

그래서, 당신이 원하는 의미에 따라 다른 장소에서 하나를 선택 하는 경우 가 동일 하나가 다른 것보다 더 읽을 수 있는지 여부.

더 많은 분해 된 코드가 느린 코드를 의미하지는 않습니다 . 그러나 더 많은 바이트 코드 작업을 실행하면 각 작업이 더 단순하지만 기본 루프를 반복해야합니다. 이러한 것이 의미 하는 경우 가 수행하는 동작은 (예를 들어 로컬 변수 조회가 존재하고있는 바와 같이) 매우 신속하고, 그 오버 더 바이트 동작을 실행 중요 할 수있다.

그러나이 결과는 않습니다 하지 만 프로파일 링하는 일이있는 "최악의 경우"에,보다 일반적인 상황에서 개최. 다른 사람들이 언급했듯이, 변경하면y 조금 더 시간이 걸리는 것으로 변경하면 체인 표기법이 한 번만 평가하기 때문에 결과가 변경되는 것을 볼 수 있습니다.

요약 :

  • 성능 전에 의미를 고려하십시오.
  • 가독성을 고려하십시오.
  • 마이크로 벤치 마크를 신뢰하지 마십시오. 함수 / 표현 타이밍이 상기 파라미터와 관련하여 동작하는 방법을보고 사용 방법을 고려하려면 항상 다른 종류의 파라미터로 프로파일 링하십시오.

5
귀하의 답변에는 질문의 특정 경우-플로트 비교와 같이 인용 된 페이지가 단순히 잘못되었다는 간단하고 관련있는 사실이 포함되어 있지 않습니다. 체인 비교는 측정과 생성 된 바이트 코드 모두에서보다 빠르지 않습니다.
pvg

30
"성능에 대해 너무 많이 생각해서는 안된다"는 성과 태그가 붙은 질문에 대답하는 것은 나에게 도움이되지 않는 것 같습니다. 당신은 일반적인 프로그래밍 원칙에 대한 질문자의 이해에 대해 잠재적으로 애용하는 가정을하고 있으며, 현재 문제 대신에 그것들에 대해 이야기하고 있습니다.
벤 밀우드

@Veerdac 댓글을 잘못 읽고 있습니다. 최소 플로팅의 경우 OP가 의존 한 원본 문서에서 제안 된 최적화가 잘못되었습니다. 더 빠르지 않습니다.
pvg
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.