조기 반납이 다른 것보다 느린 이유는 무엇입니까?


179

이것은 며칠 전에 답한 답에 대한 후속 질문 입니다. 편집 : 해당 질문의 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트리거 되는 조건에 관계없이 절을 갖는 것이 더 빠릅니다 .

나는 그것이 두 바이트에 의해 생성 된 다른 바이트 코드와 관련이 있다고 가정하지만, 아무도 상세하게 확인 / 설명 할 수 있습니까?

편집 : 모든 사람이 내 타이밍을 재현 할 수있는 것은 아니므로 시스템에 대한 정보를 제공하는 것이 유용 할 것이라고 생각했습니다. 기본 파이썬이 설치된 상태에서 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
그래서 나는 지금 찾을 수없는 동일한 질문이있었습니다. 그들은 생성 된 바이트 코드를 확인했으며 한 가지 추가 단계가있었습니다. 관찰 된 차이는 테스터 (machine, SO ..)에 매우 의존적이며 때로는 매우 작은 차이 만 발견합니다.
joaquin

3
3.x에서 둘 다 끝에 도달 할 수없는 일부 코드에 대해 동일한 바이트 코드 저장을 생성 합니다 ( LOAD_CONST(None); RETURN_VALUE그러나 명시된 바와 같이 도달하지 못했습니다) with_else. 데드 코드가 기능을 더 빨리 만든다고 의심합니다. 누군가 dis2.7을 제공 할 수 있습니까?

4
나는 이것을 재현 할 수 없었다. 그들 else과 함께 기능 하고 False가장 느리다 (152ns). 두 번째로 빠른했다 True없이 else(143ns) 등이 기본적으로 (137ns와 138ns) 동일 하였다. 기본 매개 변수를 사용하지 않고 %timeitiPython에서 측정했습니다 .
rplnt

이러한 타이밍을 재현 할 수없고 때로는 with_else가 더 빠르며 때로는 without_else 버전 인 경우도 있습니다.
Cédric Julien

1
분해 결과가 추가되었습니다. @mac과 동일한 구성 인 Ubuntu 11.10, 64 비트, 기본 Python 2.7을 사용하고 있습니다. 또한 상당히 with_else빠릅니다.
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]

바이트 코드가 동일하다는 것을 고려하면 유일한 차이점은 함수의 이름입니다. 특히 타이밍 테스트는 글로벌 이름을 검색합니다. 이름을 바꾸면 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모듈로 32를 8로 줄인 505688136으로 해시되어 충돌이 발생합니다. 이 파이썬을 해결하려면 다음을 계산하십시오.

로 시작:

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해시 테이블의 크기도 i해시 값에서 사용 된 비트 수입니다 j.

테이블의 각 프로브는 다음 중 하나를 찾을 수 있습니다.

  • 슬롯이 비어 있습니다.이 경우 프로빙이 중지되고 값이 테이블에 없다는 것을 알 수 있습니다.

  • 슬롯은 사용되지 않았지만 과거에 사용되었으므로 위와 같이 계산 된 다음 값을 사용해보십시오.

  • 슬롯이 가득 찼지만 테이블에 저장된 전체 해시 값이 찾고있는 키의 해시와 동일하지 않습니다 (이 경우 __builtins__vs 의 경우 without_else).

  • 슬롯이 가득 차서 원하는 해시 값을 정확히 얻은 다음 Python은 우리가 찾고있는 키와 객체가 동일한 객체인지 확인합니다 (이 경우 식별자 일 수있는 짧은 문자열이 서로 연결되어 있기 때문에 동일한 식별자는 정확히 동일한 문자열을 사용합니다).

  • 마지막으로 슬롯이 가득 차면 해시는 정확히 일치하지만 키는 동일한 객체가 아니며 파이썬은 동등성을 비교하려고 시도합니다. 이것은 비교적 느리지 만 이름 조회의 경우 실제로 일어나지 않아야합니다.


9
@Chris, 문자열의 길이는 중요하지 않습니다. 문자열을 처음 해시 할 때 문자열 길이에 비례하여 시간이 걸리지 만 계산 된 해시는 문자열 객체에 캐시되므로 후속 해시는 O (1)입니다.
던컨

1
아, 알았어, 캐싱에 대해 잘
몰랐지만

9
매혹적인! 셜록이라고 불러도 될까요? ;) 어쨌든 나는 질문이 자격이 되 자마자 현상금과 함께 추가 포인트를주는 것을 잊지 않기를 바랍니다.
Voo

4
@mac, 그렇지 않습니다. 해시 해상도에 대해 조금 추가 할 것입니다 (댓글에 압착하려고했지만 생각보다 흥미 롭습니다).
던컨

6
@Duncan-해시 프로세스를 설명하는 데 시간을내어 주셔서 대단히 감사합니다. 최고의 노치 답변! :)
mac
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.