파이썬 : 왜 *와 **가 /와 sqrt ()보다 빠릅니까?


80

코드를 최적화하는 동안 다음을 깨달았습니다.

>>> from timeit import Timer as T
>>> T(lambda : 1234567890 / 4.0).repeat()
[0.22256922721862793, 0.20560789108276367, 0.20530295372009277]
>>> from __future__ import division
>>> T(lambda : 1234567890 / 4).repeat()
[0.14969301223754883, 0.14155197143554688, 0.14141488075256348]
>>> T(lambda : 1234567890 * 0.25).repeat()
[0.13619112968444824, 0.1281130313873291, 0.12830305099487305]

그리고 또한:

>>> from math import sqrt
>>> T(lambda : sqrt(1234567890)).repeat()
[0.2597470283508301, 0.2498021125793457, 0.24994492530822754]
>>> T(lambda : 1234567890 ** 0.5).repeat()
[0.15409398078918457, 0.14059877395629883, 0.14049601554870605]

나는 그것이 파이썬이 C로 구현되는 방식과 관련이 있다고 가정하지만, 왜 그렇게되는지 설명해 줄 사람이 있는지 궁금합니다.


귀하의 질문에 대해 수락 한 답변 (실제 질문에 대한 답변이라고 가정)은 질문 제목과별로 관련이 없습니다. 지속적인 접기와 관련이 있도록 편집 할 수 있습니까?
Zan Lynx 2011

1
@ZanLynx-안녕하세요. 명확히 하시겠습니까? 나는 질문 제목이 내가 알고 싶은 것을 정확하게 표현하고 (왜 X가 Y보다 빠르다) 내가 선택한 답이 정확히 일치한다는 것을 발견했다 ... 나에게 완벽하게 일치하는 것 같지만 ... 뭔가 간과하고있는 것일까?
mac

8
곱셈과 거듭 제곱 함수는 그 특성 때문에 항상 나누기 및 sqrt () 함수보다 빠릅니다. 나누기와 근 연산은 일반적으로 일련의 더 미세하고 미세한 근사치를 사용해야하며 곱셈과 같이 정답으로 직접 갈 수 없습니다.
Zan Lynx 2011

질문 제목은 값이 모두 문자 그대로 상수라는 사실에 대해 말해야한다고 생각합니다. 이것은 답의 핵심입니다. 일반적인 하드웨어에서는 정수 및 FP 곱하기 및 더하기 / 빼기가 저렴합니다. 정수 및 FP div 및 FP sqrt는 모두 비용이 많이 듭니다 (아마도 FP mul보다 지연 시간이 3 배, 처리량은 10 배). (대부분의 CPU는 cube-root 또는 pow () 등과 달리 하드웨어에서 이러한 작업을 단일 asm 명령으로 구현합니다.)
Peter Cordes

1
하지만 파이썬 인터프리터 오버 헤드가 여전히 mul과 div asm 명령어의 차이를 왜소하게 만든다면 놀라지 않을 것입니다. 재미있는 사실 : x86에서 FP 분할은 일반적으로 정수 분할보다 성능이 높습니다. ( agner.org/optimize ). Intel Skylake의 64 비트 정수 분할은 지연 시간이 42-95주기, 32 비트 정수의 경우 26주기, 배정 밀도 FP의 경우 14주기입니다. (64 비트 정수 곱하기는 3주기 대기 시간, FP mul은 4입니다). 처리량 차이는 훨씬 더 큽니다 (int / FP mul 및 add는 모두 클럭 당 하나 이상이지만 division 및 sqrt는 완전히 파이프 라인되지 않습니다.)
Peter Cordes

답변:


114

결과에 대한 (다소 예상치 못한) 이유는 Python이 부동 소수점 곱셈과 지수를 포함하는 상수 표현식을 접는 것처럼 보이지만 나누는 것은 아닙니다. math.sqrt()바이트 코드가없고 함수 호출을 포함하므로 완전히 다른 짐승입니다.

Python 2.6.5에서 다음 코드 :

x1 = 1234567890.0 / 4.0
x2 = 1234567890.0 * 0.25
x3 = 1234567890.0 ** 0.5
x4 = math.sqrt(1234567890.0)

다음 바이트 코드로 컴파일됩니다.

  # x1 = 1234567890.0 / 4.0
  4           0 LOAD_CONST               1 (1234567890.0)
              3 LOAD_CONST               2 (4.0)
              6 BINARY_DIVIDE       
              7 STORE_FAST               0 (x1)

  # x2 = 1234567890.0 * 0.25
  5          10 LOAD_CONST               5 (308641972.5)
             13 STORE_FAST               1 (x2)

  # x3 = 1234567890.0 ** 0.5
  6          16 LOAD_CONST               6 (35136.418286444619)
             19 STORE_FAST               2 (x3)

  # x4 = math.sqrt(1234567890.0)
  7          22 LOAD_GLOBAL              0 (math)
             25 LOAD_ATTR                1 (sqrt)
             28 LOAD_CONST               1 (1234567890.0)
             31 CALL_FUNCTION            1
             34 STORE_FAST               3 (x4)

보시다시피 곱셈과 지수는 코드가 컴파일 될 때 완료되기 때문에 시간이 전혀 걸리지 않습니다. 분할은 런타임에 발생하므로 시간이 더 걸립니다. 제곱근은 네 가지 중 가장 계산 비용이 많이 드는 작업 일뿐만 아니라 다른 작업이 수행하지 않는 다양한 오버 헤드 (속성 조회, 함수 호출 등)를 발생시킵니다.

상수 접기의 효과를 제거하면 곱셈과 나눗셈을 분리 할 수 ​​없습니다.

In [16]: x = 1234567890.0

In [17]: %timeit x / 4.0
10000000 loops, best of 3: 87.8 ns per loop

In [18]: %timeit x * 0.25
10000000 loops, best of 3: 91.6 ns per loop

math.sqrt(x)실제로 x ** 0.5후자의 특수한 경우이므로 오버 헤드에도 불구하고보다 효율적으로 수행 할 수 있기 때문에 실제로 .

In [19]: %timeit x ** 0.5
1000000 loops, best of 3: 211 ns per loop

In [20]: %timeit math.sqrt(x)
10000000 loops, best of 3: 181 ns per loop

편집 2011-11-16 : 상수 표현 접기는 파이썬의 구멍 최적화 프로그램에 의해 수행됩니다. 소스 코드 ( peephole.c)에는 상수 나눗셈이 접히지 않는 이유를 설명하는 다음 주석이 포함되어 있습니다.

    case BINARY_DIVIDE:
        /* Cannot fold this operation statically since
           the result can depend on the run-time presence
           of the -Qnew flag */
        return 0;

-Qnew플래그는 PEP 238에 정의 된 "진정한 분할"을 활성화합니다 .


2
아마도 0으로 나누는 것에 대해 "보호"하는 것일까 요?
hugomg

2
@missingno : 컴파일 타임에 두 인수를 모두 알고 있기 때문에 이러한 "보호"가 필요한 이유가 명확하지 않으며 결과도 마찬가지입니다 (+ inf, -inf, NaN 중 하나).
NPE 2011

13
상수 폴딩이 작동 /파이썬 3에서, 그리고 함께 //파이썬 2와 3에 따라서 가장 가능성이 사실의 결과이다 /상수 계산이 완료 어쩌면 때 파이썬 2에 다른 의미를 가질 수 있습니다, 그것은 여부를 아직 알려져 있지 않다 from __future__ import divisionIS 사실상?
interjay 2011

4
@aix- 1./0.in Python 2.7은 결과가 NaN아니라 ZeroDivisionError.
detly

2
@Caridorc : Python이 바이트 코드 (.pyc 파일)로 컴파일 된 다음 Python 런타임에 의해 해석됩니다. 바이트 코드는 어셈블리 / 머신 코드 (예를 들어 C 컴파일러가 생성하는 코드)와 동일하지 않습니다. dis 모듈은 주어진 코드 조각이 컴파일되는 바이트 코드를 검사하는 데 사용할 수 있습니다.
Tony Suffolk 66
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.